refactor: model parse() failure as a Stuck value (purify the frontier)#471
Open
SJrX wants to merge 1 commit into
Open
refactor: model parse() failure as a Stuck value (purify the frontier)#471SJrX wants to merge 1 commit into
SJrX wants to merge 1 commit into
Conversation
…ontier
Replaces the side-channel from step 3 (a mutable Frontier threaded through parse())
with a single return-value channel: parse() now yields Sequence<ParseStep>, where a
ParseStep is either a Parse (a successful match) or a Stuck (a dead end carrying the
offset it got stuck at and the set of matchers expected there).
The key reframe is that failure is a VALUE, not an absence. A matcher that can't
proceed returns a Stuck instead of an empty sequence, so the "how far did we get"
information rides back up the return value — e.g. when Seq(..., EOF()) can't finish
on "AF_INET, AF_INET6", EOF yields Stuck(7, {EOF}) and that survives. This is what
the mutable Frontier was working around; with failure as a value, both kinds of
result travel the same way and the side channel disappears.
- Parse.kt: add ParseStep (sealed) = Parse | Stuck; validate() folds the Stuck
values back into (furthest, expected). A doc note records the simpler-but-asymmetric
mutable-threading alternative for posterity.
- Combinator.parse() drops the frontier parameter; Frontier.kt is deleted.
- Combinators propagate Stucks: Seq carries a dead end forward, Alt/ZeroOrMore/
OneOrMore/Repeat surface failed attempts, terminals/EOF return Stuck on no-match.
validate()'s public result (ParseOutcome) is unchanged, so ParseTest passes as-is:
"AF_INET, AF_INET6" still reports furthest=7 expected={whitespace, EOF}, and the
empty value still reports the family/none/~ completion seed. Old engine untouched;
full suite green.
Refs #467 #345 #343
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Purifies the error-localization layer from #470. The mutable
Frontier(threaded throughparse()as a pass-by-reference side effect) is gone. Insteadparse()returns a single channel —Sequence<ParseStep>— where each step is either aParse(a successful match) or aStuck(a dead end carrying the offset it stuck at and what was expected there).The reframe: failure is a value, not an absence
Previously a matcher signalled "no match" by returning an empty sequence — which discards the offset, so we needed a side-channel (the
Frontier) to recover "how far did we get."Now a matcher that can't proceed returns a
Stuckvalue. So the failure information travels back through the same return value as successes. The original bug fixes itself: onAF_INET, AF_INET6,Seq(…, EOF())can't finish,EOFyieldsStuck(7, {EOF}), and that rides up the return — no mutable state required.validate()folds theStuckvalues back into(furthest, expected)— the "frontier" is now computed from the return value rather than mutated into a shared object.Why a
Stucktype (not just a deepest-pointer)Per the discussion: a first-class
Stuck(offset, expected)leaves room to carry more diagnostic info later, and reads as a peer ofParsein the sealedParseStep.A doc note in
Parse.ktrecords the simpler alternative (thread a mutable accumulator) and its trade-off, for future reference — less code and a touch lazier, but it splits the data flow across two channels, which is exactly the asymmetry this removes.Mechanics
Parse.kt:sealed interface ParseStep = Parse | Stuck;validate()folds Stucks into the deepest offset + union of expected.Combinator.parse()drops thefrontierparameter;Frontier.ktdeleted.Seqcarries a stuck path forward;Alt/ZeroOrMore/OneOrMore/Repeatsurface failed attempts; terminals/EOFreturnStuckon no-match.Behaviour unchanged
validate()'s public result (ParseOutcome) is identical, soParseTestpasses as-is:AF_INET, AF_INET6→furthest=7, expected={whitespace, EOF}; the empty value still yields the family/none/~completion seed. OldSyntacticMatch/SemanticMatchengine untouched; full suite green.Refs #467 #345 #343
🤖 Generated with Claude Code