Skip to content

perf: keep rarely declared flag metadata out of every flag's table - #1470

Merged
jdx merged 1 commit into
mainfrom
claude/flag-meta-extra
Sep 23, 2026
Merged

jdx merged 1 commit into
mainfrom
claude/flag-meta-extra

Conversation

@jdx

@jdx jdx commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

https://entire.io/gh/jdx/usage/trails/22

Every flag in a usage-rs CLI gets a static FlagMeta record in the binary. Before this change it was 664 bytes: 58 fields, nearly all empty for a typical flag, which usually declares only help, a heading and maybe a default or choices. With oxlint's 47 flags plus the built-in help/version entries, that was ~35 KB of mostly zeros. After #1469, it was the largest remaining part of what usage-rs adds to oxlint and oxfmt (oxc-project/oxc#26027).

What changed

The 35 rarely declared fields move into a new FlagExtra struct behind FlagMeta::extra. They cover:

  • relations: conflicts, requires*, required_if*, required_unless*, overrides, exclusive, default_if;
  • deprecation, including deprecated_env;
  • validation;
  • value bounds and delimiter;
  • hidden aliases, display_order, admonitions, value_names, env_fallback, hide_env_values;
  • spec-only metadata: choice_aliases, choice_details, complete_type, surface, available_if, effect.

FlagMeta itself is now 168 bytes.

A flag that declares none of these points at one shared static, NO_FLAG_EXTRA. The derive can't always tell at expansion time whether a flag's extras are empty, because choice aliases can come from a ValueEnum's trait constants. So it builds each flag's extras as a const, and the const fn FlagExtra::shared swaps an empty one for the shared static at compile time. shared destructures every field without .., so adding a field to FlagExtra without updating the check is a compile error, not silently dropped metadata.

For code that uses these types directly

Derived CLIs need no changes. Code that reads a moved field goes through extra:

// before
if let Some(reason) = meta.deprecated { /* … */ }
// after
if let Some(reason) = meta.extra.deprecated { /* … */ }

Hand-written tables set the moved fields in a nested literal. In a static the reference is promoted, as with the table's other slices:

FlagMeta {
    flag: &OLD,
    help: Some("Use the old mode"),
    extra: &FlagExtra {
        deprecated: Some("use --new"),
        ..FlagExtra::EMPTY
    },
    ..FlagMeta::EMPTY
}

FlagMeta::EMPTY already points at the shared instance, so ..FlagMeta::EMPTY keeps working for flags with no extras.

Measured effect

Measured on oxc's PR branch, with its usage crates patched to this branch, using oxc's release profile (opt-level = 3, fat LTO) on linux x86_64. Numbers are summed allocated sections of the stripped binaries, compared with #1469 alone:

oxlint oxfmt
.data.rel.ro −42,624 B −14,880 B
.rela.dyn (one pointer per flag) +1,992 B +624 B
.text + .rodata +736 B +624 B
Total −39,896 B −13,632 B

Together with #1469, usage-rs's overhead over bpaf drops from +219,300 B to +138,372 B on oxlint (−37%) and from +169,148 B to +114,284 B on oxfmt (−32%).

Output is unchanged. On both oxc binaries, I compared help (plain and coloured), --version, error messages, __usage_spec__ and completions before and after, and all 24 cases were byte-identical. The workspace suite passes (2,800 tests), with no snapshot changes, and clippy is clean. A new test pins that empty extras resolve to the shared instance and that anything declared, even a lone bool, keeps its own copy.

ArgMeta and CommandMeta keep their layout. oxlint has one positional and ten command records, so the same split would save a few KB at most.

🤖 Generated with Claude Code


Note

Medium Risk
Wide refactor of flag metadata layout and every consumer path (help, completion, spec emission, derive); behavior should be identical but any missed extra. migration would be a subtle runtime bug.

Overview
Shrinks per-flag static metadata by moving ~35 rarely set fields (relations, deprecation, validation, hidden aliases, spec-only choice/completion data, effects, etc.) from inline FlagMeta into a new FlagExtra, referenced by FlagMeta::extra. Common flags with no extras point at a single shared NO_FLAG_EXTRA; derive and table builders construct a per-flag FlagExtra and run FlagExtra::shared so empty extras collapse to that static at compile time.

