Skip to content

feat(diagnostics): runtime-error carets + token-precise LSP ranges (#407 residual)#541

Merged
InauguralPhysicist merged 2 commits into
mainfrom
feat/407-runtime-carets-lsp-ranges
Jul 10, 2026
Merged

feat(diagnostics): runtime-error carets + token-precise LSP ranges (#407 residual)#541
InauguralPhysicist merged 2 commits into
mainfrom
feat/407-runtime-carets-lsp-ranges

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #407

The two residual halves of #407, in one change:

1. Runtime-error carets

Uncaught runtime errors now print the same one-line source excerpt + ^ caret as parse errors, with the column attributed to the failing token — the [ of an out-of-range subscript, the operator of a type mismatch, the undefined identifier, the of of a bad call:

$ eigenscript bad.eigs
Error line 2: index 9 out of range (list length 2)
     2 | print of x[9]
       |           ^
  at <module> (line 2)

Mechanism (zero dispatch-loop cost, no OP_LINE policy or tape change):

  • A per-byte cols[] table beside the chunk's lines[], stamped by a save/set/restore position cursor in compile_node (post-order child emission can't clobber the parent's column).
  • Parser anchors for fallible nodes: subscripts at [, binops/relations/unaries at the operator token (captured before p_advance — they were previously stamped with whatever token followed the right operand), dots at the key token.
  • A refcounted per-unit source blob (EigsSrcBuf) on chunks, shared by nested fn/lambda chunks, so REPL lines, eval'd strings, and imported modules each excerpt their own source even after the caller frees its buffer.
  • Uncaught-error printing moves from raise time (rt_error/builtin_throw) to the dispatch loop's CHECK_ERROR, where the live ip gives the failing instruction's byte offset. A lines[off] == g_error_line guard suppresses the caret whenever the position can't be attributed confidently (e.g. JIT-advanced ip) — a caret never points at the wrong line. The Error line N: message and at fn (line N) trace lines are byte-unchanged; the excerpt block is purely additive.

Verified precise across: index/type/undefined/call errors, function bodies (trace order preserved), imported modules (module's own source), eval strings, REPL lines, task-death reports, throw, and errors raised inside a 90k-iteration OSR-hot loop.

2. Token-precise LSP ranges

  • E002 (parse): the recorded first-error position now carries the token length (first_error_len); the squiggle spans exactly the offending token.
  • E003 (undefined name): LintDiag/LintWarning gain col/len, populated by a new lint_error_at at the E003 sites; the squiggle lands on the identifier.
  • The old 0..1000 whole-line span remains only as the fallback for diagnostics without a tracked position (unmigrated W-rules).

Pins & docs

  • examples/errors/* runtime cases declare their caret column via a new # expect-caret: directive (checker extended); a column-attribution regression now fails the corpus.
  • EM26/EM27 pin the runtime excerpt + caret in the main suite; tests/test_lsp.py pins the E002/E003 ranges (71 checks).
  • docs/DIAGNOSTICS.md (byte-exact contract) + docs/SPEC.md updated in the same change.

Validation

  • Release suite: 2731/2731.
  • ASan+UBSan detect_leaks=1: green through every executable section — [87] closure-cycle strict gate leak-clean, all 14 error examples, zero LeakSanitizer reports. (The box's background runner kept reaping the run at section [97]; the remaining [99]/[99b] are mechanical doc greps, green in the release run.)
  • LSP suite: 71/71.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm

InauguralPhysicist and others added 2 commits July 10, 2026 14:14
 residual)

Closes #407

Uncaught runtime errors now print the same one-line source excerpt +
^ caret as parse errors, column-attributed to the failing token:

    $ eigenscript bad.eigs
    Error line 2: index 9 out of range (list length 2)
         2 | print of x[9]
           |           ^
      at <module> (line 2)

Mechanism (zero dispatch-loop cost, no OP_LINE/tape change):
- Per-byte cols[] table beside the chunk's lines[], stamped by a
  save/set/restore position cursor in compile_node — post-order child
  emission can't clobber the parent's column.
- Fallible-node columns anchored in the parser: subscripts at '[',
  binops/relations/unaries at the operator token (captured before
  p_advance), dots at the key token.
- Refcounted per-unit source blob (EigsSrcBuf) on chunks, shared by
  nested fn/lambda chunks — REPL lines, eval'd strings, and imported
  modules each excerpt their own source after the caller frees its
  buffer.
- Uncaught-error printing deferred from rt_error/builtin_throw raise
  time to the dispatch loop's CHECK_ERROR, where the live ip gives the
  failing instruction's offset. lines[off] == g_error_line guards the
  caret: if the position can't be attributed confidently (JIT-advanced
  ip), the caret is suppressed rather than misplaced. Message and
  stack-trace lines stay byte-unchanged; carets are additive.

LSP diagnostics are now token-precise: E002 (parse) squiggles the
recorded token span (new first_error_len), E003 (undefined name)
squiggles the identifier via new col/len fields on LintDiag/
lint_error_at. The 0..1000 whole-line span remains only as the
no-position fallback for unmigrated W-rules.

Pinned: examples/errors/* runtime cases declare their caret column via
a new '# expect-caret:' directive in test_error_examples.sh; EM26/EM27
pin the runtime excerpt + caret; tests/test_lsp.py pins E002/E003
ranges (71 checks). docs/DIAGNOSTICS.md + docs/SPEC.md updated in the
same change.

Validation: release suite 2731/2731; ASan+UBSan with detect_leaks=1
green through every executable section ([87] closure-cycle strict gate
leak-clean, zero LeakSanitizer reports); LSP suite 71/71.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm
The #407 compile_ast signature change (src param for caret excerpts)
missed fuzz/fuzz_stdin.c and fuzz/fuzz_eigenscript.c — the CI fuzz
smoke's make fuzz failed to compile. Pass each harness's input buffer
so fuzzed programs exercise the caret path too. Also size
LintDiag.severity to LintWarning.level (16) — the fuzz build's -Wall
flagged the 16-into-12 snprintf copy in lint_collect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdDAVfZHgoQ2UNP63FYUXm
@InauguralPhysicist InauguralPhysicist merged commit 82ab2ae into main Jul 10, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the feat/407-runtime-carets-lsp-ranges branch July 10, 2026 19:57
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.

Runtime-error carets + token-precise LSP ranges (residual of #407)

1 participant