Skip to content

Parse closures and the closure pipe stage - #92

Merged
StreamDemon merged 2 commits into
mainfrom
feature/parser-closures
Aug 16, 2026
Merged

Parse closures and the closure pipe stage#92
StreamDemon merged 2 commits into
mainfrom
feature/parser-closures

Conversation

@StreamDemon

@StreamDemon StreamDemon commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Closes #61.

Summary

Closures failed with "expected expression"; the pipe-stage form x |> (|v| ...) carried an explicit not-yet-implemented error. This PR adds:

  • AST: ExprKind::Closure { is_move, params, body } with ClosureParam { pattern, ty } — the pattern grammar from Parse match expressions and the shared pattern grammar #90 is reused, so |u|, |n: i64|, |_|, |(a, b)|, |ref r|, trailing commas, move closures, and both expression and block bodies all parse. No return-type annotation — §16 has none (noted on Parser: closures (|x| expr, move) #61).
  • Parser: closure_expr()/closure_rest(); prefix() arms for |, ||, and reserved move; can_begin_expr gains Pipe | PipePipe | Move; pipe_stage()'s "(" closure ")" form replaces the old not-yet error (a non-closure paren stage like x |> (v) remains a grammar error); the old pipe_rejects_closure_stage_for_now test is retargeted.
  • Two grammar ambiguities resolved, both filed on Spec drift: §5.2/§3.7 examples do not derive from the §16 grammar (return-as-expression, ref field-pat shorthand) #89 (items 4a/4b, Rust precedent): || is the zero-param closure in expression-prefix position and Logical OR in infix position — infix || never reaches prefix(), so the rule is forced; and closure params parse the non-or pattern, because a top-level or-pattern param (|a | b| body) would swallow the closing | as its separator — |(A | B)| parenthesizes instead. Plus 4c: §3.10's stale "defined in §4.5" cross-ref for closure traits (they're in §4.6).
  • Consequence pinned by test: send || x; now opens a send-statement (closure operand → "must be a method call" error) per the §2.7 lookahead rule — a closure can begin an expression.
  • Corpus: tests/corpus/closures.sp — §4.6 capture examples (the spawn move || one adapted without spawn, noted in the fixture), §5.6 closure stage, §6.5 map chain, §7.3 both iterator styles, §8.2 filter chain.
  • Tests: 8 new unit tests (75 in the crate) — zero-arg || and spaced | |, every param form, move, infix-|| regression pin, closure stage structural assert, let/arg positions, send-head consequence, grammar violations.
  • Docs: crates/AGENTS.md updated (closures to Accepted, closure-pipe-stage error note replaced, spawn move || noted as still unparseable).

Flagged, not fixed (per repo rule): §4.6's spawn move || { ... } example still won't parse — spawn/select/emit have no productions and no milestone issue; worth deciding whether they join milestone 1 (they are §16 expr/statement alternatives) or get their own issue.

Spec Sections Affected

None — implements existing §4.6/§16; docs/reference/grammar.md remains byte-identical to §16. Grammar gaps filed on #89 (4a/4b/4c), scope note on #61.

Build Targets Tested

N/A — parser crate only, no codegen.