Call sites in completion, help rendering, KDL/spec emission, conformance table building, and tests now read those fields through meta.extra.* instead of on FlagMeta directly. help_heading stays on FlagMeta. Hand-written FlagMeta literals nest an extra: &FlagExtra { … } block; generated CLIs are unchanged at the source level.

No intended behavior change—same help, completions, and spec output—with the goal of much smaller .data.rel.ro in large CLIs.

Reviewed by Cursor Bugbot for commit e3c76b6. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Refactor
    • Command-line help, completion, and flag behavior remain unchanged. Existing flag details, such as help text, choices, visibility, and deprecation notices, continue to appear as before.
    • No new end-user features or behavior changes are included in this update.

FlagMeta was 664 bytes per flag, nearly all of it empty for a typical
flag. Relations, deprecation, validation, bounds and spec-only metadata
now live in a FlagExtra behind `FlagMeta::extra`. Derived tables build
each flag's extras as a constant and `FlagExtra::shared` swaps an empty
one for the single NO_FLAG_EXTRA static, so a flag that declares none of
these costs a pointer instead of ~500 bytes.

Code that reads a moved field goes through `extra` (`meta.extra.deprecated`),
and hand-written tables set them inside `extra: &FlagExtra { .. }`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Advanced

Run ID: c478ccc7-e2b8-43d0-b5e7-5bbdc46ded76

📥 Commits

Reviewing files that changed from the base of the PR and between 7abcdbf and e3c76b6.

📒 Files selected for processing (8)
  • argv/src/complete.rs
  • argv/src/help.rs
  • argv/src/spec.rs
  • cli/src/command_effects.rs
  • conformance/src/tables.rs
  • conformance/tests/clause.rs
  • conformance/tests/spec_roundtrip.rs
  • derive/src/codegen.rs

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Flag metadata that was previously stored directly in FlagMeta now resides in FlagExtra, referenced by FlagMeta.extra. Generated metadata and help, completion, and serialization code use the nested fields. Test fixtures and assertions reflect the updated structure.

Changes

Flag metadata consolidation

