parser(step 50): functions #10

Merged
crowmaki merged 2 commits from parser/functions into main 2026-08-23 22:05:31 +00:00
Collaborator

parser(step 50): functions

Implements Step 50 — Functions (docs/parser/50-functions.md). Both spellings of a function, plus the shared parameter/return-type grammar step 60's impl/interface members reuse. Phase: parse.

What's in

  • parse_decl / parse_decl_text — the declaration dispatcher and its text harness, parallel to the type/expr/stmt ones. Only the function arm exists; steps 60/70 fill the rest.
  • parse_function_decl — 'function' qualified-name '(' params ')' ('->' type)? block, so Vector3::dot scopes a declaration to a type.
  • parse_lambda + at_func_value — the func-value in expression position (what const f = (…) -> … {…} binds). -> is not a binary operator, so one bounded peek to the matching ) tells a lambda from a tuple or a group; nothing backtracks.
  • parse_params / parse_param — this-aware (this carries no type); an ordinary parameter's annotation is optional.
  • parse_var_decl — trailing ; now optional only after a brace-bodied func-value initializer, per language-grammar.md ▸ Functions.

Design call

A const bound to a lambda stays a var-decl statement whose initializer is an Expr::Lambda ((const f =(lambda …))) — not a Decl::FunctionDecl. This follows the step-50 doc's acceptance test (line 80-82), which overrides its own AST-section prose. All three pinned goldens pass verbatim.

Deliberately not done

The arrow-less func-value ((x: int) { … }): no fixture writes one, so left unread and said so in at_func_value's doc comment rather than adding an untested branch.

Verify

cargo fmt --check clean; cargo clippy --all --all-targets -- -D warnings clean; cargo test --all → 16 lexer + 63 parser + fixture drivers, 0 failed. Parse-fixture driver stays #[ignore]d (step-70 job). All seven function fixtures (function-c-style, function-lambda, function-void, function-overloading, global-functions, static-functions, instance-functions) build a tree end to end.

## parser(step 50): functions Implements **Step 50 — Functions** (`docs/parser/50-functions.md`). Both spellings of a function, plus the shared parameter/return-type grammar step 60's impl/interface members reuse. Phase: `parse`. ### What's in - `parse_decl` / `parse_decl_text` — the declaration dispatcher and its text harness, parallel to the type/expr/stmt ones. Only the `function` arm exists; steps 60/70 fill the rest. - `parse_function_decl` — `'function' qualified-name '(' params ')' ('->' type)? block`, so `Vector3::dot` scopes a declaration to a type. - `parse_lambda` + `at_func_value` — the `func-value` in *expression* position (what `const f = (…) -> … {…}` binds). `->` is not a binary operator, so one bounded peek to the matching `)` tells a lambda from a tuple or a group; nothing backtracks. - `parse_params` / `parse_param` — `this`-aware (`this` carries no type); an ordinary parameter's annotation is optional. - `parse_var_decl` — trailing `;` now optional *only* after a brace-bodied `func-value` initializer, per `language-grammar.md` ▸ Functions. ### Design call A `const` bound to a lambda stays a **var-decl statement** whose initializer is an `Expr::Lambda` (`(const f =(lambda …))`) — **not** a `Decl::FunctionDecl`. This follows the step-50 doc's acceptance test (line 80-82), which overrides its own AST-section prose. All three pinned goldens pass verbatim. ### Deliberately not done The arrow-less `func-value` (`(x: int) { … }`): no fixture writes one, so left unread and said so in `at_func_value`'s doc comment rather than adding an untested branch. ### Verify `cargo fmt --check` clean; `cargo clippy --all --all-targets -- -D warnings` clean; `cargo test --all` → 16 lexer + 63 parser + fixture drivers, 0 failed. Parse-fixture driver stays `#[ignore]`d (step-70 job). All seven function fixtures (`function-c-style`, `function-lambda`, `function-void`, `function-overloading`, `global-functions`, `static-functions`, `instance-functions`) build a tree end to end.
Both spellings the spec gives a function, and the shared parameter and
return-type grammar step 60's impl/interface members will reuse.

- `parse_decl` / `parse_decl_text`: the declaration dispatcher and its
  text harness, parallel to the type/expr/stmt ones. Only the `function`
  arm exists; steps 60 and 70 fill in the rest.
