Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ jobs:
exit 1
fi

- name: Run tests
run: npm test

- name: Build tree-sitter TypeScript objects
run: |
mkdir -p build
Expand All @@ -65,10 +62,21 @@ jobs:
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o
clang -c -O2 -fPIC -I vendor/tree-sitter/lib/include c_bridges/treesitter-bridge.c -o build/treesitter-bridge.o

- name: Build native chadc (static)
- name: Build native compiler for tests
run: node dist/chadc-node.js src/chadc-native.ts -o .build/chadc

- name: Smoke test native compiler
run: |
.build/chadc examples/hello.ts -o /tmp/hello
/tmp/hello

- name: Run tests
run: npm test

- name: Rebuild native chadc (static release)
run: node dist/chadc-node.js src/chadc-native.ts -o .build/chadc --static --target-cpu=x86-64

- name: Build native chad (static)
- name: Build native chad (static release)
run: node dist/chadc-node.js src/chad-native.ts -o .build/chad --static --target-cpu=x86-64

- name: Verify static binary
Expand Down Expand Up @@ -163,9 +171,6 @@ jobs:
exit 1
fi

- name: Run tests
run: npm test

- name: Build tree-sitter TypeScript objects
run: |
mkdir -p build
Expand All @@ -175,10 +180,21 @@ jobs:
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o
clang -c -O2 -fPIC -I vendor/tree-sitter/lib/include c_bridges/treesitter-bridge.c -o build/treesitter-bridge.o

- name: Build native chadc
- name: Build native compiler for tests
run: node dist/chadc-node.js src/chadc-native.ts -o .build/chadc

- name: Smoke test native compiler
run: |
.build/chadc examples/hello.ts -o /tmp/hello
/tmp/hello

- name: Run tests
run: npm test

- name: Rebuild native chadc (release)
run: node dist/chadc-node.js src/chadc-native.ts -o .build/chadc --target-cpu=x86-64

- name: Build native chad
- name: Build native chad (release)
run: node dist/chadc-node.js src/chad-native.ts -o .build/chad --target-cpu=x86-64

- name: Smoke test native chadc
Expand Down Expand Up @@ -255,9 +271,6 @@ jobs:
exit 1
fi

- name: Run tests
run: npm test

- name: Build tree-sitter TypeScript objects
run: |
mkdir -p build
Expand All @@ -267,10 +280,24 @@ jobs:
clang -c -O2 -fPIC -I $TS_SRC -I $TS_INCLUDE $TS_SRC/scanner.c -o build/tree-sitter-typescript-scanner.o
clang -c -O2 -fPIC -I vendor/tree-sitter/lib/include c_bridges/treesitter-bridge.c -o build/treesitter-bridge.o

- name: Build native chadc
- name: Build native compiler for tests
run: node dist/chadc-node.js src/chadc-native.ts -o .build/chadc

- name: Sign native compiler
run: codesign --sign - --force .build/chadc

- name: Smoke test native compiler
run: |
.build/chadc examples/hello.ts -o /tmp/hello
/tmp/hello

- name: Run tests
run: npm test

- name: Rebuild native chadc (release)
run: node dist/chadc-node.js src/chadc-native.ts -o .build/chadc

- name: Build native chad
- name: Build native chad (release)
run: node dist/chadc-node.js src/chad-native.ts -o .build/chad

- name: Sign macOS binaries
Expand Down
43 changes: 43 additions & 0 deletions pr-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Fix try/catch tests, JSON.parse on macOS, and require native compiler in test suite

### Summary

- Flatten `TryStatement` AST node back to direct fields (`catchParam`, `catchBody`) instead of nested `catchClause: { param, body }` — fixes a self-hosting bug where the native compiler couldn't handle chained inline struct field access (`tryStmt.catchClause.body`), causing catch block bodies to be silently dropped from generated IR
- Eliminate array-of-objects pattern in `json.ts` JSON.parse codegen — fixes a self-hosting crash on macOS ARM64 where the native compiler segfaulted when compiling `JSON.parse<T>()` calls
- Make `npm test` build `.build/chadc` automatically if missing, and fail loudly instead of silently falling back to the Node.js interpreter
- Run compiler tests with both native and Node.js compilers to catch self-hosting bugs early
- Restructure CI to build native compiler before tests on all platforms (consistent test setup)

### Problem

1. **Try/catch self-hosting bug**: Commit 5271d53 changed `TryStatement` from flat fields to a nested struct, relying on inline struct field access working in the native compiler. The two-level chained access `tryStmt.catchClause.body` silently produced no code in the native binary, so catch blocks were completely empty in the generated LLVM IR.

2. **JSON.parse crash on macOS**: `json.ts` used a `JsonInterfaceDef` intermediate type containing `{ name: string; type: string }[]` (array-of-objects). The native compiler can't reliably handle this pattern — it produces undefined behavior that crashes on macOS ARM64 (strict alignment/memory protection) while accidentally working on Linux x86_64. The fix requirement was surfaced by the test infrastructure change forcing CI to use the native compiler.

3. **Silent test fallback**: `compiler.test.ts` fell back to `node dist/chadc-node.js` if `.build/chadc` was missing. CI never had the native binary, so it always tested with the Node.js interpreter, masking both bugs above.

4. **Inconsistent CI setup**: Linux glibc CI ran tests before building the native compiler or tree-sitter objects, so it never tested with the native compiler even after fixing the fallback. Only macOS and musl had the native compiler available during tests.

### Changes

- `src/ast/types.ts` — `TryStatement` uses `catchParam: string | null` + `catchBody: BlockStatement | null` instead of `catchClause: { param, body } | null`
- `src/codegen/statements/control-flow.ts` — access flat fields in codegen
- `src/parser-native/transformer.ts` — produce flat fields from tree-sitter parse
- `src/parser-ts/handlers/statements.ts` — produce flat fields from TS API parse
- `src/analysis/semantic-analyzer.ts` — access flat fields
- `src/ast/visitor.ts` — access flat fields
- `src/codegen/infrastructure/closure-analyzer.ts` — access flat fields
- `src/codegen/stdlib/json.ts` — remove `JsonInterfaceDef` and `getInterfaceFields()`, use delegate methods (`interfaceStructGenGetFieldCount`, `interfaceStructGenGetFieldName`, `interfaceStructGenGetFieldTsType`) directly instead of building an intermediate array-of-objects
- `scripts/test.js` — build native compiler before running tests; re-run compiler tests with Node.js compiler as second pass
- `tests/compiler.test.ts` — configurable via `CHADC_COMPILER` env var, defaults to `.build/chadc`
- `.github/workflows/ci.yml` — all 3 platforms now build tree-sitter objects + native compiler + smoke test before running tests; macOS signs compiler before tests

### Test plan