Layer / File(s) Summary
FlagExtra contract and generation
argv/src/spec.rs, derive/src/codegen.rs
FlagMeta gains an extra reference to FlagExtra. Generated metadata uses FlagExtra::shared, which returns the shared empty static when all extra fields are empty.
Runtime metadata readers and output
argv/src/spec.rs, argv/src/complete.rs, argv/src/help.rs
KDL emission, completion, and help rendering read relocated metadata through extra. The summaries report that emitted KDL forms and help rendering behavior remain unchanged.
Fixtures and metadata assertions
argv/src/complete.rs, argv/src/help.rs, cli/src/command_effects.rs, conformance/src/tables.rs, conformance/tests/*
Fixtures and assertions use the nested metadata fields. Tests check shared empty extras and access completion, requirements, and effects through extra.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Suggested reviewers: lu-zero

Merge Risk: ⚪ Minimal · up to e3c76

This change shrinks each flag's metadata table by moving rarely declared settings into a separate record that flags without such settings share. Help output, shell completion, and spec emission read the same values as before. The reported unchanged snapshots and passing tests are consistent with no behavior change. No merge-blocking risk remains; code that hand-writes flag tables must nest the moved fields under extra.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 37 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: moving rarely declared flag metadata out of each flag's table for performance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no concrete correctness, security, or repository-rule issue identified.

Summary

This PR reduces static binary metadata size by moving rarely populated flag properties from FlagMeta into a shared, pointer-backed FlagExtra.

  • Adds an exhaustively checked shared empty FlagExtra instance.
  • Updates derive-generated and conformance-generated flag tables to populate nested extras.
  • Redirects help, completion, spec serialization, and tests to the new metadata layout.
  • Preserves non-empty extras while allowing ordinary flags to share one empty instance.

Reviews (1) · Last reviewed commit: "perf: keep rarely declared flag metadata..."

@github-actions

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▂▁▁▂▁█▇██▇▇███ 285,420,335 → 285,473,014 +0.02% 50.44 → 49.98ms -0.91%
startup ▁▁▁▁▁▁▇▇▇▇▇▇▇▇█ 1,061,172 → 1,068,793 +0.72% 1.40 → 1.44ms +3.18%

No instruction-count regression above 1%.

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

Shadow comparison

Parsing mise use -g node@20 against a shadow of mise's committed spec.
Reported, not gated: the shadow grows as the derive learns to express more, so
what to watch is the ratio rather than either column.

framework stripped binary, bytes
usage 1331504
bpaf 2494656
clap 3100904
framework instructions, cold parse vs usage
usage 8508 —
clap 6314907 742x
bpaf 21909296 2575x
                                              min       p01       p10    median
usage-rs: argv -> struct                      768       775       784       798  ns
clap: build tree + parse -> struct        1237914   1242331   1252718   1271694  ns
bpaf: build parser + parse -> struct      3376569   3376569   3414698   3491876  ns

usage: argv -> struct                             818 ns      0.82 µs
clap: build tree + parse -> struct            1286053 ns   1286.05 µs
clap: parse -> struct, tree reused              50420 ns     50.42 µs
clap: build tree only                          750713 ns    750.71 µs

e3c76b6bde4f vs 7abcdbfc0b78 · measured on the runner, not pushed to the history.

jdx added a commit that referenced this pull request Sep 23, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/23
<!-- entire-trail-link-end -->

oxc is moving oxlint and oxfmt from bpaf to usage-rs
(oxc-project/oxc#26027), and reviewers weigh the binary size usage-rs
adds. This PR takes about 10 KB off each binary without changing any
output.

## What changed

The short (`-h`) and long (`--help`) help pages were produced by two
near-identical functions. Each passed its own closures for arguments,
flags and global flags, so there were about six inlined copies of the
same entry logic: layout, annotations, admonitions, deprecation notes,
and the `next_line_help` branch. The flattened subcommand bodies
(`flatten_help`) had a short copy and a long copy of the same code
again.

Now:

- One `page_sections(…, long)` builds both pages. It covers the version
banner, about text, commands, arguments, flags, global flags, examples,
`after_help` and the package footer.
- One `flat_commands(…, long)` writes the flattened bodies for both
pages.
- Every argument or flag row is written by a single non-generic
`write_row`. It takes a small `Row` (usage, help text,
choices/env/defaults with the `hide_*` switches already applied,
admonitions, deprecation) and a layout that says which page it is on.

This is internal to `usage-argv`. No public API changes.

## Measured size

oxc's PR branch was patched to use this usage branch and built with
oxc's release profile (opt-level 3, fat LTO, 1 codegen unit) on linux
x86_64. The numbers are the summed allocated sections of the stripped
binaries. The baseline is `main` with #1470 applied.

| binary | vs #1470 | overhead vs bpaf |
| ------ | -------: | ---------------: |
| oxlint | −10,352 B | +128,020 B (was +138,372 B) |
| oxfmt  | −10,480 B | +103,804 B (was +114,284 B) |

Most of the saving is in `.text` (−9,136 B), with a further −1,056 B in
`.eh_frame`.

Help, error, spec and completion output from both binaries was compared
byte for byte against the #1470 build. That comparison covered plain,
`CLICOLOR_FORCE=1` and `COLUMNS=60` output: 30 of 30 cases were
identical.

## Validation

- `cargo test --workspace --all-features`: 2,800 tests pass, including
the existing plain and coloured help snapshots, with no snapshot
changes.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Large refactor of help layout and visibility rules; regressions in
`-h` vs `--help` formatting are possible even though tests and byte
comparisons reported no output changes.
> 
> **Overview**
> Refactors **usage-argv** help generation so `-h` and `--help` share
one code path instead of duplicated `short_sections` / `long_sections`
and `flat_commands_short` / `flat_commands_long`.
> 
> **`page_sections(…, long)`** now builds both pages (same section
order; differs via `long` for text choice, `hide_short_help` /
`hide_long_help`, long about/examples/footer, etc.). **`flat_commands(…,
long)`** does the same for `flatten_help` bodies.
> 
> Argument and flag rows go through **`Row`**, **`RowLayout`**, and a
single **`write_row`** (replacing repeated closures and
**`flag_notes`**). **`long_annotations`** takes a **`Row`**;
**`inline_environment_notes`** no longer takes a hide flag because
**`Row`** already strips hidden env fields.
> 
> Call sites (`rendered_page`, `short_help_with`, `long_help_with`,
topics) all route through **`page_sections`**. Intended effect: smaller
binaries (~10 KB in oxc’s measurement) with **unchanged** help output.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f1d1085. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Help pages continue to provide short and long formats, including
relevant examples and explanatory content. The underlying rendering has
been consolidated, with no changes to the public interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Sep 23, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/28
<!-- entire-trail-link-end -->

oxc is moving oxlint and oxfmt from bpaf to usage-rs
(oxc-project/oxc#26027), and reviewers weigh the binary size usage-rs
adds. This PR takes about 3 KB more off each binary, on top of #1471,
without changing any output.

## What changed

To colour a help page, usage-argv looks for what the page wrote: its
headings, the usage at the start of each row, and the synopsis. It
colours each line that matches one of them. Those lists came from
`help_structure`, which walked the command metadata a second time after
the page was written. That second walk repeated every visibility filter,
sort order and usage string the section writers had just produced,
including a recursive copy for flattened help.

Now the section writers note each heading and row usage as they write
it, but only when the page is coloured, so plain pages pay nothing. The
synopsis is read from the page's own `usage` section. `help_structure`,
`flat_help_usages` and `flat_help_headings` are gone. As a side effect,
each row's usage string is computed once instead of twice for the column
width and the row text.

Line matching (`styled_help`) is unchanged. It still colours author
prose that happens to match a heading or a row: for example, a line
reading `Flags:` in `before_help` is coloured as a heading. Colouring
spans directly as the page is written would drop that, which would
change coloured output, so this PR leaves it as it is.

## Measured size

oxc's PR branch was patched to use this usage branch and built with
oxc's release profile (opt-level 3, fat LTO, 1 codegen unit) on linux
x86_64. The numbers are the summed allocated sections of the stripped
binaries.

| binary | vs #1470 | vs #1471 | overhead vs bpaf |
| ------ | -------: | -------: | ---------------: |
| oxlint | −13,304 B | −2,952 B | +125,068 B (was +138,372 B) |
| oxfmt  | −13,432 B | −2,952 B | +100,852 B (was +114,284 B) |

Help, error, spec and completion output from both binaries was compared
byte for byte against the #1470 build. That comparison covered plain,
`CLICOLOR_FORCE=1` and `COLUMNS=60` output: 30 of 30 cases were
identical.

Separately, plain, coloured and palette-remapped short and long help was
rendered for every command of every KDL spec in the repo, plus two
fixtures written for this PR. That was about 3,500 pages at widths 100
and 50, and the output was identical across #1470, #1471 and this
branch.

## Tests

`conformance/tests/coloured_help.rs` adds coloured snapshots for cases
the existing tests did not pin:

- global flags
- a flattened page
- next-line help
- a logo in the margin
- a help template that reorders and splits sections
- author prose that looks like a heading

The snapshots were recorded on #1471 and pass unchanged here.

## Validation

- `cargo test --workspace --all-features`: 2,804 tests pass.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`
is clean.

Stacked on #1471.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches the core help assembly and colouring path, though behaviour is
intended to stay byte-identical and is heavily snapshotted and
regression-tested.
> 
> **Overview**
> Coloured help no longer runs a second metadata pass via
**`help_structure`** (and the related **`flat_help_*`** helpers). While
**`page_sections`** builds the page, it now records headings and row
usage strings in **`Sections.known`** only when output is coloured; the
synopsis for styling comes from the already-written **`usage`** section.
> 
> Section writers (**`commands_section`**, **`flat_commands`**, grouped
sections via **`SectionSink`**) push into that structure as they emit
lines, and arg/flag usage strings are computed once for both column
sizing and row text. **`styled_help`** still uses the same recognition
rules (including author prose that looks like headings).
> 
> Adds **`conformance/tests/coloured_help.rs`** with insta snapshots for
globals, flattened help, next-line layout, logos, templates, and prose
edge cases.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
eca35c5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Sep 23, 2026
…ble (#1472)

<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/24
<!-- entire-trail-link-end -->

oxc is moving oxlint and oxfmt from bpaf to usage-rs
(oxc-project/oxc#26027), and reviewers weigh how much binary size
usage-rs adds. Much of that is static metadata. Each `CommandMeta` was
about 600 bytes. A derived CLI emits one for every subcommand and every
flattened `Args` type, so oxlint has about 11. Nearly every field in
them was empty.

This applies the #1470 `FlagMeta` → `FlagExtra` split to commands.
`CommandMeta` keeps only what nearly every command sets: `cmd`, `about`,
`long_about`, `hide`, `args_override_self`, `flags`, `args`,
`subcommands`, `groups`, `flatten_groups`. Everything else is now in a
`CommandExtra` behind `CommandMeta::extra`. That covers deprecation,
hidden aliases, heading and display order, surface/`available_if`,
effect, mount, restart token, clause, subcommand settings,
`next_line_help`/`flatten_help`, term widths, before/after help,
examples, headings, outputs, `select`, and exit codes.

Derived tables build each command's extras as a constant.
`CommandExtra::shared` then replaces an empty one with the single
`NO_COMMAND_EXTRA` static, so a command that declares none of these
costs one pointer. `shared` destructures every field without `..`, so a
field added later and missed in the emptiness check is a compile error,
not a silently dropped declaration. A test pins the sharing.

## Size

| binary | vs #1470 | overhead vs bpaf |
|---|---|---|
| oxlint | −3,608 B | +134,764 B (was +138,372) |
| oxfmt | −1,488 B | +112,796 B (was +114,284) |

How this was measured: oxc's PR branch was patched to use this usage
branch and built with oxc's release profile (opt-level 3, fat LTO, 1
codegen unit) on linux x86_64. The numbers are the summed allocated
sections of the stripped binaries. Most of the saving is in
`.data.rel.ro` (−4,128 B oxlint, −1,824 B oxfmt). Help, error, spec and
completion output from both binaries was compared byte-for-byte against
the #1470 build, and all 30 cases were identical.

`ArgMeta` was left alone. oxlint and oxfmt each have one positional, so
splitting it would save a few hundred bytes at most.

## Migration

Code that reads or builds `CommandMeta` directly has to go through
`extra` for the moved fields. Derived CLIs need no changes.

```rust
// before
let text = meta.after_long_help;
static ROOT: CommandMeta = CommandMeta {
    cmd: &CMD,
    about: Some("…"),
    deprecated: Some("use `new`"),
    ..CommandMeta::EMPTY
};

// after
let text = meta.extra.after_long_help;
static ROOT: CommandMeta = CommandMeta {
    cmd: &CMD,
    about: Some("…"),
    extra: &CommandExtra {
        deprecated: Some("use `new`"),
        ..CommandExtra::EMPTY
    },
    ..CommandMeta::EMPTY
};
```

## Validation

`cargo test --workspace --all-features` passes (2,801 tests, no snapshot
changes), and `cargo clippy --workspace --all-targets --all-features --
-D warnings` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Layout/API refactor across help, completion, and spec emission with
broad test coverage; runtime CLI behavior should be unchanged if all
`extra` paths were migrated consistently.
> 
> **Overview**
> Introduces **`CommandExtra`** and moves rarely set command metadata
(deprecation, hidden aliases, clause/restart tokens, help layout,
examples, outputs, effects, etc.) off **`CommandMeta`** behind
`meta.extra`, mirroring the existing **`FlagExtra`** pattern.
**`CommandMeta`** keeps the common parse/help surface (`flags`, `args`,
`subcommands`, `hide`, …) so each static table is smaller; derived CLIs
call **`CommandExtra::shared`** to point empty extras at a single
**`NO_COMMAND_EXTRA`** static.
> 
> All readers/writers are updated to use `extra.*`: shell
**completion**, **help** rendering and usage lines, **`Spec::to_kdl`**,
conformance **KDL→tables** building, **usage-dynamic** collision checks,
and **derive** codegen (root/subcommand/enum variant metadata).
Hand-built **`CommandMeta`** in tests and fixtures nest moved fields
under `CommandExtra { .. }`.
> 
> **Breaking for direct `CommandMeta` users only:** field access becomes
`meta.extra.after_help` (etc.); behavior and emitted
help/spec/completions are intended to stay the same.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a2f8dbb. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Updated how command settings are organized across command definitions
and related tools. Help text, examples, completion behavior, deprecation
details, and other settings retain their existing behavior.
* Generated commands and specifications continue to preserve their
existing metadata and help-page output.
* **User Impact**
* No changes to command-line behavior or help-page content are
indicated.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Sep 23, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/26
<!-- entire-trail-link-end -->

Every `#[derive(Cli)]` / `#[derive(Args)]` emits a static `Command` and
`CommandMeta` for each command, and that table data is part of the size
usage-rs adds to a CLI. That size is under review in the oxlint/oxfmt
migration from bpaf (oxc-project/oxc#26027). Two things made these
records bigger than they needed to be:

- **The clause was stored inline.** `Command::clause` held a whole
`Clause` (72 bytes) and `CommandMeta::clause` held a whole `ClauseMeta`
(104 bytes), whether or not the command declared a clause. Almost no
command does.
- **Numbers were `Option<usize>`** (16 bytes each), even though they are
small positions, bounds and widths.

This PR changes both:

| Field | Before | After |
|---|---|---|
| `Command::clause` | `Option<Clause<'a>>` | `Option<&'a Clause<'a>>` |
| `CommandMeta::clause` | `Option<ClauseMeta<'a>>` | `Option<&'a
ClauseMeta<'a>>` |
| `CommandMeta::display_order`, `ArgMeta::display_order`,
`FlagExtra::display_order` | `Option<usize>` | `Option<u32>` |
| `ArgMeta::{var_min, var_max}`, `FlagExtra::{var_min, var_max,
value_var_min, value_var_max}` | `Option<usize>` | `Option<u32>` |
| `CommandMeta::{term_width, max_term_width}` | `Option<usize>` |
`Option<u16>` |

Record sizes on x86_64: `Command` goes from 184 to 120 bytes,
`CommandMeta` from 600 to 472, and `ArgMeta` from 480 to 456. `Flag`
(144) and `FlagMeta` (168) are unchanged.

When a declared value doesn't fit, the derive and the conformance table
builder saturate it instead of wrapping, which matches how the parse
table's `var_max` already worked. A `display_order` above `u32::MAX`
sorts last, a bound that large still means "no practical limit", and a
terminal width above 65535 still never wraps. `term_width = 0` still
turns wrapping off.

### Measured effect

oxc's PR branch was patched to build against this branch, using oxc's
release profile (opt-level 3, fat LTO, 1 codegen unit) on linux x86_64.
The numbers below are summed allocated sections of the stripped
binaries, compared with #1470:

| Binary | Total | `.data.rel.ro` | `.text` | Other |
|---|---|---|---|---|
| oxlint | **−2,912 B** | −2,304 | −512 | −96 |
| oxfmt | **−1,744 B** | −1,056 | −528 | −160 |

This brings the overhead compared with bpaf from +138,372 B to +135,460
B for oxlint, and from +114,284 B to +112,540 B for oxfmt. The small
`.rodata` deltas partly come from differences in embedded source paths
between the two builds. Help, error, `--version`, spec, and completion
output from both binaries was compared byte-for-byte with the #1470
build (30 invocations), and all of it is identical.

### Migration

Hand-written tables need to borrow the clause:

```rust
// before
static RUN: Command = Command { clause: Some(TASKS), ..Command::EMPTY };
static META: CommandMeta = CommandMeta {
    cmd: &RUN,
    clause: Some(ClauseMeta { /* ... */ }),
    ..CommandMeta::EMPTY
};

