Skip to content

parser: anchor incomplete-fn-header errors at the offending function (ILO-P020)#305

Merged
danieljohnmorris merged 5 commits into
mainfrom
fix/multi-fn-error-span
May 16, 2026
Merged

parser: anchor incomplete-fn-header errors at the offending function (ILO-P020)#305
danieljohnmorris merged 5 commits into
mainfrom
fix/multi-fn-error-span

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Fixes a token-spend trap reported by qa-tester + devops-sre rerun3: a malformed function header in a multi-function file used to report its error span on the wrong function. Personas wasted session time bisecting which function actually had the bug.

The root cause is in Parser::new: every Token::Newline is filtered out before parsing. When parse_fn_decl reads a malformed header, parse_type / parse_params happily walked across the newline into the next function and emitted the error there.

Manifesto framing: a correctly attributed span lands the persona on the right line first try instead of forcing a token-expensive bisect.

Repro (before → after)

f1 a:n>n;+a 1
f2 a:n>R       -- f2's err-type is missing
main>n;0
  • Before: ILO-P003: expected Greater, got Ident("main") at line 3 col 1 (the start of main).
  • After: ILO-P007: expected type, got end of line at line 2 col 8 (anchored on f2's line).
f1 a:n>n;+a 1
f2 a:n         -- f2 missing `>type;body` entirely
main>n;0
  • Before: ILO-P003 at line 3 col 1.
  • After: ILO-P020: incomplete function header for \f2`: header runs off the end of the lineat line 2 col 1, with a hint pointing at thename params>type;body` shape and the indent-the-continuation workaround.

EOF cases (file ends mid-header) used to fall through to Span::UNKNOWN and render as line 1 col 1. Now they anchor at the previous token (last token of the offending function's line) and surface ILO-P020 with the same friendly message.

What's in the diff (per commit)

  1. parser: anchor incomplete-header errors at the offending function — Parser records decl-boundary metadata in Parser::new (parallel Vec<Option<Span>> populated before newlines are filtered out). New helper check_fn_header_boundary runs at the two header pinch points in parse_fn_decl (after params, after >). parse_params stops at a boundary so the loop can't drag the next function's name in as another param. parse_type gets a safety net for nested type slots that anchors its existing ILO-P007 at prev_span().
  2. diag: document ILO-P020 incomplete function header — registry entry with the three failure shapes (missing return type, missing >, missing err-type after R) and the indent-the-continuation workaround. SKILL.md common-mistakes gets a matching entry as Fix unary negation -x parsing #12.
  3. test: cross-line attribution for parse errors in multi-fn files — initial 9 regression cases.
  4. parser: extend ILO-P020 to cover EOF mid-fn-headercheck_fn_header_boundary treats EOF as a soft boundary alongside decl boundaries. parse_type EOF case anchors at prev_span() instead of Span::UNKNOWN. Existing eof_while_expecting_identifier parser unit test had its assertion updated to accept the strictly-better ILO-P020 wording.
  5. test: cover EOF mid-fn-header attribution — 1 additional case + restoration of error_on_last_function_stays_on_last_function to assert proper attribution (not the EOF-fallback shape it was relaxed to mid-development).

ILO-P020 not P019 because P017/P018/P019 are already documented in SPEC.md/ai.txt for the import system. Pre-existing namespace mess; this PR doesn't touch it, just steps around.

Test plan

  • New regression test tests/regression_multi_fn_error_span.rs — 10 cases covering missing-err-type, missing->, missing-return-type-after-arrow, middle-of-three attribution, first-fn attribution, last-fn EOF attribution, last-fn missing-> EOF attribution, zero-param-fn variant, valid multi-fn parity, valid indented continuation parity. All passing.
  • Parser unit tests (cargo test --lib parser::) — 295/295 passing, including the updated eof_while_expecting_identifier.
  • Existing P001-cascade regression — 5/5 passing.
  • Full suite: 3064 passing locally. 7 AOT Cranelift tests fail with cannot find libilo.a — Searched: target/release/libilo.a because they hardcode target/ and ignore my CARGO_TARGET_DIR=target-isolated override. CI uses the default target dir and won't hit this — pre-existing infra constraint, not caused by this PR.
  • Manual repros verified on the rebased release binary across all four shapes (mid-file/EOF × missing-arrow/missing-err-type).

Follow-ups

  • EOF rendering as Span::UNKNOWN (line 1 col 1) is infra-wide — every error site uses peek_span() which returns UNKNOWN at EOF. This PR routes around it for the fn-header case specifically. A follow-up could replace peek_span() with a prev_span()-on-EOF fallback site by site.
  • P017/P018/P019 are double-booked between the parser (lambda capture, variadic, my originally-planned slot) and the import system (SPEC.md / ai.txt / main.rs). Worth untangling in a separate cleanup PR; not in scope here.

When a function header is malformed (missing return type, missing
`>`, etc.) and the line ends before the parser has what it needs, it
used to walk across the unindented newline and consume tokens from the
next function. The resulting error span landed on the wrong function,
sending personas to bisect the wrong line.

Record decl-boundary metadata in `Parser::new` (parallel to the
filtered token stream, one entry per token slot), then check it at the
two header pinch points in `parse_fn_decl` (after params, after the
`>`) and at the top of `parse_params` so the param loop cannot drag
the next function's name across the boundary. `parse_type` gets a
safety net for nested type slots (`R` / `M` / `F` / `L` / `O` /
`S`) that anchors its existing ILO-P007 at the previous token.

The friendly diagnostic is a new ILO-P020 with a hint pointing at the
`name params>type;body` shape and the indent-the-continuation
workaround. Picked P020 not P019 because P017/P018/P019 are already
documented in SPEC.md / ai.txt for the import system.
Registry entry covers the three header shapes that surface this:
missing return type after `>`, missing `>` entirely, and the
indent-the-continuation workaround. SKILL.md common-mistakes gets a
matching line so agents who see the code from the json diag have a
direct pointer to the fix.
Nine cases pin the regression and the symmetry: missing err-type,
missing `>`, missing return-type-after-arrow, middle-of-three
attribution, first-fn attribution, last-fn attribution with trailing
newline, zero-param-fn variant, valid multi-fn parity, and valid
indented continuation parity. The valid-program cases are the
safety rail — the boundary checks must not reject well-formed input.

Pure-EOF on the final function (`... main a:n>R`) is not covered:
it falls back to ILO-P008 with `Span::UNKNOWN` (a pre-existing
limitation unrelated to this fix). The trailing-newline case covers
the same shape with a proper anchor.
The header-level boundary check in `check_fn_header_boundary` now
treats EOF as a soft boundary alongside top-level decl boundaries, and
`parse_type`'s safety net anchors EOF errors at `prev_span()` rather
than the default `Span::UNKNOWN`. Same model as the mid-file
boundary case: errors land on the last token of the offending
function rather than line 1 col 1.

`prev_span()` is the right anchor for EOF because the parser has
already consumed the function name and at least the first malformed
header token, so `prev_span` always points at the offending line. The
existing parser unit test `eof_while_expecting_identifier` had been
pinning the old shape (bare `f` yielded an EOF message); updated its
assertion to accept the new ILO-P020 wording, which is strictly more
informative for personas.

EOF rendering as line 1 col 1 is infra-wide (every error site uses
`peek_span()` which returns `Span::UNKNOWN` at EOF) but route-around
in fn-header parsing is enough for the reported regression. A
follow-up could replace the `peek_span()` fallback site-by-site.
Restore the last-fn case to assert real attribution (`main a:n>R` at
EOF → line 3, not line 1), and add a sibling case for missing `>` at
EOF asserting ILO-P020 specifically. Same shape as the mid-file
boundary cases above but exercises the EOF branch of
`check_fn_header_boundary` and the EOF guard in `parse_type`.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

❌ Patch coverage is 98.33333% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/parser/mod.rs 98.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@danieljohnmorris danieljohnmorris merged commit 168a345 into main May 16, 2026
5 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/multi-fn-error-span branch May 16, 2026 14:51
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