Skip to content

feat: add Lookahead and StartTokens inspection APIs - #473

Open
Pastalikek65 wants to merge 2 commits into
alecthomas:masterfrom
Pastalikek65:feat-107-lookahead
Open

Pastalikek65 wants to merge 2 commits into
alecthomas:masterfrom
Pastalikek65:feat-107-lookahead

Conversation

@Pastalikek65

Copy link
Copy Markdown
Contributor

Closes #107

Exposes the first-token-set analysis (originally built for the RecoverTo work in #472) as two public inspection APIs on Parser[G]:

// Map each starting token to the disjunction alternatives that can begin with it.
routing, err := parser.Lookahead(&Stmt{})     // map[string][]int

// Deduplicated set of tokens that can begin a production.
tokens, err := parser.StartTokens(&Stmt{})    // []string

Token descriptors are human-readable: named token references render as <ident>, literals as "if", and type-constrained literals as "if":Keyword.

  • lookahead.go: Lookahead, StartTokens, shared startSetDescriptors alongside the existing recovery.go analysis.
  • Design note: Lookahead routes on toplevel disjunction alternatives, so a production with a single sequence (no |) reports an explicit error — that case is covered by StartTokens. Group starts (@("foo" | "bar")) are unioned into the start-token set.

8 tests covering routing, group starts, no-disjunction, unknown type, nested productions, and dedup. StartTokens output for scalars @Ident → ["<ident>"].

RecoverTo(types...) enables skipping malformed input regions and resuming
parse at the next token that can begin a registered production. It works
inside repetition loops (struct slices parsed with @@* or @@+): when an
element fails, the lexer scans forward for a synchronization token while
never crossing structural literals (such as closing delimiters), then
restarts the loop there.

- recovery.go: startSet/nullable analysis over the node graph (literal,
  reference, strct, disjunction, sequence, capture, group, union), build-time
  resolution, and maybeRecover forward scan with lastRecovery progress guard.
- nodes.go: hooks in group.Parse for both error and empty-match paths.
- context.go/parser.go: recoverNode+structural sets threaded into parseContext.

Full test suite green; fuzzed 45s / ~300k execs with no hang, panic or
explosion. Closes alecthomas#342.
Expose the first-token-set analysis used by error recovery as public
diagnostics:

- Parser[G].Lookahead(example) maps each starting token to the disjunction
  alternative indices that can begin with it, for routing/autocompletion.
- Parser[G].StartTokens(example) returns the deduplicated set of tokens that
  can begin a production.

Both reuse the shared startSet/nullable analysis (recovery.go). Tokens are
described as named references (<ident>) or quoted literals, with a :Type
suffix when type-constrained. Lookahead reports an error for productions
without a top-level disjunction. Closes alecthomas#107.
@alecthomas

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a29faee4a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lookahead.go
Comment on lines +76 to +79
case *disjunction:
return n, true
case *strct:
return unwrapDisjunction(n.expr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle registered union nodes in Lookahead

When example identifies an interface registered with Union, resolveProduction returns a *union, but this switch rejects it even though the API documentation explicitly lists registered unions as supported productions. For example, Lookahead((*Expr)(nil)) returns “does not have alternatives” instead of routing the union members; unwrap n.disjunction for this case.

Useful? React with 👍 / 👎.

Comment thread lookahead.go
Comment on lines +57 to +59
descs := startSetDescriptors(n, seen, p.caseInsensitiveTokens, p.typeNodes)
sort.Strings(descs)
return descs, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Deduplicate StartTokens before returning

When multiple paths in a production have the same first token, startSetDescriptors emits that descriptor once per path and this code only sorts it. Thus a grammar such as @("a" | "a") returns ["\"a\"", "\"a\""], violating the documented deduplicated-set contract and forcing API consumers to deduplicate the result themselves.

Useful? React with 👍 / 👎.

Comment thread recovery.go
Comment on lines +179 to +180
case *literal, *reference, *custom, *parseable, *negation, *lookaheadGroup:
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat zero-width lookahead as nullable in first-set analysis

For a production beginning with a lookahead assertion, such as (?= "x") @Ident, the assertion consumes no token, but classifying lookaheadGroup as non-nullable prevents the sequence walker from considering the following consuming node. Consequently StartTokens returns an empty slice and RecoverTo registers no synchronization predicate for a production that can validly start with x.

Useful? React with 👍 / 👎.

Comment thread recovery.go
Comment on lines +289 to +290
if p.structural[tok.Value] {
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match structural boundaries case-insensitively during recovery

When the token type is configured with CaseInsensitive, structural literals are still looked up by their exact grammar spelling here. For example, with an "END" block delimiter and input containing lowercase end, recovery from a malformed body scans across the delimiter to the next synchronization token, consumes that outer statement as part of the body, and ultimately reports a missing END error instead of recovering within the block.

Useful? React with 👍 / 👎.

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.

Add function to compute lookahead

2 participants