// after
static RUN: Command = Command { clause: Some(&TASKS), ..Command::EMPTY };
static META: CommandMeta = CommandMeta {
    cmd: &RUN,
    clause: Some(&ClauseMeta { /* ... */ }),
    ..CommandMeta::EMPTY
};
```

Integer literals such as `display_order: Some(3)` need no change. Code
that reads these fields as `usize` needs a conversion, for example
`meta.display_order.map(|o| o as usize)`. `Event::ClauseSeparator` still
carries the `Clause` by value. Code generated by `#[derive(...)]` needs
no changes.

### Validation

`cargo test --workspace --all-features` passes (2,800 tests) with no
snapshot changes, and `cargo clippy --workspace --all-targets
--all-features -- -D warnings` is clean. The parser only follows the
clause reference when the command declares a clause. For every other
command, the check is a null test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core parser/spec metadata layout and clause handling across
derive and table builders; behavior is meant to be equivalent with
saturation edge cases, but any hand-maintained `Command` tables must be
updated.
> 
> **Overview**
> Shrinks the static `Command` / `CommandMeta` tables that every
`#[derive(Cli)]` emits by storing **clauses behind references**
(`Option<&Clause>` / `Option<&ClauseMeta>`) instead of inlining large
structs on almost every command, and by narrowing metadata integers
(`display_order` and variadic bounds to **`u32`**, terminal widths to
**`u16`**) so each record takes less space in the binary.
> 
> The parser, help renderer, completion fixtures, **derive codegen**,
and **conformance** spec→table builder are updated to use the new shapes
(including `usize` conversions at use sites). Oversized attribute or
spec values **saturate** to `u32::MAX` / `u16::MAX` instead of
wrapping—covered by new tests on 64-bit targets. Hand-written static
tables must pass `&TASKS` / `&ClauseMeta { … }` for clause fields;
generated code and observable CLI output are intended to stay the same
aside from binary size.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
55efdcb. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jdx pushed a commit that referenced this pull request Sep 23, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/usage/trails/16
<!-- entire-trail-link-end -->

