Skip to content

go/post binding - #957

Closed
jdx wants to merge 2 commits into
go/shadowfrom
go/post-binding
Closed

go/post binding#957
jdx wants to merge 2 commits into
go/shadowfrom
go/post-binding

Conversation

@jdx

@jdx jdx commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Stack created with GitHub Stacks CLIGive Feedback 💬


Note

Medium Risk
Changes how argv values are validated and defaulted after binding, with broad conformance coverage; mistakes could alter CLI error behavior and env/default semantics, though binding itself stays unchanged and well unit-tested.

Overview
Adds a post-binding layer in post.go that stays off the parse hot path: a cold Meta / Metadata table keyed like flags and args, plus Fill (argv → env → default) and Check (required, choices, var_min, var_max). Parse-time argv.Error gains matching codes and fields.

Spec lowering now builds parse tables and metadata together via Spec.Build(), including defaults on nested flag args (but not nested env, aligned with usage-lib).

Conformance runs post-binding vectors with env-aware Fill/Check instead of skipping the whole layer; 145/152 corpus vectors pass. Seven ids stay in an explicit notYet map for conflicts, overrides, and required_unless, with a test that skipped count matches that list.

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

jdx and others added 2 commits August 17, 2026 02:00
Binding says which token becomes which flag or argument. This says whether what
landed is acceptable, and fills in what the command line left empty. The corpus
goes from 122 answered to 145 of 152.

`required`, `choices`, the `env`-then-`default` fallback, `var_min` and `var_max`
all need to know something no single token can tell you, which is why they were
left out of the parser rather than overlooked. They read a second, cold table —
`Meta`, indexed by the same key the parse table carries, so the two cannot drift
on identity — and a program that never applies them never touches it. The
zero-allocation test covers a parse with metadata present, since the property
that matters is that binding does not reach for any of this.

Deliberately not a framework. `Fill` and `Check` are pure functions over what
binding produced, because the caller is the one that knows how it accumulated: a
generated struct assigns to a field, a harness with no target type appends to a
slice. Inventing a value model here would force both through it, and this crate
has spent four PRs not doing that to the parser either.

Three things the corpus settled that guessing would have got wrong:

An environment variable set to the empty string is set — `EX_JOBS=` is a value,
because treating empty as unset would make it mean something no other empty value
in the grammar means. The value is one token, never re-split: quoting is the
shell's job and there was no shell here at all.

A flag that holds no value reads its variable as a yes or a no, and the rule is
an allow-list — `1`, `true`, `True`, `TRUE` — matching usage-lib exactly. So
`yes`, `on` and `TrUe` are all false, which is worth pinning rather than
discovering: `EX_VERBOSE=0` meaning verbose would be a trap. The corpus pins the
four cases it cares about and `TestEnvTruth` records the rest.

`var_max` counts occurrences here, never values. A variadic's per-occurrence
bound is a limit binding applies, so judging the total again afterwards would
fail an invocation that never broke it.

The seven vectors still unanswered are relationships *between* flags —
`conflicts`, `overrides`, `required_unless` — which need a name resolved to the
entry it refers to. They are listed by id with a reason rather than inferred from
the spec, because `overrides-loser-is-not-refilled-from-env` is as much an env
question as an overrides one and inference would have exempted vectors nobody
meant to exempt. The skip count is asserted against the list, so it cannot become
a way of hiding failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…does

A default can be written in two places, and only one was being read:

    flag "--jobs <n>" {
        arg "<n>" default="4"
    }

usage-lib falls back to the nested one — `lib/src/parse.rs` says so in as many
words, and a live parse of exactly that spec prints `jobs=4` — so a Go CLI
generated from the same spec has to as well, or the two disagree about what a
bare invocation means. `choices` was already read through the same nesting,
which is what made the gap look arbitrary.

The flag's own default wins where both are written, that being the narrower
declaration.

`env` deliberately does not follow, and the asymmetry is the interesting part.
usage-lib does not read a nested `env` either: with `arg "<m>" env="EX_MODE"`
inside a flag, `EX_MODE=turbo` leaves the flag unset. Reading it here would be
this implementation inventing behaviour the reference does not have, which is
worse than the inconsistency. Checked rather than assumed — the review that
raised the default said nothing about `env`, and it would have been easy to
"fix" both and diverge.

Both halves are tested, along with the tie between the two tables: every entry's
metadata must describe that entry and no other, since they are separate data
joined only by key.

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

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d28b501-8525-43ae-9f84-29ddc9d2a5e8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

jdx commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Closing: gh stack submit re-proposed this branch after #931, #932 and #943 had already merged, so this is a duplicate of work that is on main. The two PRs that carry new work — #958 and #959 — are being rebased onto main and will remain open.

This comment was generated by Claude Code.

@jdx jdx closed this Aug 17, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit de6de50. Configure here.

// The text has nowhere to go, so it is read as a yes or a no.
return argv.EnvTruth(values[0]), true
case argv.FromDefault:
return len(values) > 0 && values[0] == "true", true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default truthiness mismatches env

Medium Severity

Boolean flags filled from a default only treat the literal true as set, while the env path uses EnvTruth, which also accepts 1, True, and TRUE. usage-lib applies that same allow-list to defaults, so a default of 1 or TRUE would disagree between the two implementations.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit de6de50. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds Go post-binding metadata, fallback handling, and validation for required values, choices, and variadic bounds, enabling most post-binding conformance vectors.

  • Adds cold metadata keyed to parser declarations.
  • Implements argv/environment/default precedence and post-binding checks.
  • Extends the Go conformance harness while retaining explicit skips for unsupported flag relationships.
  • Adds unit and conformance coverage and updates the Go documentation.

Confidence Score: 4/5

The shadowing defect should be fixed before merging because valid subcommand invocations can receive false required errors or values from inaccessible global declarations.

The parser correctly resolves a shadowing subcommand flag, but the new post-binding traversal independently processes the shadowed ancestor declaration and can validate or render it anyway.

Files Needing Attention: go/conformance/conformance_test.go

Important Files Changed

Filename Overview
go/argv/post.go Adds focused fallback and post-binding validation helpers whose implemented precedence and constraint checks match the supported corpus semantics.
go/internal/spec/spec.go Builds aligned hot and cold declaration tables and maps lowered post-binding fields into metadata.
go/conformance/conformance_test.go Enables post-binding conformance behavior, but validates shadowed ancestor declarations that are no longer in scope.
go/argv/argv.go Extends the shared error model with post-binding codes and structured details.
go/argv/post_test.go Covers fallback precedence, required and choice checks, bounds, environment truth values, and metadata lookup.
go/internal/spec/spec_test.go Verifies nested defaults, environment behavior, choices, and metadata identity alignment.

Fix all with Greploop

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(go): read a flag's default from the ..." | Re-trigger Greptile

Comment on lines +244 to +245
for _, cmd := range path {
for _, f := range cmd.Flags {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shadowed globals remain validated

When a subcommand redeclares an ancestor global flag, parsing selects the subcommand declaration but this loop still fills and checks both declarations, causing a shadowed required global to produce a false missing-required error or a shadowed fallback to populate the subcommand flag's output name.

Knowledge Base Used: Compiled argv parsing and derives

Fix in Claude Code

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