Skip to content

fix(native): emitError chained method call caused segv on every compile error#521

Merged
cs01 merged 1 commit intomainfrom
debug-fuzz
Apr 17, 2026
Merged

fix(native): emitError chained method call caused segv on every compile error#521
cs01 merged 1 commit intomainfrom
debug-fuzz

Conversation

@cs01
Copy link
Copy Markdown
Owner

@cs01 cs01 commented Apr 17, 2026

User impact

The native self-hosted compiler (`.build/chad`) segfaulted instead of printing any compile error whose upstream code path ran through `emitError`. Concretely: fuzz A2-A5 repros (`new Date()` at module scope, `instanceof` with class hierarchy, `typeof d.getTime()`, etc.) all crashed with no output. Users saw a silent SIGSEGV where they should have seen a diagnostic.

After this PR, every one of those inputs prints the clean error the Node compiler already prints:

```
error: cannot determine type of module-scope variable 'd' (expression type: new). Move the declaration inside a function, or add a type annotation
```

Root cause

`emitError` in `src/codegen/llvm-generator.ts` built the output via a chained expression:

```typescript
this.diagnostics.formatDiagnostic(
this.diagnostics.getErrors()[this.diagnostics.getErrors().length - 1],
)
```

This is `method_call()[method_call().length - 1]` — an index access into the result of a method call. The native compiler's codegen for this pattern does not propagate the result's `elementInterfaceName` through, so the indexed element is handed to `formatDiagnostic` as a bare `i8*` pointing to memory the reader treats as a `%Diagnostic` struct with a different layout than what was stored. Subsequent `diag.message` GEP loads garbage; `strlen` on the garbage pointer crashes.

Confirmed under lldb: backtrace lands in `strlen` called from `formatDiagnostic + 260` (the `bold(": " + diag.message)` expression), with x1 holding ANSI-escape byte values instead of a string pointer. Debug prints before and after the intermediate-variable refactor prove the fix: with intermediates, `severity` reads `0` and `message` reads the correct string; without, severity reads `9.48606e-322` (garbage) and the message read segfaults.

Change

  • `src/codegen/llvm-generator.ts` — `emitError` extracts `getErrors()` result and the last diagnostic into intermediate variables before passing to `formatDiagnostic`. Functionally identical; mechanically avoids the chained-index codegen bug.
  • `tests/fixtures/edge-cases/new-date-module-scope.ts`, `tests/fixtures/edge-cases/new-date-zero-module-scope.ts` — regression fixtures using `@test-compile-error` annotation. They used to segv; now they emit the expected diagnostic.

Follow-up

The underlying codegen bug (method-call-result indexed access loses element interface type) is still present and worth fixing at the root in `access/member.ts` / `handleIndexAccessPropertyAccess`. This PR is the targeted workaround to immediately unblock A2-A5. Filed as future work.

Test plan

  • `npm run verify` green (tests + stage 1 + stage 2)
  • /tmp/chad_fuzz/c29_date, c36_date_basic, c37_date_zero, c38_date_typeof, c62_instanceof — all now emit clean compile errors (no segfault)
  • Two regression fixtures added to `tests/fixtures/edge-cases/`

…use emitError chained getErrors() twice per expression — extract to intermediate vars and add module-scope Date fixtures
@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results (Linux x86-64)

Benchmark C ChadScript Go Node Place
Binary Trees 1.374s 1.258s 2.725s 1.207s 🥈
Cold Start 1.6ms 0.8ms 1.2ms 27.3ms 🥇
Fibonacci 0.814s 0.763s 1.559s 3.179s 🥇
File I/O 0.119s 0.093s 0.087s 0.207s 🥈
JSON Parse/Stringify 0.004s 0.005s 0.019s 0.015s 🥈
Matrix Multiply 0.454s 0.598s 0.573s 0.360s #4
Monte Carlo Pi 0.389s 0.410s 0.405s 2.250s 🥉
N-Body Simulation 1.671s 2.125s 2.199s 2.381s 🥈
Quicksort 0.215s 0.247s 0.212s 0.262s 🥉
SQLite 0.354s 0.376s 0.433s 🥈
Sieve of Eratosthenes 0.016s 0.029s 0.020s 0.039s 🥉
String Manipulation 0.008s 0.018s 0.017s 0.037s 🥉

CLI Tool Benchmarks

Benchmark ChadScript grep node xxd Place
Hex Dump 0.435s 0.910s 0.132s 🥈
Recursive Grep 0.019s 0.010s 0.098s 🥈

@cs01 cs01 merged commit eb3fd24 into main Apr 17, 2026
13 checks passed
@cs01 cs01 deleted the debug-fuzz branch April 17, 2026 22:48
cs01 added a commit that referenced this pull request Apr 18, 2026
…elegates to canonical, revert #521 emitError workaround

canonical path (rich.arrayDepth>0 + rich.fields) now fires for all indexed-object-array element-field accesses; legacy branches remain as fallback for shapes canonical doesn't yet enrich.

with canonical in place, #521's emitError intermediate-variable workaround is no longer necessary — reverted to the original chained method_call+index_access form; native compiler emits the expected diagnostic (no segv).

zero fuzz regression; stage 2 self-hosting + fixture suite green.
cs01 added a commit that referenced this pull request Apr 18, 2026
…elegates to canonical, revert #521 emitError workaround (#527)

canonical path (rich.arrayDepth>0 + rich.fields) now fires for all indexed-object-array element-field accesses; legacy branches remain as fallback for shapes canonical doesn't yet enrich.

with canonical in place, #521's emitError intermediate-variable workaround is no longer necessary — reverted to the original chained method_call+index_access form; native compiler emits the expected diagnostic (no segv).

zero fuzz regression; stage 2 self-hosting + fixture suite green.

Co-authored-by: cs01 <cs01@users.noreply.github.com>
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