### 🐛 Bug Fixes

- **(help)** colour only the structure the renderer writes by
[@jdx](https://github.com/jdx) in
[#1480](#1480)

### ⚡ Performance

- shrink the help and completion code usage-rs adds to a binary by
[@jdx](https://github.com/jdx) in
[#1469](#1469)
- keep rarely declared flag metadata out of every flag's table by
[@jdx](https://github.com/jdx) in
[#1470](#1470)
- write help rows through one renderer for both help pages by
[@jdx](https://github.com/jdx) in
[#1471](#1471)
- collect coloured-help structure while the page is written by
[@jdx](https://github.com/jdx) in
[#1476](#1476)
- shrink the per-CLI code the derive generates for binding by
[@jdx](https://github.com/jdx) in
[#1477](#1477)
- shrink the parse-error renderer usage-rs adds to a binary by
[@jdx](https://github.com/jdx) in
[#1478](#1478)
- shrink the shell-completion code a derived CLI carries by
[@jdx](https://github.com/jdx) in
[#1475](#1475)
- keep rarely declared command metadata out of every command's table by
[@jdx](https://github.com/jdx) in
[#1472](#1472)
- shrink the command and metadata tables a derive emits by
[@jdx](https://github.com/jdx) in
[#1474](#1474)

### 🔍 Other Changes

- **(ci)** comment on a discussion when the PR implementing it merges by
[@jdx](https://github.com/jdx) in
[#1460](#1460)

### 📦️ Dependency Updates

- update jdx/packslip action to v1.2.0 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#1466](#1466)
- update zizmorcore/zizmor-action action to v0.6.4 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#1465](#1465)
- update module github.com/urfave/cli/v3 to v3.12.0 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#1467](#1467)
- update aube to v2.3.0 by [@jdx](https://github.com/jdx) in
[#1473](#1473)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant