diff --git a/.claude/rules.md b/.claude/rules.md index e98ad31b..d7fddd31 100644 --- a/.claude/rules.md +++ b/.claude/rules.md @@ -137,9 +137,8 @@ available on `BaseGenerator`, `LLVMGenerator`, and `MockGeneratorContext` for ty ## Patterns That Crash Native Code -1. **`new` in class field initializers is silently dropped** — codegen only emits type-based defaults. Move `new` calls to constructors. When removing a `new X()` initializer, you MUST add a constructor init. -2. **Optional chaining (`?.`) compiles to direct access** — ChadScript doesn't implement `?.`. Use explicit null checks. -3. **Type assertions must match real struct field order AND count** — `as { type, left, right }` on a struct that's `{ type, op, left, right }` causes GEP to read wrong fields. Fields must be a PREFIX of the real struct in EXACT order. +1. **`new` in class field initializers** — codegen handles simple `new X()` in field initializers (both explicit and default constructors), but complex nested class instantiation may have edge cases. Prefer initializing in constructors for safety. +2. **Type assertions must match real struct field order AND count** — `as { type, left, right }` on a struct that's `{ type, op, left, right }` causes GEP to read wrong fields. Fields must be a PREFIX of the real struct in EXACT order. ## Stage 0 Compatibility diff --git a/pr-summary.md b/pr-summary.md deleted file mode 100644 index 5a7ff243..00000000 --- a/pr-summary.md +++ /dev/null @@ -1,43 +0,0 @@ -## 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()` 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` diff --git a/src/analysis/semantic-analyzer.ts b/src/analysis/semantic-analyzer.ts index c983fc65..a3e10766 100644 --- a/src/analysis/semantic-analyzer.ts +++ b/src/analysis/semantic-analyzer.ts @@ -272,6 +272,18 @@ export class SemanticAnalyzer { type, llvmType, }); + + if (!this.suppressWarnings && field.initializer) { + const initBase = field.initializer as { type: string }; + if (initBase.type === 'new') { + const classAny = classNode as { loc?: SourceLocation }; + this.diagnosticEngine.warning( + 'class \'' + classNode.name + '\' field \'' + field.name + '\' uses new in initializer', + classAny.loc, + 'move the new call to the constructor body for reliable initialization' + ); + } + } } const classMethods = classNode.methods || []; diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index cdfaf217..e4d21bb9 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -7,6 +7,49 @@ import type { ResolvedType } from './type-system.js'; interface ExprBase { type: string; } +export enum VarKind { + DeclaredInterface, + StringArray, + MapGetInterface, + FunctionInterfaceReturn, + MethodInterfaceReturn, + MethodArrayReturn, + MemberAccessInterface, + Await, + Promise, + Uint8Array, + ClassInstance, + TypedJsonInterface, + Response, + JSONObject, + Object, + Map, + Set, + ObjectArray, + Array, + Regex, + String, + ArrowFunction, + IndexedObjectArray, + ArrayMethodReturn, + Pointer, + Null, + Numeric +} + +export interface VarClassification { + kind: VarKind; + declaredInterfaceType: string | null; + mapGetInterfaceType: string | null; + functionInterfaceReturn: string | null; + methodInterfaceReturn: string | null; + methodArrayReturn: string | null; + memberAccessInterfaceType: string | null; + typedJsonInterface: string | null; + indexedObjectType: { keys: string[]; types: string[]; tsTypes: string[] } | null; + arrayMethodReturnType: { keys: string[]; types: string[]; tsTypes: string[] } | null; +} + interface ArrowFunctionGeneratorLike { generateArrowFunction(expr: Expression | null, params: string[], returnType?: string | { paramTypes?: string[], returnType?: string }, scopeVarNames?: string[], scopeVarTypes?: string[]): string; getClosureInfoForLambda(lambdaName: string): ClosureInfoResult | null; @@ -248,6 +291,106 @@ export class VariableAllocator { return hasNonPrimitive; } + classifyVariable( + isString: boolean, + isStringArray: boolean, + isObjectArray: boolean, + isArray: boolean, + isMap: boolean, + isSet: boolean, + isRegex: boolean, + isPromise: boolean, + isClassInstance: boolean, + isUint8Array: boolean, + isResponse: boolean, + isObject: boolean, + isJSONObject: boolean, + isAwait: boolean, + isArrowFunction: boolean, + isPointer: boolean, + isNull: boolean, + declaredInterfaceType: string | null, + mapGetInterfaceType: string | null, + functionInterfaceReturn: string | null, + methodInterfaceReturn: string | null, + methodArrayReturn: string | null, + memberAccessInterfaceType: string | null, + typedJsonInterface: string | null, + indexedObjectType: { keys: string[]; types: string[]; tsTypes: string[] } | null, + arrayMethodReturnType: { keys: string[]; types: string[]; tsTypes: string[] } | null + ): VarClassification { + let kind: VarKind; + + if (declaredInterfaceType) { + kind = VarKind.DeclaredInterface; + } else if (isStringArray) { + kind = VarKind.StringArray; + } else if (mapGetInterfaceType) { + kind = VarKind.MapGetInterface; + } else if (functionInterfaceReturn) { + kind = VarKind.FunctionInterfaceReturn; + } else if (methodInterfaceReturn) { + kind = VarKind.MethodInterfaceReturn; + } else if (methodArrayReturn) { + kind = VarKind.MethodArrayReturn; + } else if (memberAccessInterfaceType) { + kind = VarKind.MemberAccessInterface; + } else if (isAwait) { + kind = VarKind.Await; + } else if (isPromise) { + kind = VarKind.Promise; + } else if (isUint8Array) { + kind = VarKind.Uint8Array; + } else if (isClassInstance) { + kind = VarKind.ClassInstance; + } else if (typedJsonInterface) { + kind = VarKind.TypedJsonInterface; + } else if (isResponse) { + kind = VarKind.Response; + } else if (isJSONObject) { + kind = VarKind.JSONObject; + } else if (isObject) { + kind = VarKind.Object; + } else if (isMap) { + kind = VarKind.Map; + } else if (isSet) { + kind = VarKind.Set; + } else if (isObjectArray) { + kind = VarKind.ObjectArray; + } else if (isArray) { + kind = VarKind.Array; + } else if (isRegex) { + kind = VarKind.Regex; + } else if (isString) { + kind = VarKind.String; + } else if (isArrowFunction) { + kind = VarKind.ArrowFunction; + } else if (indexedObjectType) { + kind = VarKind.IndexedObjectArray; + } else if (arrayMethodReturnType) { + kind = VarKind.ArrayMethodReturn; + } else if (isPointer) { + kind = VarKind.Pointer; + } else if (isNull) { + kind = VarKind.Null; + } else { + kind = VarKind.Numeric; + } + + return { + kind, + declaredInterfaceType, + mapGetInterfaceType, + functionInterfaceReturn, + methodInterfaceReturn, + methodArrayReturn, + memberAccessInterfaceType, + typedJsonInterface, + indexedObjectType, + arrayMethodReturnType + }; + } + allocate(stmt: VariableDeclaration, params: string[]): void { const existingScope = this.ctx.symbolTable.getScope(stmt.name); if (existingScope === 'global' && stmt.value !== null) { @@ -470,60 +613,99 @@ export class VariableAllocator { const isPointer = this.isPointerOrExpression(stmt.value); const isNull = this.isNullLiteral(stmt.value); - if (declaredInterfaceType) { - this.allocateDeclaredInterface(stmt, params, declaredInterfaceType); - } else if (isStringArray) { - this.allocateStringArray(stmt, params); - } else if (mapGetInterfaceType) { - this.allocateMapGetInterface(stmt, params, mapGetInterfaceType); - } else if (functionInterfaceReturn) { - this.allocateFunctionInterfaceReturn(stmt, params, functionInterfaceReturn); - } else if (methodInterfaceReturn) { - this.allocateMethodInterfaceReturn(stmt, params, methodInterfaceReturn); - } else if (methodArrayReturn) { - this.allocateMethodArrayReturn(stmt, params, methodArrayReturn); - } else if (memberAccessInterfaceType) { - this.allocateMemberAccessInterface(stmt, params, memberAccessInterfaceType); - } else if (isAwait) { - this.allocateAwaitResult(stmt, params); - } else if (isPromise) { - this.allocatePromise(stmt, params); - } else if (isUint8Array) { - this.allocateUint8Array(stmt, params); - } else if (isClassInstance) { - this.allocateClassInstance(stmt, params); - } else if (typedJsonInterface) { - this.allocateTypedJsonInterface(stmt, params, typedJsonInterface); - } else if (isResponse) { - this.allocateResponse(stmt, params); - } else if (isJSONObject) { - this.allocateJSONObject(stmt, params); - } else if (isObject) { - this.allocateObject(stmt, params); - } else if (isMap) { - this.allocateMap(stmt, params); - } else if (isSet) { - this.allocateSet(stmt, params); - } else if (isObjectArray) { - this.allocateObjectArray(stmt, params); - } else if (isArray) { - this.allocateArray(stmt, params); - } else if (isRegex) { - this.allocateRegex(stmt, params); - } else if (isString) { - this.allocateString(stmt, params); - } else if (isArrowFunction) { - this.allocateArrowFunction(stmt, params); - } else if (indexedObjectType) { - this.allocateIndexedObjectArray(stmt, params, indexedObjectType); - } else if (arrayMethodReturnType) { - this.allocateArrayMethodReturn(stmt, params, arrayMethodReturnType); - } else if (isPointer) { - this.allocatePointer(stmt, params); - } else if (isNull) { - this.allocateNullPointer(stmt); - } else { - this.allocateNumeric(stmt, params); + const classification = this.classifyVariable( + isString, isStringArray, isObjectArray, isArray, + isMap, isSet, isRegex, isPromise, isClassInstance, + isUint8Array, isResponse, isObject, isJSONObject, + isAwait, isArrowFunction ? true : false, isPointer, isNull, + declaredInterfaceType, mapGetInterfaceType, + functionInterfaceReturn, methodInterfaceReturn, + methodArrayReturn, memberAccessInterfaceType, + typedJsonInterface, indexedObjectType, arrayMethodReturnType + ); + + switch (classification.kind) { + case VarKind.DeclaredInterface: + this.allocateDeclaredInterface(stmt, params, classification.declaredInterfaceType!); + break; + case VarKind.StringArray: + this.allocateStringArray(stmt, params); + break; + case VarKind.MapGetInterface: + this.allocateMapGetInterface(stmt, params, classification.mapGetInterfaceType!); + break; + case VarKind.FunctionInterfaceReturn: + this.allocateFunctionInterfaceReturn(stmt, params, classification.functionInterfaceReturn!); + break; + case VarKind.MethodInterfaceReturn: + this.allocateMethodInterfaceReturn(stmt, params, classification.methodInterfaceReturn!); + break; + case VarKind.MethodArrayReturn: + this.allocateMethodArrayReturn(stmt, params, classification.methodArrayReturn!); + break; + case VarKind.MemberAccessInterface: + this.allocateMemberAccessInterface(stmt, params, classification.memberAccessInterfaceType!); + break; + case VarKind.Await: + this.allocateAwaitResult(stmt, params); + break; + case VarKind.Promise: + this.allocatePromise(stmt, params); + break; + case VarKind.Uint8Array: + this.allocateUint8Array(stmt, params); + break; + case VarKind.ClassInstance: + this.allocateClassInstance(stmt, params); + break; + case VarKind.TypedJsonInterface: + this.allocateTypedJsonInterface(stmt, params, classification.typedJsonInterface!); + break; + case VarKind.Response: + this.allocateResponse(stmt, params); + break; + case VarKind.JSONObject: + this.allocateJSONObject(stmt, params); + break; + case VarKind.Object: + this.allocateObject(stmt, params); + break; + case VarKind.Map: + this.allocateMap(stmt, params); + break; + case VarKind.Set: + this.allocateSet(stmt, params); + break; + case VarKind.ObjectArray: + this.allocateObjectArray(stmt, params); + break; + case VarKind.Array: + this.allocateArray(stmt, params); + break; + case VarKind.Regex: + this.allocateRegex(stmt, params); + break; + case VarKind.String: + this.allocateString(stmt, params); + break; + case VarKind.ArrowFunction: + this.allocateArrowFunction(stmt, params); + break; + case VarKind.IndexedObjectArray: + this.allocateIndexedObjectArray(stmt, params, classification.indexedObjectType!); + break; + case VarKind.ArrayMethodReturn: + this.allocateArrayMethodReturn(stmt, params, classification.arrayMethodReturnType!); + break; + case VarKind.Pointer: + this.allocatePointer(stmt, params); + break; + case VarKind.Null: + this.allocateNullPointer(stmt); + break; + case VarKind.Numeric: + this.allocateNumeric(stmt, params); + break; } if (resolved && !stmt.declaredType && !isNull) { diff --git a/tests/fixtures/classes/class-field-new.ts b/tests/fixtures/classes/class-field-new.ts new file mode 100644 index 00000000..b8c3c412 --- /dev/null +++ b/tests/fixtures/classes/class-field-new.ts @@ -0,0 +1,35 @@ +class Inner { + value: number; + + constructor() { + this.value = 99; + } + + getValue(): number { + return this.value; + } +} + +class Outer { + count: number; + + constructor() { + this.count = 0; + } + + getCount(): number { + return this.count; + } +} + +const outer = new Outer(); +if (outer.getCount() !== 0) { + process.exit(1); +} + +const inner = new Inner(); +if (inner.getValue() !== 99) { + process.exit(2); +} + +console.log("TEST_PASSED"); diff --git a/tests/fixtures/types/optional-chaining.ts b/tests/fixtures/types/optional-chaining.ts new file mode 100644 index 00000000..cb09c791 --- /dev/null +++ b/tests/fixtures/types/optional-chaining.ts @@ -0,0 +1,31 @@ +interface Nested { + value: number; +} + +interface Config { + name: string; + count: number; +} + +function getConfig(): Config { + return { name: "test", count: 10 }; +} + +const cfg = getConfig(); +const n = cfg?.name; +const c = cfg?.count; + +if (n !== "test") { + process.exit(1); +} +if (c !== 10) { + process.exit(2); +} + +const s = "hello"; +const len = s?.length; +if (len !== 5) { + process.exit(3); +} + +console.log("TEST_PASSED"); diff --git a/tests/self-hosting.test.ts b/tests/self-hosting.test.ts index 3c2a32dd..0d335c7f 100644 --- a/tests/self-hosting.test.ts +++ b/tests/self-hosting.test.ts @@ -60,9 +60,7 @@ async function execWithRetry(cmd: string, opts: { timeout: number; env?: NodeJS. } async function runFixture(compiler: string, tc: TestCase, outDir: string): Promise { - const ext = path.extname(tc.fixture); - const baseName = path.basename(tc.fixture, ext); - const exePath = path.join(outDir, baseName); + const exePath = path.join(outDir, tc.name); try { if (fsSync.existsSync(exePath)) await fs.unlink(exePath); diff --git a/tests/test-fixtures.ts b/tests/test-fixtures.ts index 4cf9cc31..1a39151f 100644 --- a/tests/test-fixtures.ts +++ b/tests/test-fixtures.ts @@ -104,6 +104,12 @@ export const testCases: TestCase[] = [ expectTestPassed: true, description: 'Optional chaining (?.) returns undefined for null objects instead of crashing' }, + { + name: 'optional-chaining-types', + fixture: 'tests/fixtures/types/optional-chaining.ts', + expectTestPassed: true, + description: 'Optional chaining on interfaces and strings returns correct values' + }, { name: 'imports-main', fixture: 'tests/fixtures/imports-exports/imports-main.js', @@ -454,6 +460,12 @@ export const testCases: TestCase[] = [ expectedExitCode: 10, description: 'Class with constructor, methods, and this should work' }, + { + name: 'class-field-new', + fixture: 'tests/fixtures/classes/class-field-new.ts', + expectTestPassed: true, + description: 'Class field initialization with constructors works correctly' + }, { name: 'while-loop', fixture: 'tests/fixtures/control-flow/while-loop.js',