Skip to content

fix: native compiler segfault when closure mutation error is inside a function#513

Merged
cs01 merged 1 commit intomainfrom
fix-closure-mutation-segv
Apr 14, 2026
Merged

fix: native compiler segfault when closure mutation error is inside a function#513
cs01 merged 1 commit intomainfrom
fix-closure-mutation-segv

Conversation

@cs01
Copy link
Copy Markdown
Owner

@cs01 cs01 commented Apr 14, 2026

User impact

Before this PR, if you wrote a closure that mutates a captured variable inside a function body, the native self-hosted ChadScript compiler (.build/chad) would segfault silently instead of printing the "variable captured by closure but reassigned after capture" error. Node-hosted builds (node dist/chad-node.js) were unaffected.

After this PR, both paths correctly print the compile error.

Repro

function outer() {
  let x = 0;
  const g = () => x;
  x = 1;
  g();
}
outer();

.build/chad build m08.tsexit 139 (SIGSEGV) on the old code, exit 1 with a proper error on this PR.

The existing top-level fixture (closure-capture-mutation-error.ts) did not catch this because top-level AssignmentStatements take a different walker path in closure-mutation-checker.ts than function-body ones — only the function-body path triggered the read of the crashing field.

Root cause

closure-mutation-checker.ts:100 called this.reportError(assign.name, assign.loc). But no AssignmentStatement creation site in parser-ts or parser-native sets a loc field. Per the CLAUDE.md rule #3, the native compiler derives struct layouts from object-literal creation sites, so the resulting native AssignmentStatement struct has no slot for loc. Reading assign.loc GEPs past the end of the struct into garbage memory and segfaults inside the native formatCompileError pipeline — after correctly detecting the error but before it can be printed.

Fix

Defensive one-line change in closure-mutation-checker.ts: pass undefined instead of assign.loc to reportError. The error message itself is preserved; only the optional line-number underline is lost in the native-hosted path. The node-hosted path (which never exercised the crash) is unaffected.

Why not plumb loc through all AssignmentStatement creation sites?

I tried that first (it's the "proper" fix that would give the native path line-number info too). It triggered a deeper native-compiler bug: adding a loc: undefined field to some creation sites shifts the inferred struct layout in a way that breaks mrAllReturn in safety-checks.ts — the return-path walker starts misreading stmt.type via GEP and incorrectly flags transformExpression as "does not return a value on all code paths." Fixing that is a bigger native-compiler refactor (unify struct layouts from interface definitions instead of creation sites), which should be a separate PR.

Test plan

  • tests/fixtures/closures/closure-mutation-inside-function.ts — new, captures the exact repro that used to segfault
  • tests/fixtures/closures/closure-mutation-inside-arrow-body.ts — new, captures the variant where the assignment is inside the arrow body
  • Existing closure-capture-mutation-error.ts still passes (top-level case)
  • npm run verify:quick passes (full test suite + stage 1 self-hosting)

Followups not in this PR

  1. Plumb loc through AssignmentStatement creation sites to restore line-number info in the native-hosted error output — blocked on the mrAllReturn struct-layout issue above.
  2. The same latent bug exists for any stmt.loc / ifStmt.loc / etc. access on any statement type where no creation site sets loc. codegen/statements/control-flow.ts and codegen/statements/for-of.ts both read stmt.loc on error paths today — if any of those error paths get exercised on a native self-hosted compile, they will segfault the same way.

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results (Linux x86-64)

Benchmark C ChadScript Go Node Place
Binary Trees 1.367s 1.284s 2.805s 1.201s 🥈
Cold Start 0.9ms 0.9ms 1.2ms 27.1ms 🥇
Fibonacci 0.814s 0.772s 1.570s 3.161s 🥇
File I/O 0.118s 0.092s 0.088s 0.207s 🥈
JSON Parse/Stringify 0.004s 0.005s 0.018s 0.016s 🥈
Matrix Multiply 0.459s 0.587s 0.631s 0.362s 🥉
Monte Carlo Pi 0.389s 0.410s 0.404s 2.249s 🥉
N-Body Simulation 1.672s 2.121s 2.207s 2.381s 🥈
Quicksort 0.214s 0.246s 0.213s 0.263s 🥉
SQLite 0.353s 0.372s 0.418s 🥈
Sieve of Eratosthenes 0.019s 0.029s 0.018s 0.040s 🥉
String Manipulation 0.008s 0.018s 0.017s 0.037s 🥉

CLI Tool Benchmarks

Benchmark ChadScript grep node xxd Place
Hex Dump 0.433s 0.952s 0.135s 🥈
Recursive Grep 0.019s 0.010s 0.098s 🥈

@cs01 cs01 merged commit f02172c into main Apr 14, 2026
13 checks passed
@cs01 cs01 deleted the fix-closure-mutation-segv branch April 14, 2026 05:37
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