Test Plan

  • cargo fmt --all -- --check — pass
  • cargo clippy --workspace --all-targets -- -D warnings — pass
  • cargo test --workspace — 87 tests pass (11 lexer, 75 parser incl. 8 new, 1 corpus over 17 fixtures)
  • Corpus fixture covers §4.6/§5.6/§6.5/§7.3/§8.2 closure shapes
  • CI green (rust.yml triggers: crates/** and tests/corpus/** changed)

Summary by cubic

Parses closure expressions and the parenthesized closure pipe stage, and fixes closure/stage span anchoring. Previously closures errored with “expected expression,” x |> (|v| ...) returned a not‑yet‑implemented error, and spans anchored to the wrong tokens; all now parse and report spans per §4.6/§16.

  • AST: adds ExprKind::Closure { is_move, params, body } and ClosureParam { pattern, ty }. Supports ||, |x|, |x: T|, |_|, tuple/ref patterns, trailing commas, move, and expression or block bodies. No return‑type annotation.
  • Parser: adds closure_expr()/closure_rest(); can_begin_expr now includes Pipe, PipePipe, and Move. pipe_stage() accepts "(" closure ")" and rejects non‑closure parens in a stage (x |> (v) is a grammar error).
  • Disambiguation: || is a zero‑param closure in prefix position; infix || remains Logical OR. Closure params use the non‑or pattern; top‑level or‑patterns must parenthesize (|(A | B)|).
  • Behavior: send || x; now parses as a send‑statement (then fails the method‑call check) because a closure can start an expression. Spans anchor at the closure’s own opener (including move), and a parenthesized closure stage’s span covers its parens.
  • Tests/Docs: adds tests/corpus/closures.sp and unit tests covering shapes, disambiguation, span anchors, and regressions. Updates crates/AGENTS.md to mark closures and the closure pipe stage as implemented and to document the paren‑stage restriction.

Written for commit a098cfa. Summary will update on new commits.

Review in cubic

Implements #61. Closures failed with "expected expression"; the pipe
stage form x |> (|v| ...) carried an explicit not-yet-implemented error.

Adds ExprKind::Closure with ClosureParam { pattern, ty } reusing the
shared pattern grammar, so |u|, |n: i64|, |_|, |(a, b)|, and |ref r|
params all parse, as do move closures and both expression and block
bodies.

Two grammar ambiguities needed resolutions, both filed on #89 (items
4a/4b) with Rust-precedent readings: || is the empty-param closure in
expression-prefix position and Logical OR in infix position (infix ||
never reaches prefix, so the rule is forced); and closure params parse
the non-or pattern because a top-level or-pattern param would swallow
the closing | as its separator - |(A | B)| parenthesizes instead.

The parenthesized pipe stage now parses a closure callee (a non-closure
paren stage stays a grammar error), and send followed by a closure
opener opens a send-statement per the §2.7 lookahead rule, whose operand
then fails the method-call check. Closures take no return-type
annotation - §16 has none, noted on #61.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files

Architecture diagram
sequenceDiagram
    participant L as Lexer
    participant P as Parser
    participant A as AST
    participant C as Corpus Tests
    participant T as Unit Tests

    Note over L,P: Token stream for closures and pipe stages
    L->>P: Pipe (|), PipePipe (||), Move keyword tokens
    P->>P: can_begin_expr() checks (Pipe, PipePipe, Move)
    
    Note over P: Prefix expression parsing
    P->>P: prefix() sees Pipe/PipePipe/Move
    
    alt Pipe or PipePipe in prefix position
        P->>P: closure_expr() - consume move/pipe
        P->>P: closure_rest() parse params
        P->>P: pattern_atom() for non-or patterns
        opt Colon encountered
            P->>P: ty() parse type annotation
        end
        opt Comma encountered
            P->>P: Parse additional params
        end
        P->>P: expect Pipe (closing delimiter)
        alt LBrace body
            P->>P: block() parse block body
        else Expression body
            P->>P: expr(0) parse expression
        end
        P->>A: Create ExprKind::Closure { is_move, params, body }
    
    else Move keyword not followed by pipe
        P->>P: error "expected closure after `move`"
    end

    Note over P: Pipe stage parsing (integrated with pipe_stage())
    P->>P: pipe_stage() sees LParen
    P->>P: bump LParen
    alt Pipe, PipePipe, or Move follows
        P->>P: closure_expr() parse closure
        P->>P: expect RParen
        P->>A: Closure as pipe stage callee
    else Non-closure content
        P->>P: error "parenthesized pipe stage must contain a closure"
    end

    Note over P: Infix || remains Logical OR
    P->>P: Binding-power loop consumes PipePipe before prefix()

    Note over C,T: Validation paths
    C->>P: Corpus fixture closures.sp
    P-->>C: Parse all closure shapes (§4.6/§5.6/§6.5/§7.3/§8.2)
    T->>P: Unit tests for param forms, zero-arg, move
    P-->>T: AST assertions pass
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/sploosh-parser/src/lib.rs Outdated
Comment thread crates/sploosh-parser/src/lib.rs Outdated
Review feedback on #92: closure_expr snapshotted prev_span() before
consuming anything, so non-move closures anchored to the token before
the opener (the `=` of a let, say), and the parenthesized closure stage
span excluded its opening paren. The start now snapshots the current
token before move is consumed, and the stage span covers `(` through
`)`. A span test pins all three anchor points.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Implements spec-defined closure parsing and the parenthesized closure pipe stage, with documented grammar-ambiguity resolutions, pinned unit tests, and a corpus fixture; grammar.md stays byte-identical to §16, no runtime API, operational, or data changes, and no irreversible exposure.

Re-trigger cubic

@StreamDemon
StreamDemon merged commit 4aedec0 into main Aug 16, 2026
3 checks passed
@StreamDemon
StreamDemon deleted the feature/parser-closures branch August 16, 2026 10:53
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.

Parser: closures (|x| expr, move)

1 participant