Skip to content

refactor: model parse() failure as a Stuck value (purify the frontier)#471

Open
SJrX wants to merge 1 commit into
issue-345-3from
issue-345-4
Open

refactor: model parse() failure as a Stuck value (purify the frontier)#471
SJrX wants to merge 1 commit into
issue-345-3from
issue-345-4

Conversation

@SJrX

@SJrX SJrX commented Jun 21, 2026

Copy link
Copy Markdown
Owner

What

Purifies the error-localization layer from #470. The mutable Frontier (threaded through parse() as a pass-by-reference side effect) is gone. Instead parse() returns a single channel — Sequence<ParseStep> — where each step is either a Parse (a successful match) or a Stuck (a dead end carrying the offset it stuck at and what was expected there).

Stacked on #470 (issue-345-3). Addresses review feedback that successes-via-return + failures-via-mutable-arg was an odd, asymmetric data flow.

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 Stuck value. So the failure information travels back through the same return value as successes. The original bug fixes itself: on AF_INET, AF_INET6, Seq(…, EOF()) can't finish, EOF yields Stuck(7, {EOF}), and that rides up the return — no mutable state required.

validate() folds the Stuck values back into (furthest, expected) — the "frontier" is now computed from the return value rather than mutated into a shared object.

Why a Stuck type (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 of Parse in the sealed ParseStep.

A doc note in Parse.kt records 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 the frontier parameter; Frontier.kt deleted.
  • Combinators propagate dead ends: Seq carries a stuck path forward; Alt/ZeroOrMore/OneOrMore/Repeat surface failed attempts; terminals/EOF return Stuck on no-match.

Behaviour unchanged

validate()'s public result (ParseOutcome) is identical, so ParseTest passes as-is: AF_INET, AF_INET6furthest=7, expected={whitespace, EOF}; the empty value still yields the family/none/~ completion seed. Old SyntacticMatch/SemanticMatch engine untouched; full suite green.

Refs #467 #345 #343

🤖 Generated with Claude Code

…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>
@github-actions

Copy link
Copy Markdown

Test Results

1 118 tests  ±0   1 118 ✅ ±0   49s ⏱️ ±0s
  295 suites ±0       0 💤 ±0 
  295 files   ±0       0 ❌ ±0 

Results for commit 44b6e41. ± Comparison against base commit 6a8c012.

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