- [x] `npm test` — 299 native + 161 node passing
- [x] `npm run verify:quick` — self-hosting passes
- [x] All 3 try/catch fixtures produce correct output with native compiler
- [x] All 3 JSON.parse fixtures compile and run correctly with native compiler
- [x] JSON.stringify still works (no regression)
- [x] Missing `.build/chadc` now throws a clear error instead of silent fallback
- [x] Both native and Node.js compiler passes run in `npm test`
27 changes: 26 additions & 1 deletion scripts/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { spawn, execSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
Expand All @@ -14,6 +15,17 @@ try {
process.exit(1);
}

const chadc = path.join(projectRoot, '.build', 'chadc');
if (!fs.existsSync(chadc)) {
console.log('Building native compiler (.build/chadc)...');
try {
execSync('node dist/chadc-node.js src/chadc-native.ts -o .build/chadc', { cwd: projectRoot, stdio: 'inherit' });
} catch (error) {
console.error('Native compiler build failed');
process.exit(1);
}
}

const args = process.argv.slice(2);

const testPattern = args.length === 0 ? [
Expand All @@ -33,5 +45,18 @@ const child = spawn('node', nodeArgs, {
});

child.on('exit', (code) => {
process.exit(code);
if (code !== 0 || args.length > 0) {
process.exit(code);
return;
}

console.log('\nRe-running compiler tests with Node.js compiler...');
const child2 = spawn('node', ['--import', 'tsx', '--test', 'tests/compiler.test.ts'], {
stdio: 'inherit',
shell: false,
env: { ...process.env, CHADC_COMPILER: 'node dist/chadc-node.js' }
});
child2.on('exit', (code2) => {
process.exit(code2);
});
});
10 changes: 6 additions & 4 deletions src/analysis/semantic-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,15 @@ export class SemanticAnalyzer {
} else if (stmtType === 'try') {
const tryStmt = stmt as TryStatement;
if (tryStmt.tryBlock) this.analyzeBlock(tryStmt.tryBlock);
if (tryStmt.catchClause) {
this.symbols.set(tryStmt.catchClause.param, {
name: tryStmt.catchClause.param,
if (tryStmt.catchParam) {
this.symbols.set(tryStmt.catchParam, {
name: tryStmt.catchParam,
type: 'string',
llvmType: 'i8*',
});
this.analyzeBlock(tryStmt.catchClause.body);
}
if (tryStmt.catchBody) {
this.analyzeBlock(tryStmt.catchBody);
}
if (tryStmt.finallyBlock) this.analyzeBlock(tryStmt.finallyBlock);
} else if (stmtType === 'return') {
Expand Down
3 changes: 2 additions & 1 deletion src/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ export interface ThrowStatement {
export interface TryStatement {
type: 'try';
tryBlock: BlockStatement;
catchClause: { param: string; body: BlockStatement } | null;
catchParam: string | null;
catchBody: BlockStatement | null;
finallyBlock: BlockStatement | null;
loc?: SourceLocation;
}
Expand Down
4 changes: 2 additions & 2 deletions src/ast/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ export class RecursiveASTVisitor {

visitTryStatement(node: TryStatement): void {
this.visitBlock(node.tryBlock);
if (node.catchClause) {
this.visitBlock(node.catchClause.body);
if (node.catchBody) {
this.visitBlock(node.catchBody);
}
if (node.finallyBlock) {
this.visitBlock(node.finallyBlock);
Expand Down
9 changes: 5 additions & 4 deletions src/codegen/infrastructure/closure-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ interface CatchHandler {
interface TryNode {
type: string;
tryBlock: BlockStatement;
catchClause: { param: string; body: BlockStatement } | null;
catchParam: string | null;
catchBody: BlockStatement | null;
finallyBlock: BlockStatement | null;
}

Expand Down Expand Up @@ -322,10 +323,10 @@ export class ClosureAnalyzer {
this.walkExpression(s.iterable);
this.walkBlock(s.body);
} else if (stmtType === 'try') {
const tryStmt = stmt as { type: string; tryBlock: BlockStatement; catchClause: { param: string; body: BlockStatement } | null; finallyBlock: BlockStatement | null };
const tryStmt = stmt as { type: string; tryBlock: BlockStatement; catchParam: string | null; catchBody: BlockStatement | null; finallyBlock: BlockStatement | null };
this.walkBlock(tryStmt.tryBlock);
if (tryStmt.catchClause !== null) {
this.walkBlock(tryStmt.catchClause.body);
if (tryStmt.catchBody !== null) {
this.walkBlock(tryStmt.catchBody);
}
if (tryStmt.finallyBlock !== null) {
this.walkBlock(tryStmt.finallyBlock);
Expand Down
8 changes: 4 additions & 4 deletions src/codegen/statements/control-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,7 +1329,7 @@ export class ControlFlowGenerator {
if (stmt.type !== 'try') {
throw new Error('Expected try statement');
}
const tryStmt = stmt as { type: string; tryBlock: BlockStatement; catchClause: { param: string; body: BlockStatement } | null; finallyBlock: BlockStatement | null };
const tryStmt = stmt as { type: string; tryBlock: BlockStatement; catchParam: string | null; catchBody: BlockStatement | null; finallyBlock: BlockStatement | null };

const frameRaw = this.nextTemp();
this.emit(`${frameRaw} = call i8* @GC_malloc(i64 216)`);
Expand Down Expand Up @@ -1369,8 +1369,8 @@ export class ControlFlowGenerator {
this.ctx.setCurrentLabel(catchEntryLabel);
this.emit(`store i8* ${prevFrame}, i8** @__exception_stack`);

if (tryStmt.catchClause) {
const paramName = tryStmt.catchClause.param;
if (tryStmt.catchBody) {
const paramName = tryStmt.catchParam;
if (paramName) {
const excMsg = this.nextTemp();
this.emit(`${excMsg} = load i8*, i8** @__exception_message`);
Expand All @@ -1379,7 +1379,7 @@ export class ControlFlowGenerator {
this.emit(`store i8* ${excMsg}, i8** ${paramAlloca}`);
this.ctx.defineVariable(paramName, paramAlloca, 'i8*', SymbolKind.String, 'local');
}
this.ctx.generateBlock(tryStmt.catchClause.body, params);
this.ctx.generateBlock(tryStmt.catchBody, params);
}

const catchHasTerminator = this.ctx.lastInstructionIsTerminator();
Expand Down
Loading
Loading