- `parse_function_decl`: `'function' qualified-name '(' params ')'
  ('->' type?)? block`, so `Vector3::dot` scopes a declaration to a type.
- `parse_lambda` + `at_func_value`: the `func-value` in *expression*
  position, which is what `const f = (…) -> … {…}` binds. `->` is not a
  binary operator, so one bounded peek to the matching `)` tells a lambda
  from a tuple or a group; nothing backtracks.
- `parse_params` / `parse_param`: `this` carries no type, an ordinary
  parameter's annotation is optional.
- `parse_var_decl` now admits the optional trailing `;` after a
  brace-bodied `func-value` (language-grammar.md ▸ Functions).

A `const` bound to a lambda stays a `var-decl` statement whose
initializer is an `Expr::Lambda` — not a declaration form of its own, per
language-grammar.md ▸ Program and declarations.

Phase: parse. All seven function fixtures (function-c-style,
function-lambda, function-void, function-overloading, global-functions,
static-functions, instance-functions) now build a tree; their shapes are
pinned as goldens here until step 70 enables the fixture driver.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
crowbot left a comment

Review of parser(step 50): functions. Parse-phase only; I took the two settled calls as given (const-lambda = a var-decl whose init is Expr::Lambda, and empty parse()/ignored fixture driver = tracked #25/#26).

VERIFIED: fmt + clippy (-D warnings) + 63 parser tests all green. The shapes that ARE implemented parse correctly. Lambda-vs-tuple-vs-group is disambiguated soundly for the arrow form: the "-> after the matching close paren" heuristic cannot false-positive, because Arrow is not a binary operator and the depth-scan correctly skips nested lambdas, function-type parens, and inner arrows. Whole-declaration spans pass the pin test. I specifically stress-tested the new optional-semicolon-after-brace-lambda logic and found it sound: it keys on init being a TOP-LEVEL body-carrying Expr::Lambda, so a grouped / call / ternary-armed lambda and a trailing call all still require ';', and the block-terminated ASI is exactly what language-grammar.md blesses. No bug there.

Three findings, one 3 and two 1s, all in the func-value grammar contract and diagnostic quality -- none in the semicolon logic.

TOP FINDING (severity 3): at_func_value() / parse_lambda() realize only the -> ... block form of func-value; the canonical grammar's other parse-legal shapes are silently rejected or mis-parsed. See the inline comment on at_func_value.

Verdict in one line: the implemented surface is correct and well-tested, but the func-value grammar contract is silently narrowed from the source of truth -- close that before a future fixture trips it. Severity 3.

Review of parser(step 50): functions. Parse-phase only; I took the two settled calls as given (const-lambda = a var-decl whose init is Expr::Lambda, and empty parse()/ignored fixture driver = tracked #25/#26). VERIFIED: fmt + clippy (-D warnings) + 63 parser tests all green. The shapes that ARE implemented parse correctly. Lambda-vs-tuple-vs-group is disambiguated soundly for the arrow form: the "-> after the matching close paren" heuristic cannot false-positive, because Arrow is not a binary operator and the depth-scan correctly skips nested lambdas, function-type parens, and inner arrows. Whole-declaration spans pass the pin test. I specifically stress-tested the new optional-semicolon-after-brace-lambda logic and found it sound: it keys on init being a TOP-LEVEL body-carrying Expr::Lambda, so a grouped / call / ternary-armed lambda and a trailing call all still require ';', and the block-terminated ASI is exactly what language-grammar.md blesses. No bug there. Three findings, one 3 and two 1s, all in the func-value grammar contract and diagnostic quality -- none in the semicolon logic. TOP FINDING (severity 3): at_func_value() / parse_lambda() realize only the `-> ... block` form of func-value; the canonical grammar's other parse-legal shapes are silently rejected or mis-parsed. See the inline comment on at_func_value. Verdict in one line: the implemented surface is correct and well-tested, but the func-value grammar contract is silently narrowed from the source of truth -- close that before a future fixture trips it. Severity 3.
@ -1554,0 +1657,4 @@
/// lets the callee's signature supply the type, which is a semantic question
/// rather than a syntactic one.
fn parse_param(c: &mut Cursor) -> Result<Param, ParseError> {
if let Some(tok) = c.eat_kw(KeywordKind::This) {
Author
Collaborator

SEVERITY 1. The grammar forbids a type on this (param = 'this' | ident [':' type]), but function f(this: int) {} is reported as "expected ')' to close the parameter list" at the :. The code already applies the right pattern for the function NAME (lines 1616-1620 check it here so parse_qualified_name's type-focused message doesn't leak to the reader of a function decl); the same check is missing for this. eat_kw(This) returns immediately and the illegal : falls through to the generic list-close error. A one-line if c.is(Colon) after the this arm, rejecting with "this takes no type", would name the actual rule instead of the nearest delimiter.

SEVERITY 1. The grammar forbids a type on `this` (`param = 'this' | ident [':' type]`), but `function f(this: int) {}` is reported as "expected ')' to close the parameter list" at the `:`. The code already applies the right pattern for the function NAME (lines 1616-1620 check it here so parse_qualified_name's type-focused message doesn't leak to the reader of a function decl); the same check is missing for `this`. eat_kw(This) returns immediately and the illegal `:` falls through to the generic list-close error. A one-line `if c.is(Colon)` after the `this` arm, rejecting with "`this` takes no type", would name the actual rule instead of the nearest delimiter.
@ -1554,0 +1707,4 @@
///
/// `->` is not a binary operator, so nothing in the expression grammar can
/// follow a completed parenthesized group with an `Arrow`. That makes one
/// bounded peek — scan to the matching `)`, look at the token after it —
Author
Collaborator

SEVERITY 1 (nit). The doc comment calls this "one bounded peek", but it is a full forward scan to the matching ')' -- every '(' in primary position rescans its entire group, so deeply stacked parenthesized input is O(n^2) overall. Correctness is fine (I verified it can't false-positive: Arrow is not a binary operator, and the depth counter correctly skips nested lambdas and function-type parens; the nth() clamp and the guaranteed depth>=1 at the opening '(' mean no underflow/panic). Only the "bounded" wording overstates what this does.

SEVERITY 1 (nit). The doc comment calls this "one bounded peek", but it is a full forward scan to the matching ')' -- every '(' in primary position rescans its entire group, so deeply stacked parenthesized input is O(n^2) overall. Correctness is fine (I verified it can't false-positive: Arrow is not a binary operator, and the depth counter correctly skips nested lambdas and function-type parens; the nth() clamp and the guaranteed depth>=1 at the opening '(' mean no underflow/panic). Only the "bounded" wording overstates what this does.
@ -1554,0 +1733,4 @@
}
k += 1;
}
c.nth(k + 1).kind == TokenKind::Arrow
Author
Collaborator

SEVERITY 3. at_func_value() decides a leading '(' is a func-value solely from whether the token after the matching ')' is an Arrow (this line). That realizes only the -> ... block subset of the canonical production in language-grammar.md / Functions:

func-value = "(" [ params ] ")" [ "->" [ type ] ] ( block | expr ); (line 378)

whose disambiguation note (lines 326-329) says a "(" is a func-value when a param-shape is inside OR () is followed by a "->" OR a block. Because the arrow and the return type are BOTH optional, three parse-legal shapes are silently rejected or mis-parsed here:

  1. const g = () { return 1; }; -- empty params, no arrow, block body
    at_func_value returns false; parse_tuple_or_group tries parse_expr on )
    -> "expected an expression".
  2. const g = (x: int) { ... }; (and (x) { ... }) -- arrowless, block body
    at_func_value false; parse_tuple_or_group reads x: int as a broken group
    -> "expected ')' after a parenthesized expression".
  3. const g = (x: int) -> x * 2; -- expression body
    Grammar line 388 explicitly documents -> expr ("-> { or -> expr infers the
    type"), but this branch is not read at all: the type parser reads x as a
    return TYPE, then parse_lambda's mandatory parse_block errors on *.

The code comment (lines 1714-1716) owns the arrowless case explicitly, but the expr body is never mentioned and neither shape is pinned by any test. No current fixture writes these, so nothing fails today -- but each is a production the source-of-truth grammar admits at parse phase, and each fails with a diagnostic pointing at the wrong rule. Per AGENTS.md the grammar is canonical; either narrow the grammar to block-only bodies (and say so there), or close these. The -> expr case is the sharper of the two because it is undocumented in code and explicitly blessed in the grammar.

SEVERITY 3. at_func_value() decides a leading '(' is a func-value solely from whether the token after the matching ')' is an Arrow (this line). That realizes only the `-> ... block` subset of the canonical production in language-grammar.md / Functions: func-value = "(" [ params ] ")" [ "->" [ type ] ] ( block | expr ); (line 378) whose disambiguation note (lines 326-329) says a "(" is a func-value when a param-shape is inside OR `()` is followed by a "->" OR a block. Because the arrow and the return type are BOTH optional, three parse-legal shapes are silently rejected or mis-parsed here: 1. `const g = () { return 1; };` -- empty params, no arrow, block body at_func_value returns false; parse_tuple_or_group tries parse_expr on `)` -> "expected an expression". 2. `const g = (x: int) { ... };` (and `(x) { ... }`) -- arrowless, block body at_func_value false; parse_tuple_or_group reads `x: int` as a broken group -> "expected ')' after a parenthesized expression". 3. `const g = (x: int) -> x * 2;` -- expression body Grammar line 388 explicitly documents `-> expr` ("-> { or -> expr infers the type"), but this branch is not read at all: the type parser reads `x` as a return TYPE, then parse_lambda's mandatory parse_block errors on `*`. The code comment (lines 1714-1716) owns the arrowless case explicitly, but the `expr` body is never mentioned and neither shape is pinned by any test. No current fixture writes these, so nothing fails today -- but each is a production the source-of-truth grammar admits at parse phase, and each fails with a diagnostic pointing at the wrong rule. Per AGENTS.md the grammar is canonical; either narrow the grammar to block-only bodies (and say so there), or close these. The `-> expr` case is the sharper of the two because it is undocumented in code and explicitly blessed in the grammar.
Review of #10 found `at_func_value` / `parse_lambda` realizing only the
`-> … block` subset of the canonical `func-value`, so three parse-legal
shapes were silently rejected with diagnostics naming the wrong rule.
AGENTS.md makes the grammar canonical, so the parser widens to meet it.

    func-value = "(" [ params ] ")" [ "->" [ type ] ] ( block | expr ) ;

- `ast.rs`: new `LambdaBody { Block, Expr }`, and `Expr::Lambda`'s body
  goes from `Option<Box<Stmt>>` to it. The `Option` modelled a bodiless
  func-value the grammar cannot derive and no code ever built.
- `at_func_value`: two signals, per language-grammar.md ▸ Paren
  disambiguation — a param shape in the first slot (`this`, or
  `ident :`), or `->`/`{` after the *matching* `)`. The `{` case is what
  admits an arrow-less body at any arity, `(x: int) { … }` as well as
  `() { … }`.
- `parse_lambda_tail`: `-> int { … }` and `-> x * 2` are told apart by
  reading a type speculatively — a type followed by `{` was an
  annotation, anything else means the arrow opened an expression body.
  It restores the whole `Cursor`, not just its position, because
  `eat_generic_close` rewrites the token under it when it splits a `>>`.
  `parse_return_type` stays as it was for the declaration form, which
  has no expression body and so never needs this.
- `parse_var_decl`: the optional trailing `;` now keys on
  `LambdaBody::Block`, so an expression-bodied initializer — having no
  closing brace to end the declaration — still needs its `;`.
- `parse_param`: a `:` after `this` is refused as "`this` takes no type"
  at the `:`, instead of falling through to `parse_params`' unclosed-list
  message. Same pattern as the function-name check above it.
- `at_func_value`'s doc comment said "one bounded peek"; it is a scan to
  the matching `)`, O(n²) over nested parens. language-grammar.md
  carried the same wording and was doubly wrong — `parse_for` already
  backtracks. Both corrected.

Docs, since the grammar is the source of truth and the spec had no
example of any of these shapes: language-spec.md ▸ Derived ▸ Functions
documents the arrow-less block body and the concise expression body;
language-grammar.md records the arrow disambiguation and restricts the
optional `;` to block bodies; 50-functions.md is rewritten against what
shipped (it still carried its own drifted grammar, a `lambda-const` that
was deleted, and `parse_lambda_decl`, which never existed).

Phase: parse. Two new fixtures, function-arrowless-body and
function-expression-body, registered in the manifest and pinned as
goldens here until step 70 enables the fixture driver. 67 parser tests,
up from 63; fmt and clippy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
crowmaki deleted branch parser/functions 2026-08-23 22:05:31 +00:00
crowmaki referenced this pull request from a commit 2026-08-23 22:05:31 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crowmaki/catlang!10
No description provided.