Skip to content

Don't apply enclosing struct captures when a branch fails - #461

Open
hdimer wants to merge 2 commits into
alecthomas:masterfrom
hdimer:fix/216-branch-capture-leak
Open

Don't apply enclosing struct captures when a branch fails#461
hdimer wants to merge 2 commits into
alecthomas:masterfrom
hdimer:fix/216-branch-capture-leak

Conversation

@hdimer

@hdimer hdimer commented Jul 31, 2026

Copy link
Copy Markdown

Fixes #216.

strct.Parse flushed the entire deferred-capture queue when its expression failed (ctx.Apply()), applying captures that belong to enclosing structs. When a lookahead disjunction later discards that branch, those captures have already been written, e.g. the Bad field in the repro ends up true.

Each struct now snapshots its start offset into the queue and applies only its own captures, on both the success and failure paths, leaving enclosing structs' captures for those structs to apply. Removed the now-unused Apply().

One behaviour change worth flagging: because an inner struct no longer flushes its enclosing structs' pending captures, the global order of setField calls across nested structs shifts. Field order within a struct is unchanged, so this is only observable through a Capture()/UnmarshalText() implementation with side effects.

Test: TestIssue216, the reporter's minimal repro. It fails on the unfixed parser with Bad: true and passes with the fix.

Used AI assistance on this; I reviewed and tested it.

strct.Parse flushed the entire deferred-capture queue when its
expression failed, applying captures belonging to enclosing structs. A
lookahead disjunction that later discards the branch has then already
written them (e.g. the Bad field in the issue's repro ends up true).

Each struct now snapshots its start offset into the queue and applies
only its own captures, on both the success and failure paths, leaving
enclosing captures for their own structs to apply.

Fixes alecthomas#216

@Pastalikek65 Pastalikek65 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.

Reviewed and tested locally against master: both new tests pass (TestStructCapturesInBadBranchBad is now correctly false instead of leaking true, and B: 4 — and TestNestedStructCaptureNoPanic), and the full go test ./... suite plus go vet are clean.

The approach is sound: scoping Apply to the struct\x27s own queue offset (ApplyFrom) is the minimal way to stop discarded lookahead branches from flushing enclosing structs\x27 deferred captures, and the queue truncation (p.apply[:start]) keeps the "apply exactly once" invariant. The offset-snapshot also guards the nested-struct edge case correctly.

Two small notes (non-blocking):

  • ctx.Apply() callers were removed entirely — worth a quick grep in follow-ups that nothing else relied on the full-queue flush semantics.
  • The out == nil (optional struct) path still leaves deferred entries in the queue; safe today because the top-level struct ultimately flushes from offset 0, but a comment noting that invariant would help future maintainers.

Otherwise this looks correct and ready to merge. Fixes #216 as far as I can verify.

Scope TestIssue216 to the repo's naming and local-type idiom, and remove
TestNestedStructCaptureNoPanic: it passes against the unfixed parser, so
it never guarded alecthomas#216, and six existing tests already cover ApplyFrom at
a non-zero offset (TestPosInjection goes deeper).

Trim the duplicated rationale from ApplyFrom's doc comment, and note on
the no-match path that the queue is back at applyStart.
@hdimer

hdimer commented Aug 14, 2026

Copy link
Copy Markdown
Author

Thanks for the review.

On the first note: grepped, nothing else relied on the full flush. The only other place the queue moves is Accept, which appends an accepted branch's entries onto the parent, so they land at or after the in-progress struct's offset and that struct still applies them.

On the second: added a line there, but narrower than "nothing was deferred". A (@Ident 'x')+ that starts matching and then bails does defer a capture, it just dies with the discarded branch. The queue is back at applyStart either way, which is the part that matters, so that's what the comment says.

Separately, I dropped TestNestedStructCaptureNoPanic. Put the old context.go/nodes.go back and ran it: it passes against the unfixed parser, so it was never guarding #216. TestPosInjection and five other existing tests already exercise ApplyFrom at a non-zero offset, one of them deeper than that test did. Renamed the remaining one TestIssue216 and moved its types inside the function to match the rest of the file.

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.

Struct capture wrongly applying previous captures in a failed branch

2 participants