From 39151818fd154949091609652d25f5f527fd5121 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 2 Mar 2026 22:11:18 -0800 Subject: [PATCH 01/10] =?UTF-8?q?add=20generic=20class=20support=20via=20t?= =?UTF-8?q?ype=20erasure=20(T=20=E2=86=92=20i8*,=20T[]=20=E2=86=92=20Objec?= =?UTF-8?q?tArray)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ast/types.ts | 1 + src/codegen/expressions/method-calls.ts | 12 +++- .../types/collections/array/mutators.ts | 59 ++++++++++++++++++- src/parser-native/transformer.ts | 30 +++++++++- src/parser-ts/handlers/declarations.ts | 6 ++ tests/fixtures/generics/stack.ts | 21 +++++++ 6 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/generics/stack.ts diff --git a/src/ast/types.ts b/src/ast/types.ts index 98abb3bb..6e050cfe 100644 --- a/src/ast/types.ts +++ b/src/ast/types.ts @@ -441,6 +441,7 @@ export interface ClassNode { fields: ClassField[]; // Explicit field declarations methods: ClassMethod[]; loc?: SourceLocation; + typeParameters?: string[]; } export interface ImportSpecifier { diff --git a/src/codegen/expressions/method-calls.ts b/src/codegen/expressions/method-calls.ts index 0e0f8ef3..08fabc25 100644 --- a/src/codegen/expressions/method-calls.ts +++ b/src/codegen/expressions/method-calls.ts @@ -313,6 +313,13 @@ export class MethodCallGenerator { return result; } + private isClassInstanceExpression(expr: Expression): boolean { + const e = expr as ExprBase; + if (e.type !== "variable") return false; + const varName = (expr as VariableNode).name; + return this.ctx.symbolTable.isClass(varName); + } + private isVariableWithName(expr: Expression, name: string): boolean { if (!expr) { return false; @@ -1242,9 +1249,10 @@ export class MethodCallGenerator { } // Handle array methods (arrayGen uses context pattern - no sync needed! 🎯) - if (method === "push") { + // Skip to class dispatch if object is a class instance (e.g. Stack.push / Stack.pop) + if (method === "push" && !this.isClassInstanceExpression(expr.object)) { return this.ctx.arrayGen.generateArrayPush(expr, params); - } else if (method === "pop") { + } else if (method === "pop" && !this.isClassInstanceExpression(expr.object)) { return this.ctx.arrayGen.generateArrayPop(expr, params); } else if (method === "includes" && this.ctx.isArrayExpression(expr.object)) { return this.ctx.arrayGen.generateArrayIncludes(expr, params); diff --git a/src/codegen/types/collections/array/mutators.ts b/src/codegen/types/collections/array/mutators.ts index 23164316..ab8ada21 100644 --- a/src/codegen/types/collections/array/mutators.ts +++ b/src/codegen/types/collections/array/mutators.ts @@ -76,6 +76,7 @@ export function generateArrayPop( // Determine array type let isStringArray = false; + let isObjectArray = false; let isPointerArray = false; const exprObjBase2 = expr.object as ExprBase; if (exprObjBase2.type === "variable") { @@ -83,16 +84,20 @@ export function generateArrayPop( const varName = varNode.name; const varType = gen.getVariableType(varName); isStringArray = varType === "%StringArray*" || varType === "%StringArray"; + isObjectArray = varType === "%ObjectArray*" || varType === "%ObjectArray"; isPointerArray = varType === "i8*"; } - if (!isStringArray && !isPointerArray) { + if (!isStringArray && !isObjectArray && !isPointerArray) { const ptrType = gen.getVariableType(arrayPtr); if (ptrType === "%StringArray*" || ptrType === "%StringArray") isStringArray = true; + else if (ptrType === "%ObjectArray*" || ptrType === "%ObjectArray") isObjectArray = true; else if (ptrType === "i8*") isPointerArray = true; } if (isStringArray) { return generateStringArrayPop(gen, arrayPtr); + } else if (isObjectArray) { + return generateObjectArrayPop(gen, arrayPtr); } else if (isPointerArray) { return generatePointerArrayPop(gen, arrayPtr); } else { @@ -217,6 +222,58 @@ function generateStringArrayPop(gen: IGeneratorContext, arrayPtr: string): strin return result; } +function generateObjectArrayPop(gen: IGeneratorContext, arrayPtr: string): string { + const lenPtr = gen.nextTemp(); + gen.emit( + `${lenPtr} = getelementptr inbounds %ObjectArray, %ObjectArray* ${arrayPtr}, i32 0, i32 1`, + ); + const currentLen = gen.nextTemp(); + gen.emit(`${currentLen} = load i32, i32* ${lenPtr}`); + + const isEmpty = gen.emitIcmp("eq", "i32", currentLen, "0"); + + const emptyLabel = gen.nextLabel("pop_empty"); + const notEmptyLabel = gen.nextLabel("pop_notempty"); + const endLabel = gen.nextLabel("pop_end"); + + gen.emitBrCond(isEmpty, emptyLabel, notEmptyLabel); + + gen.emitLabel(emptyLabel); + const nullPtr = gen.nextTemp(); + gen.emit(`${nullPtr} = inttoptr i64 0 to i8*`); + gen.emitBr(endLabel); + + gen.emitLabel(notEmptyLabel); + + const lastIndex = gen.nextTemp(); + gen.emit(`${lastIndex} = sub i32 ${currentLen}, 1`); + + const dataPtrField = gen.nextTemp(); + gen.emit( + `${dataPtrField} = getelementptr inbounds %ObjectArray, %ObjectArray* ${arrayPtr}, i32 0, i32 0`, + ); + const dataPtrRaw = gen.emitLoad("i8*", dataPtrField); + const dataPtr = gen.emitBitcast(dataPtrRaw, "i8*", "i8**"); + + const elemPtr = gen.nextTemp(); + gen.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 ${lastIndex}`); + const lastElem = gen.nextTemp(); + gen.emit(`${lastElem} = load i8*, i8** ${elemPtr}`); + + gen.emitStore("i32", lastIndex, lenPtr); + + gen.emitBr(endLabel); + + gen.emitLabel(endLabel); + const result = gen.nextTemp(); + gen.emit( + `${result} = phi i8* [ ${nullPtr}, %${emptyLabel} ], [ ${lastElem}, %${notEmptyLabel} ]`, + ); + gen.setVariableType(result, "i8*"); + + return result; +} + function generatePointerArrayPop(gen: IGeneratorContext, arrayPtr: string): string { const castPtr = gen.emitBitcast(arrayPtr, "i8*", "%Array*"); diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 36a30a8c..5f6211e9 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -2666,7 +2666,35 @@ function transformClassDeclaration(node: TreeSitterNode): ClassNode | null { } } - return { name, extends: extendsClause, implements: implementsClause, fields, methods }; + let typeParameters: string[] | undefined; + const typeParamsNode = getChildByFieldName(node, "type_parameters"); + if (typeParamsNode) { + const tpn = typeParamsNode as NodeBase; + const tps: string[] = []; + for (let i = 0; i < tpn.namedChildCount; i++) { + const tp = getNamedChild(typeParamsNode, i); + if (!tp) continue; + const tpBase = tp as NodeBase; + if (tpBase.type === "type_parameter") { + const tpName = getChildByFieldName(tp, "name"); + if (tpName) { + tps.push((tpName as NodeBase).text); + } + } + } + if (tps.length > 0) { + typeParameters = tps; + } + } + + return { + name, + extends: extendsClause, + implements: implementsClause, + fields, + methods, + typeParameters, + }; } function transformClassField(node: TreeSitterNode): ClassField | null { diff --git a/src/parser-ts/handlers/declarations.ts b/src/parser-ts/handlers/declarations.ts index eb33316c..20a02ba4 100644 --- a/src/parser-ts/handlers/declarations.ts +++ b/src/parser-ts/handlers/declarations.ts @@ -140,6 +140,11 @@ export function transformClassDeclaration( } } + let typeParameters: string[] | undefined; + if (node.typeParameters && node.typeParameters.length > 0) { + typeParameters = node.typeParameters.map((tp) => tp.name.text); + } + return { name, extends: extendsClause, @@ -147,6 +152,7 @@ export function transformClassDeclaration( fields, methods, loc: getLoc(node), + typeParameters, }; } diff --git a/tests/fixtures/generics/stack.ts b/tests/fixtures/generics/stack.ts new file mode 100644 index 00000000..d59048bd --- /dev/null +++ b/tests/fixtures/generics/stack.ts @@ -0,0 +1,21 @@ +class Stack { + items: T[]; + constructor() { + this.items = []; + } + push(x: T): void { + this.items.push(x); + } + pop(): T { + return this.items.pop(); + } + size(): number { + return this.items.length; + } +} +const s = new Stack(); +s.push("hello"); +s.push("world"); +console.log(s.pop()); +console.log(s.size().toString()); +console.log("TEST_PASSED"); From c89dfd9c3544badfa0e6bfa4533fefc1ebb04db2 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 2 Mar 2026 22:18:04 -0800 Subject: [PATCH 02/10] add more generic tests and fix objectarray shift --- .../types/collections/array/reorder.ts | 66 ++++++++++++++++++- tests/fixtures/generics/box.ts | 17 +++++ tests/fixtures/generics/generic-extends.ts | 27 ++++++++ tests/fixtures/generics/pair.ts | 20 ++++++ tests/fixtures/generics/queue.ts | 25 +++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/generics/box.ts create mode 100644 tests/fixtures/generics/generic-extends.ts create mode 100644 tests/fixtures/generics/pair.ts create mode 100644 tests/fixtures/generics/queue.ts diff --git a/src/codegen/types/collections/array/reorder.ts b/src/codegen/types/collections/array/reorder.ts index 17ea3eb7..002fb96b 100644 --- a/src/codegen/types/collections/array/reorder.ts +++ b/src/codegen/types/collections/array/reorder.ts @@ -167,20 +167,26 @@ export function generateArrayShift( const arrayPtr = gen.generateExpression(expr.object, params); let isStringArray = false; + let isObjectArray = false; const exprObjBase = expr.object as ExprBase; if (exprObjBase.type === "variable") { const varName = (expr.object as VariableNode).name; const varType = gen.getVariableType(varName); isStringArray = varType === "%StringArray*" || varType === "%StringArray"; + isObjectArray = varType === "%ObjectArray*" || varType === "%ObjectArray"; } - if (!isStringArray) { + if (!isStringArray && !isObjectArray) { const ptrType = gen.getVariableType(arrayPtr); if (ptrType === "%StringArray*" || ptrType === "%StringArray") isStringArray = true; + else if (ptrType === "%ObjectArray*" || ptrType === "%ObjectArray") isObjectArray = true; } if (isStringArray) { return generateStringArrayShift(gen, arrayPtr); } + if (isObjectArray) { + return generateObjectArrayShift(gen, arrayPtr); + } return generateNumericArrayShift(gen, arrayPtr); } @@ -292,6 +298,64 @@ function generateStringArrayShift(gen: IGeneratorContext, arrayPtr: string): str return result; } +function generateObjectArrayShift(gen: IGeneratorContext, arrayPtr: string): string { + const lenPtr = gen.nextTemp(); + gen.emit( + `${lenPtr} = getelementptr inbounds %ObjectArray, %ObjectArray* ${arrayPtr}, i32 0, i32 1`, + ); + const currentLen = gen.nextTemp(); + gen.emit(`${currentLen} = load i32, i32* ${lenPtr}`); + + const isEmpty = gen.emitIcmp("eq", "i32", currentLen, "0"); + + const emptyLabel = gen.nextLabel("shift_empty"); + const notEmptyLabel = gen.nextLabel("shift_notempty"); + const endLabel = gen.nextLabel("shift_end"); + + gen.emitBrCond(isEmpty, emptyLabel, notEmptyLabel); + + gen.emitLabel(emptyLabel); + const nullPtr = gen.nextTemp(); + gen.emit(`${nullPtr} = inttoptr i64 0 to i8*`); + gen.emitBr(endLabel); + + gen.emitLabel(notEmptyLabel); + const dataPtrField = gen.nextTemp(); + gen.emit( + `${dataPtrField} = getelementptr inbounds %ObjectArray, %ObjectArray* ${arrayPtr}, i32 0, i32 0`, + ); + const dataPtrRaw = gen.emitLoad("i8*", dataPtrField); + const dataPtr = gen.emitBitcast(dataPtrRaw, "i8*", "i8**"); + + const firstElem = gen.emitLoad("i8*", dataPtr); + + const newLen = gen.nextTemp(); + gen.emit(`${newLen} = sub i32 ${currentLen}, 1`); + + const destI8 = gen.emitBitcast(dataPtr, "i8**", "i8*"); + const srcPtr = gen.nextTemp(); + gen.emit(`${srcPtr} = getelementptr inbounds i8*, i8** ${dataPtr}, i32 1`); + const srcI8 = gen.emitBitcast(srcPtr, "i8*", "i8*"); + const moveLen = gen.nextTemp(); + gen.emit(`${moveLen} = zext i32 ${newLen} to i64`); + const moveBytes = gen.nextTemp(); + gen.emit(`${moveBytes} = mul i64 ${moveLen}, 8`); + gen.emit( + `call void @llvm.memmove.p0i8.p0i8.i64(i8* ${destI8}, i8* ${srcI8}, i64 ${moveBytes}, i1 false)`, + ); + + gen.emitStore("i32", newLen, lenPtr); + gen.emitBr(endLabel); + + gen.emitLabel(endLabel); + const result = gen.nextTemp(); + gen.emit( + `${result} = phi i8* [ ${nullPtr}, %${emptyLabel} ], [ ${firstElem}, %${notEmptyLabel} ]`, + ); + gen.setVariableType(result, "i8*"); + return result; +} + export function generateArrayUnshift( gen: IGeneratorContext, expr: MethodCallNode, diff --git a/tests/fixtures/generics/box.ts b/tests/fixtures/generics/box.ts new file mode 100644 index 00000000..6460efe9 --- /dev/null +++ b/tests/fixtures/generics/box.ts @@ -0,0 +1,17 @@ +class Box { + value: T; + constructor(v: T) { + this.value = v; + } + get(): T { + return this.value; + } + set(v: T): void { + this.value = v; + } +} +const b = new Box("hello"); +console.log(b.get()); +b.set("world"); +console.log(b.get()); +console.log("TEST_PASSED"); diff --git a/tests/fixtures/generics/generic-extends.ts b/tests/fixtures/generics/generic-extends.ts new file mode 100644 index 00000000..6ffddf7c --- /dev/null +++ b/tests/fixtures/generics/generic-extends.ts @@ -0,0 +1,27 @@ +interface Printable { + toString(): string; +} + +class Wrapper { + items: T[]; + constructor() { + this.items = []; + } + add(x: T): void { + this.items.push(x); + } + count(): number { + return this.items.length; + } + first(): T { + return this.items[0]; + } +} + +const w = new Wrapper(); +w.add("alpha"); +w.add("beta"); +w.add("gamma"); +console.log(w.count().toString()); +console.log(w.first()); +console.log("TEST_PASSED"); diff --git a/tests/fixtures/generics/pair.ts b/tests/fixtures/generics/pair.ts new file mode 100644 index 00000000..c4abfff7 --- /dev/null +++ b/tests/fixtures/generics/pair.ts @@ -0,0 +1,20 @@ +class Pair { + first: A; + second: B; + constructor(a: A, b: B) { + this.first = a; + this.second = b; + } + getFirst(): A { + return this.first; + } + getSecond(): B { + return this.second; + } +} +const p = new Pair("foo", "bar"); +console.log(p.getFirst()); +console.log(p.getSecond()); +const p2 = new Pair("x", "y"); +console.log(p2.getFirst()); +console.log("TEST_PASSED"); diff --git a/tests/fixtures/generics/queue.ts b/tests/fixtures/generics/queue.ts new file mode 100644 index 00000000..1ec88ab8 --- /dev/null +++ b/tests/fixtures/generics/queue.ts @@ -0,0 +1,25 @@ +class Queue { + items: T[]; + constructor() { + this.items = []; + } + enqueue(x: T): void { + this.items.push(x); + } + dequeue(): T { + return this.items.shift(); + } + isEmpty(): boolean { + return this.items.length === 0; + } + size(): number { + return this.items.length; + } +} +const q = new Queue(); +q.enqueue("a"); +q.enqueue("b"); +q.enqueue("c"); +console.log(q.dequeue()); +console.log(q.size().toString()); +console.log("TEST_PASSED"); From fda01f08713da7e5b6989cd2839f7c8d2c5332c6 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 2 Mar 2026 22:50:31 -0800 Subject: [PATCH 03/10] add generic function support: type param return type inference for globals and locals --- src/ast/types.ts | 1 + .../infrastructure/variable-allocator.ts | 44 +++++++++++++++++++ src/codegen/llvm-generator.ts | 21 ++++++++- src/parser-native/transformer.ts | 13 +++++- src/parser-ts/handlers/expressions.ts | 5 +++ tests/fixtures/generics/generic-functions.ts | 12 +++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/generics/generic-functions.ts diff --git a/src/ast/types.ts b/src/ast/types.ts index 6e050cfe..6103b49f 100644 --- a/src/ast/types.ts +++ b/src/ast/types.ts @@ -124,6 +124,7 @@ export interface CallNode { name: string; args: Expression[]; loc?: SourceLocation; + typeArgs?: string[]; } export interface MethodCallNode { diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index b4c73668..e695b973 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -19,6 +19,7 @@ import { TypeAliasDeclaration, MethodCallNode, CallNode, + FunctionNode, CommonField, BinaryNode, MapNode, @@ -263,6 +264,42 @@ export class VariableAllocator { return this.interfaceAlloc.getInterface(name); } + private resolveGenericCallReturnType(expr: Expression): string | null { + const e = expr as { type: string }; + if (e.type !== "call") return null; + const callNode = expr as CallNode; + if (!callNode.typeArgs || callNode.typeArgs.length === 0) return null; + const ast = this.ctx.getAst(); + if (!ast || !ast.functions) return null; + let func: FunctionNode | null = null; + for (let i = 0; i < ast.functions.length; i++) { + const f = ast.functions[i] as FunctionNode; + if (f.name === callNode.name) { + func = f; + break; + } + } + if (!func || !func.typeParameters || func.typeParameters.length === 0) return null; + if (!func.returnType) return null; + let ret = func.returnType; + if (callNode.typeArgs && callNode.typeArgs.length > 0) { + for (let i = 0; i < func.typeParameters.length; i++) { + const param = func.typeParameters[i] || ""; + const arg = callNode.typeArgs[i] || "any"; + ret = ret.split(param).join(arg); + } + } else { + for (let i = 0; i < func.typeParameters.length; i++) { + const param = func.typeParameters[i] || ""; + if (ret === param) { + ret = "string"; + break; + } + } + } + return ret; + } + private getAllInterfaceFields(iface: InterfaceDeclaration): InterfaceField[] { return this.interfaceAlloc.getAllInterfaceFields(iface); } @@ -751,6 +788,13 @@ export class VariableAllocator { const isPointer = this.isPointerOrExpression(stmt.value); const isNull = this.isNullLiteral(stmt.value); + if (!isString && !isStringArray && !isObjectArray && !isArray && !isClassInstance) { + const genericReturn = this.resolveGenericCallReturnType(stmtValue); + if (genericReturn === "string") isString = true; + else if (genericReturn === "string[]") isStringArray = true; + else if (genericReturn && genericReturn.endsWith("[]")) isObjectArray = true; + } + const classification = this.classifyVariable( isString, isStringArray, diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index 52919bc5..f2ef4ecd 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -1800,14 +1800,31 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { const name = stmt.name; if ((stmt.value as { type: string }).type === "call") { - const callNode = stmt.value as { type: string; name: string }; + const callNode = stmt.value as CallNode; if (callNode.name) { let handled = false; for (let fi = 0; fi < this.ast.functions.length; fi++) { const fn = this.ast.functions[fi]; if (!fn) continue; if (fn.name === callNode.name && fn.returnType) { - const rt = fn.returnType; + let rt = fn.returnType; + if (fn.typeParameters && fn.typeParameters.length > 0) { + if (callNode.typeArgs && callNode.typeArgs.length > 0) { + for (let ti = 0; ti < fn.typeParameters.length; ti++) { + const tp = fn.typeParameters[ti] || ""; + const ta = callNode.typeArgs[ti] || "any"; + rt = rt.split(tp).join(ta); + } + } else { + for (let ti = 0; ti < fn.typeParameters.length; ti++) { + const tp = fn.typeParameters[ti] || ""; + if (rt === tp) { + rt = "string"; + break; + } + } + } + } if (rt === "string" || rt === "i8_ptr" || rt === "ptr") { ir += `@${name} = global i8* null` + "\n"; this.globalVariables.set(name, { diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 5f6211e9..4aa0df82 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -973,7 +973,18 @@ function transformCallExpression(node: TreeSitterNode): Expression { pos: 0, }; } else if (fn.type === "identifier") { - return { type: "call", name: fn.text, args }; + let callTypeArgs: string[] | undefined; + if (typeArgsNode) { + const ncc = typeArgsNode.namedChildCount; + if (ncc > 0) { + callTypeArgs = []; + for (let i = 0; i < ncc; i++) { + const ta = getNamedChild(typeArgsNode, i); + if (ta) callTypeArgs.push((ta as NodeBase).text); + } + } + } + return { type: "call", name: fn.text, args, typeArgs: callTypeArgs }; } else if (fn.type === "super") { return { type: "call", name: "super", args }; } else { diff --git a/src/parser-ts/handlers/expressions.ts b/src/parser-ts/handlers/expressions.ts index 71343b17..79dea432 100644 --- a/src/parser-ts/handlers/expressions.ts +++ b/src/parser-ts/handlers/expressions.ts @@ -397,11 +397,16 @@ function transformCallExpression( optional: isOptional || undefined, } as MethodCallNode; } else if (ts.isIdentifier(node.expression)) { + const callTypeArgs = + node.typeArguments && node.typeArguments.length > 0 + ? node.typeArguments.map((ta) => ta.getText()) + : undefined; return { type: "call", name: node.expression.text, args, loc: getLoc(node), + typeArgs: callTypeArgs, }; } else if ( ts.isCallExpression(node.expression) || diff --git a/tests/fixtures/generics/generic-functions.ts b/tests/fixtures/generics/generic-functions.ts new file mode 100644 index 00000000..d01c7a00 --- /dev/null +++ b/tests/fixtures/generics/generic-functions.ts @@ -0,0 +1,12 @@ +function identity(x: T): T { + return x; +} +function first(arr: T[]): T { + return arr[0]; +} +const s = identity("hello"); +console.log(s); +const items: string[] = ["a", "b", "c"]; +const f = first(items); +console.log(f); +console.log("TEST_PASSED"); From aa47f29fdacf9022829510500714d17076fdca45 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 2 Mar 2026 22:59:20 -0800 Subject: [PATCH 04/10] emit compile error when generic method return used without type annotation --- .../infrastructure/variable-allocator.ts | 43 +++++++++++++++++++ src/codegen/llvm-generator.ts | 35 +++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index e695b973..7a4fffe7 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -264,6 +264,45 @@ export class VariableAllocator { return this.interfaceAlloc.getInterface(name); } + private getGenericMethodReturnError(expr: Expression, varName: string): string | null { + const e = expr as { type: string }; + if (e.type !== "method_call") return null; + const methodExpr = expr as MethodCallNode; + const objBase = methodExpr.object as { type: string }; + if (objBase.type !== "variable") return null; + const objName = (methodExpr.object as VariableNode).name; + const className = this.ctx.symbolTable.getConcreteClass(objName); + if (!className) return null; + const ast = this.ctx.getAst(); + if (!ast || !ast.classes) return null; + for (let i = 0; i < ast.classes.length; i++) { + const cls = ast.classes[i] as { + name: string; + typeParameters?: string[]; + methods: { name: string; isConstructor: boolean; returnType?: string }[]; + }; + if (cls.name !== className) continue; + if (!cls.typeParameters || cls.typeParameters.length === 0) return null; + for (let j = 0; j < cls.methods.length; j++) { + const m = cls.methods[j]; + if (m.isConstructor || m.name !== methodExpr.method) continue; + if (!m.returnType) return null; + for (let k = 0; k < cls.typeParameters.length; k++) { + if ( + m.returnType === cls.typeParameters[k] || + m.returnType.includes(cls.typeParameters[k] as string) + ) { + return ( + `'${varName}' is assigned from '${objName}.${methodExpr.method}()' which returns generic type '${m.returnType}' — ` + + `add a type annotation: 'const ${varName}: YourType = ${objName}.${methodExpr.method}()'` + ); + } + } + } + } + return null; + } + private resolveGenericCallReturnType(expr: Expression): string | null { const e = expr as { type: string }; if (e.type !== "call") return null; @@ -908,6 +947,10 @@ export class VariableAllocator { // VarKind.Numeric is correct for number/boolean literals and arithmetic, // but suspicious for calls/method calls that might return non-numeric types. if (nodeType === "call" || nodeType === "method_call") { + const genericErr = this.getGenericMethodReturnError(stmtValue, stmt.name); + if (genericErr) { + return this.ctx.emitError(genericErr); + } this.ctx.emitWarning( `variable '${stmt.name}' classified as numeric from expression type '${nodeType}' — ` + `if this is wrong, add a type annotation`, diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index f2ef4ecd..3a161aa9 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -2286,6 +2286,13 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { // Phase 3: Final catch-all based on expression node type if (llvmType === "") { const exprNodeType = stmt.value ? (stmt.value as { type: string }).type : ""; + if (exprNodeType === "method_call") { + const genericErr = this.getGenericMethodReturnError( + stmt.value as MethodCallNode, + name, + ); + if (genericErr) return this.emitError(genericErr); + } // Expression types that can plausibly return a number at module scope. // String/array/map/etc. returning expressions should have been caught // by the specific detectors above — anything that falls through is likely numeric. @@ -3432,6 +3439,34 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { return this.typeInference.isResponseExpression(expr); } + private getGenericMethodReturnError(expr: MethodCallNode, varName: string): string | null { + const objBase = expr.object as { type: string }; + if (objBase.type !== "variable") return null; + const objName = (expr.object as { type: string; name: string }).name; + const className = this.symbolTable.getConcreteClass(objName); + if (!className || !this.ast || !this.ast.classes) return null; + for (let i = 0; i < this.ast.classes.length; i++) { + const cls = this.ast.classes[i] as ClassNode; + if (cls.name !== className) continue; + if (!cls.typeParameters || cls.typeParameters.length === 0) return null; + for (let j = 0; j < cls.methods.length; j++) { + const m = cls.methods[j]; + if (m.isConstructor || m.name !== expr.method) continue; + if (!m.returnType) return null; + for (let k = 0; k < cls.typeParameters.length; k++) { + const tp = cls.typeParameters[k] as string; + if (m.returnType === tp || m.returnType.includes(tp)) { + return ( + `'${varName}' is assigned from '${objName}.${expr.method}()' which returns generic type '${m.returnType}' — ` + + `add a type annotation: 'const ${varName}: YourType = ${objName}.${expr.method}()'` + ); + } + } + } + } + return null; + } + private isKnownClass(name: string): boolean { if (!name) return false; // Also check resolved alias (e.g., import MyGreeter from './greeter' → Greeter) From 3b5d7295968c7c9f72fe8eb4c137cb56d0491baa Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 2 Mar 2026 23:29:36 -0800 Subject: [PATCH 05/10] fix struct layout error in generic error check; add interface/class type arg support and fixtures --- .../infrastructure/variable-allocator.ts | 18 ++++++----- src/codegen/llvm-generator.ts | 21 ++++++++++++- tests/fixtures/generics/generic-class-arg.ts | 31 +++++++++++++++++++ tests/fixtures/generics/generic-interface.ts | 18 +++++++++++ 4 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/generics/generic-class-arg.ts create mode 100644 tests/fixtures/generics/generic-interface.ts diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index 7a4fffe7..15278d25 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -19,6 +19,7 @@ import { TypeAliasDeclaration, MethodCallNode, CallNode, + ClassNode, FunctionNode, CommonField, BinaryNode, @@ -276,11 +277,7 @@ export class VariableAllocator { const ast = this.ctx.getAst(); if (!ast || !ast.classes) return null; for (let i = 0; i < ast.classes.length; i++) { - const cls = ast.classes[i] as { - name: string; - typeParameters?: string[]; - methods: { name: string; isConstructor: boolean; returnType?: string }[]; - }; + const cls = ast.classes[i] as ClassNode; if (cls.name !== className) continue; if (!cls.typeParameters || cls.typeParameters.length === 0) return null; for (let j = 0; j < cls.methods.length; j++) { @@ -804,6 +801,9 @@ export class VariableAllocator { if (!isUint8Array && strippedDeclType === "Uint8Array") { isUint8Array = true; } + if (!isClassInstance && strippedDeclType && this.isKnownClass(strippedDeclType)) { + isClassInstance = true; + } // Detect Uint8Array from expression analysis (e.g. getEmbeddedFileAsUint8Array) if (!isUint8Array && this.ctx.isUint8ArrayExpression(stmtValue)) { isUint8Array = true; @@ -947,9 +947,11 @@ export class VariableAllocator { // VarKind.Numeric is correct for number/boolean literals and arithmetic, // but suspicious for calls/method calls that might return non-numeric types. if (nodeType === "call" || nodeType === "method_call") { - const genericErr = this.getGenericMethodReturnError(stmtValue, stmt.name); - if (genericErr) { - return this.ctx.emitError(genericErr); + if (!stmt.declaredType) { + const genericErr = this.getGenericMethodReturnError(stmtValue, stmt.name); + if (genericErr) { + return this.ctx.emitError(genericErr); + } } this.ctx.emitWarning( `variable '${stmt.name}' classified as numeric from expression type '${nodeType}' — ` + diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index 3a161aa9..b617d10a 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -2213,6 +2213,25 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { ); continue; } + if (this.isKnownClass(strippedDeclaredType)) { + const fields = this.classGen + ? this.classGen.getClassFields(strippedDeclaredType) || [] + : []; + llvmType = fields.length > 0 ? `%${strippedDeclaredType}_struct*` : "i8*"; + kind = SymbolKind.Class; + defaultValue = "null"; + ir += `@${name} = global ${llvmType} ${defaultValue}` + "\n"; + this.globalVariables.set(name, { llvmType, kind, initialized: false }); + this.defineVariableWithMetadata( + name, + `@${name}`, + llvmType, + kind, + "global", + createClassMetadata({ className: strippedDeclaredType }), + ); + continue; + } // Unrecognized declared type — fall through to expression-based detection } } @@ -2286,7 +2305,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { // Phase 3: Final catch-all based on expression node type if (llvmType === "") { const exprNodeType = stmt.value ? (stmt.value as { type: string }).type : ""; - if (exprNodeType === "method_call") { + if (exprNodeType === "method_call" && !stmt.declaredType) { const genericErr = this.getGenericMethodReturnError( stmt.value as MethodCallNode, name, diff --git a/tests/fixtures/generics/generic-class-arg.ts b/tests/fixtures/generics/generic-class-arg.ts new file mode 100644 index 00000000..821b200e --- /dev/null +++ b/tests/fixtures/generics/generic-class-arg.ts @@ -0,0 +1,31 @@ +class Animal { + name: string; + constructor(n: string) { + this.name = n; + } + speak(): string { + return this.name + " speaks"; + } +} +class Container { + items: T[]; + constructor() { + this.items = []; + } + add(x: T): void { + this.items.push(x); + } + get(i: number): T { + return this.items[i]; + } + size(): number { + return this.items.length; + } +} +const c = new Container(); +c.add(new Animal("cat")); +c.add(new Animal("dog")); +const a: Animal = c.get(0); +console.log(a.speak()); +console.log(c.size().toString()); +console.log("TEST_PASSED"); diff --git a/tests/fixtures/generics/generic-interface.ts b/tests/fixtures/generics/generic-interface.ts new file mode 100644 index 00000000..855be95b --- /dev/null +++ b/tests/fixtures/generics/generic-interface.ts @@ -0,0 +1,18 @@ +interface Point { + x: number; + y: number; +} +class Box { + value: T; + constructor(v: T) { + this.value = v; + } + get(): T { + return this.value; + } +} +const b = new Box({ x: 3, y: 4 }); +const p: Point = b.get(); +console.log(p.x.toString()); +console.log(p.y.toString()); +console.log("TEST_PASSED"); From 572c9296334f4d91a5f5f2d596092327ebbd7970 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 2 Mar 2026 23:46:01 -0800 Subject: [PATCH 06/10] support type alias as generic type arg (type Point = {...} works like interface) --- src/parser-native/transformer.ts | 42 +++++++++++++++---- src/parser-ts/transformer.ts | 17 ++++++-- tests/fixtures/generics/generic-type-alias.ts | 18 ++++++++ 3 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/generics/generic-type-alias.ts diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 4aa0df82..d9a63112 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -153,12 +153,16 @@ function transformTopLevelNode(node: TreeSitterNode, ast: AST): void { } break; - case "type_alias_declaration": - const typeAlias = transformTypeAliasDeclaration(node); - if (typeAlias) { - ast.typeAliases.push(typeAlias); + case "type_alias_declaration": { + const objIface = transformObjectTypeAlias(node); + if (objIface) { + ast.interfaces.push(objIface); + } else { + const typeAlias = transformTypeAliasDeclaration(node); + if (typeAlias) ast.typeAliases.push(typeAlias); } break; + } case "enum_declaration": const enumDecl = transformEnumDeclaration(node); @@ -398,9 +402,12 @@ function handleExportStatement(node: TreeSitterNode, ast: AST): void { ast.interfaces.push(iface); } } else if (c.type === "type_alias_declaration") { - const typeAlias = transformTypeAliasDeclaration(child); - if (typeAlias) { - ast.typeAliases.push(typeAlias); + const objIface = transformObjectTypeAlias(child); + if (objIface) { + ast.interfaces.push(objIface); + } else { + const typeAlias = transformTypeAliasDeclaration(child); + if (typeAlias) ast.typeAliases.push(typeAlias); } } else if (c.type === "enum_declaration") { const enumDecl = transformEnumDeclaration(child); @@ -2962,6 +2969,27 @@ function transformInterfaceDeclaration(node: TreeSitterNode): InterfaceDeclarati }; } +function transformObjectTypeAlias(node: TreeSitterNode): InterfaceDeclaration | null { + const nameNode = getChildByFieldName(node, "name"); + const valueNode = getChildByFieldName(node, "value"); + if (!nameNode || !valueNode) return null; + if ((valueNode as NodeBase).type !== "object_type") return null; + const name = (nameNode as NodeBase).text; + const fields: { name: string; type: string }[] = []; + const vn = valueNode as NodeBase; + for (let i = 0; i < vn.namedChildCount; i++) { + const member = getNamedChild(valueNode, i); + if (!member) continue; + if ((member as NodeBase).type !== "property_signature") continue; + const propNameNode = getChildByFieldName(member, "name"); + const propTypeNode = getChildByFieldName(member, "type"); + const fieldName = propNameNode ? (propNameNode as NodeBase).text : ""; + const fieldType = propTypeNode ? extractTypeString(propTypeNode) : "any"; + fields.push({ name: fieldName, type: fieldType }); + } + return { name, extends: [], fields, methods: [] }; +} + function transformTypeAliasDeclaration(node: TreeSitterNode): TypeAliasDeclaration | null { const nameNode = getChildByFieldName(node, "name"); const valueNode = getChildByFieldName(node, "value"); diff --git a/src/parser-ts/transformer.ts b/src/parser-ts/transformer.ts index a54b4d74..fc40e316 100644 --- a/src/parser-ts/transformer.ts +++ b/src/parser-ts/transformer.ts @@ -137,9 +137,20 @@ function transformTopLevelStatement( } case ts.SyntaxKind.TypeAliasDeclaration: { - const typeAlias = transformTypeAliasDeclaration(node as ts.TypeAliasDeclaration); - if (typeAlias) { - ast.typeAliases.push(typeAlias); + const taNode = node as ts.TypeAliasDeclaration; + if (ts.isTypeLiteralNode(taNode.type)) { + const fields: { name: string; type: string }[] = []; + for (const m of taNode.type.members) { + if (ts.isPropertySignature(m) && ts.isIdentifier(m.name)) { + fields.push({ name: m.name.text, type: m.type ? m.type.getText() : "any" }); + } + } + ast.interfaces.push({ name: taNode.name.text, extends: [], fields, methods: [] }); + } else { + const typeAlias = transformTypeAliasDeclaration(taNode); + if (typeAlias) { + ast.typeAliases.push(typeAlias); + } } break; } diff --git a/tests/fixtures/generics/generic-type-alias.ts b/tests/fixtures/generics/generic-type-alias.ts new file mode 100644 index 00000000..96d832da --- /dev/null +++ b/tests/fixtures/generics/generic-type-alias.ts @@ -0,0 +1,18 @@ +type Point = { + x: number; + y: number; +}; +class Box { + value: T; + constructor(v: T) { + this.value = v; + } + get(): T { + return this.value; + } +} +const b = new Box({ x: 3, y: 4 }); +const p: Point = b.get(); +console.log(p.x.toString()); +console.log(p.y.toString()); +console.log("TEST_PASSED"); From 61270c8ac5996203e05cfd72936f645f86d4eba3 Mon Sep 17 00:00:00 2001 From: cs01 Date: Tue, 3 Mar 2026 22:35:21 -0800 Subject: [PATCH 07/10] move typeParameters to end of interfaces; refactor union-type-checker; add type alias global var support --- src/ast/types.ts | 2 +- src/codegen/expressions/arrow-functions.ts | 4 +- src/codegen/llvm-generator.ts | 22 +++ src/parser-native/transformer.ts | 3 +- src/parser-ts/handlers/declarations.ts | 2 +- src/parser-ts/transformer.ts | 2 +- src/semantic/union-type-checker.ts | 206 +++++++++------------ 7 files changed, 118 insertions(+), 123 deletions(-) diff --git a/src/ast/types.ts b/src/ast/types.ts index 6103b49f..fa6d71bf 100644 --- a/src/ast/types.ts +++ b/src/ast/types.ts @@ -406,13 +406,13 @@ export interface FunctionNode { body: BlockStatement; returnType?: string; paramTypes?: string[]; - typeParameters?: string[]; async?: boolean; parameters?: FunctionParameter[]; loc?: SourceLocation; // External C function declaration (declare function foo(): void) // When true, codegen emits LLVM `declare` instead of `define`, no _cs_ prefix declare?: boolean; + typeParameters?: string[]; } export interface ClassMethod { diff --git a/src/codegen/expressions/arrow-functions.ts b/src/codegen/expressions/arrow-functions.ts index 32071385..0f9e79c6 100644 --- a/src/codegen/expressions/arrow-functions.ts +++ b/src/codegen/expressions/arrow-functions.ts @@ -163,11 +163,11 @@ export class ArrowFunctionExpressionGenerator extends BaseGenerator { body: liftedBody, returnType: liftedReturnType, paramTypes: liftedParamTypes, - typeParameters: undefined, async: undefined, parameters: undefined, loc: undefined, declare: false, + typeParameters: undefined, closureInfo, }; @@ -226,11 +226,11 @@ export class ArrowFunctionExpressionGenerator extends BaseGenerator { body: BlockStatement; returnType: string; paramTypes: string[]; - typeParameters: string[]; async: boolean; parameters: FunctionParameter[]; loc: SourceLocation; declare: boolean; + typeParameters: string[]; closureInfo: ClosureInfo; }; return func.closureInfo; diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index b617d10a..03423db6 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -2232,6 +2232,28 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { ); continue; } + // Check typeAliases — object-shaped aliases (Point, Config, etc.) resolve to i8* + if (this.ast.typeAliases) { + let foundAlias = false; + for (let i = 0; i < this.ast.typeAliases.length; i++) { + const alias = this.ast.typeAliases[i]; + if (!alias || alias.name !== strippedDeclaredType) continue; + const members = alias.unionMembers; + let aliasLlvm = "i8*"; + if (members && members.length > 0) { + aliasLlvm = tsTypeToLlvm(members[0].trim()); + } + llvmType = aliasLlvm; + kind = llvmType === "double" ? SymbolKind.Number : SymbolKind.Object; + defaultValue = llvmType === "double" ? "0.0" : "null"; + ir += `@${name} = global ${llvmType} ${defaultValue}` + "\n"; + this.globalVariables.set(name, { llvmType, kind, initialized: false }); + this.defineVariable(name, `@${name}`, llvmType, kind, "global"); + foundAlias = true; + break; + } + if (foundAlias) continue; + } // Unrecognized declared type — fall through to expression-based detection } } diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index d9a63112..033edc73 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -314,7 +314,6 @@ function handleAmbientDeclaration(node: TreeSitterNode, ast: AST): void { body: createEmptyBlock(), returnType: returnType, paramTypes: paramTypesList, - typeParameters: undefined, async: undefined, parameters: undefined, loc: undefined, @@ -2357,11 +2356,11 @@ function transformFunctionDeclaration(node: TreeSitterNode): FunctionNode | null body, returnType, paramTypes, - typeParameters, async: isAsync || undefined, parameters, loc: undefined, declare: false, + typeParameters, }; } diff --git a/src/parser-ts/handlers/declarations.ts b/src/parser-ts/handlers/declarations.ts index 20a02ba4..136d8a87 100644 --- a/src/parser-ts/handlers/declarations.ts +++ b/src/parser-ts/handlers/declarations.ts @@ -74,11 +74,11 @@ export function transformFunctionDeclaration( body, returnType, paramTypes: paramTypes.length > 0 ? paramTypes : undefined, - typeParameters, async: isAsync || undefined, parameters: parameters.length > 0 ? parameters : undefined, loc: getLoc(node), declare: false, + typeParameters, }; } diff --git a/src/parser-ts/transformer.ts b/src/parser-ts/transformer.ts index fc40e316..527d6c31 100644 --- a/src/parser-ts/transformer.ts +++ b/src/parser-ts/transformer.ts @@ -102,11 +102,11 @@ function transformTopLevelStatement( body: func.body, returnType: func.returnType, paramTypes: func.paramTypes, - typeParameters: func.typeParameters, async: func.async, parameters: func.parameters, loc: func.loc, declare: true, + typeParameters: func.typeParameters, }; ast.functions.push(declFunc); } diff --git a/src/semantic/union-type-checker.ts b/src/semantic/union-type-checker.ts index 8ab0408e..2f182e69 100644 --- a/src/semantic/union-type-checker.ts +++ b/src/semantic/union-type-checker.ts @@ -1,136 +1,110 @@ -// Union type checker — semantic pass that rejects unsafe union type aliases -// used as function/method parameter types. -// -// The existing checkUnsafeUnionType (called by SemanticAnalyzer) catches inline -// unions with different LLVM representations (e.g., `string | number`). But type -// alias unions like `type Mixed = string | number` bypass that check because the -// parameter type string is just "Mixed" (no " | " to split on). -// -// This pass resolves type aliases and checks whether their members would map to -// different LLVM types. When they do, the codegen emits the alias name literally -// as the LLVM param type, which defaults to i8* — causing a segfault if the -// caller passes a value with a different representation (e.g., double for number). - import type { AST, SourceLocation } from "../ast/types.js"; import { tsTypeToLlvm } from "../codegen/infrastructure/type-system.js"; -export function checkUnionTypes(ast: AST): void { - const checker = new UnionTypeChecker(ast); - checker.check(); -} - -class UnionTypeChecker { - private ast: AST; - // Names of type aliases whose union members have different LLVM representations - private unsafeAliases: string[]; +function buildUnsafeAliases(ast: AST): string[] { + const unsafeAliases: string[] = []; + if (!ast.typeAliases) return unsafeAliases; + for (let i = 0; i < ast.typeAliases.length; i++) { + const alias = ast.typeAliases[i]; + const members = alias.unionMembers; + if (!members || members.length < 2) continue; - constructor(ast: AST) { - this.ast = ast; - this.unsafeAliases = []; - this.buildUnsafeAliasIndex(); - } - - // Pre-compute which type alias names resolve to unions with mixed LLVM types. - private buildUnsafeAliasIndex(): void { - if (!this.ast.typeAliases) return; - for (let i = 0; i < this.ast.typeAliases.length; i++) { - const alias = this.ast.typeAliases[i]; - const members = alias.unionMembers; - if (!members || members.length < 2) continue; - - // Collect LLVM types for non-null members - const llvmTypes: string[] = []; - for (let j = 0; j < members.length; j++) { - const m = members[j].trim(); - if (m === "null" || m === "undefined") continue; - llvmTypes.push(tsTypeToLlvm(m)); - } - if (llvmTypes.length < 2) continue; + const llvmTypes: string[] = []; + for (let j = 0; j < members.length; j++) { + const m = members[j].trim(); + if (m === "null" || m === "undefined") continue; + llvmTypes.push(tsTypeToLlvm(m)); + } + if (llvmTypes.length < 2) continue; - // Check if any member has a different LLVM type than the first - let hasMixed = false; - for (let j = 1; j < llvmTypes.length; j++) { - if (llvmTypes[j] !== llvmTypes[0]) { - hasMixed = true; - break; - } - } - if (hasMixed) { - this.unsafeAliases.push(alias.name); + let hasMixed = false; + for (let j = 1; j < llvmTypes.length; j++) { + if (llvmTypes[j] !== llvmTypes[0]) { + hasMixed = true; + break; } } - } - - private isUnsafeAlias(typeName: string): boolean { - let name = typeName; - if (name.endsWith("[]")) { - name = name.substring(0, name.length - 2); + if (hasMixed) { + unsafeAliases.push(alias.name); } - return this.unsafeAliases.indexOf(name) !== -1; } + return unsafeAliases; +} - check(): void { - // Check standalone function parameters - for (let i = 0; i < this.ast.functions.length; i++) { - const fn = this.ast.functions[i]; - if (fn.declare) continue; - this.checkParams(fn.name, fn.paramTypes, fn as { loc?: SourceLocation }); - } +function isUnsafeAlias(unsafeAliases: string[], typeName: string): boolean { + let name = typeName; + if (name.endsWith("[]")) { + name = name.substring(0, name.length - 2); + } + return unsafeAliases.indexOf(name) !== -1; +} - // Check class method parameters - for (let i = 0; i < this.ast.classes.length; i++) { - const cls = this.ast.classes[i]; - for (let j = 0; j < cls.methods.length; j++) { - const method = cls.methods[j]; - const qualName = cls.name + "." + method.name; - this.checkParams(qualName, method.paramTypes, method as { loc?: SourceLocation }); - } - } +function reportUnionError( + funcName: string, + aliasName: string, + loc: SourceLocation | undefined, +): void { + let msg = ""; + if (loc !== null && loc !== undefined) { + const file = loc.file || ""; + msg += + file + + ":" + + loc.line + + ":" + + (loc.column + 1) + + ": error: in function '" + + funcName + + "', parameter type '" + + aliasName + + "' is a union type alias with mixed representations\n"; + } else { + msg += + "error: in function '" + + funcName + + "', parameter type '" + + aliasName + + "' is a union type alias with mixed representations\n"; } + msg += + " note: '" + + aliasName + + "' is a type alias for a union whose members have different native types (e.g., i8* vs double)\n"; + msg += + " note: this will be miscompiled and segfault at runtime. Use a common base interface or separate the types.\n"; + console.error(msg); + process.exit(1); +} - private checkParams( - funcName: string, - paramTypes: string[] | undefined, - locHolder: { loc?: SourceLocation }, - ): void { - if (!paramTypes) return; - for (let i = 0; i < paramTypes.length; i++) { - if (this.isUnsafeAlias(paramTypes[i])) { - this.reportError(funcName, paramTypes[i], locHolder.loc); +export function checkUnionTypes(ast: AST): void { + const unsafeAliases = buildUnsafeAliases(ast); + + for (let i = 0; i < ast.functions.length; i++) { + const fn = ast.functions[i]; + if (fn.declare) continue; + const paramTypes = fn.paramTypes; + if (!paramTypes) continue; + const locHolder = fn as { loc?: SourceLocation }; + for (let j = 0; j < paramTypes.length; j++) { + if (isUnsafeAlias(unsafeAliases, paramTypes[j])) { + reportUnionError(fn.name, paramTypes[j], locHolder.loc); } } } - private reportError(funcName: string, aliasName: string, loc: SourceLocation | undefined): void { - let msg = ""; - if (loc !== null && loc !== undefined) { - const file = loc.file || ""; - msg += - file + - ":" + - loc.line + - ":" + - (loc.column + 1) + - ": error: in function '" + - funcName + - "', parameter type '" + - aliasName + - "' is a union type alias with mixed representations\n"; - } else { - msg += - "error: in function '" + - funcName + - "', parameter type '" + - aliasName + - "' is a union type alias with mixed representations\n"; + for (let i = 0; i < ast.classes.length; i++) { + const cls = ast.classes[i]; + for (let k = 0; k < cls.methods.length; k++) { + const method = cls.methods[k]; + const qualName = cls.name + "." + method.name; + const paramTypes = method.paramTypes; + if (!paramTypes) continue; + const locHolder = method as { loc?: SourceLocation }; + for (let j = 0; j < paramTypes.length; j++) { + if (isUnsafeAlias(unsafeAliases, paramTypes[j])) { + reportUnionError(qualName, paramTypes[j], locHolder.loc); + } + } } - msg += - " note: '" + - aliasName + - "' is a type alias for a union whose members have different native types (e.g., i8* vs double)\n"; - msg += - " note: this will be miscompiled and segfault at runtime. Use a common base interface or separate the types.\n"; - console.error(msg); - process.exit(1); } } From 08f5f6e99935541b196892c76bb965dc1ef976c7 Mon Sep 17 00:00:00 2001 From: cs01 Date: Wed, 4 Mar 2026 01:14:25 -0800 Subject: [PATCH 08/10] fix FunctionNode struct layout crash in native compiler --- .../expressions/method-calls/class-dispatch.ts | 6 +++--- src/compiler.ts | 11 ++++++++++- src/parser-native/transformer.ts | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/codegen/expressions/method-calls/class-dispatch.ts b/src/codegen/expressions/method-calls/class-dispatch.ts index 255aba28..7c1ea314 100644 --- a/src/codegen/expressions/method-calls/class-dispatch.ts +++ b/src/codegen/expressions/method-calls/class-dispatch.ts @@ -24,7 +24,7 @@ type ClassNode = { implements?: string[]; methods: { name: string; isConstructor?: boolean; isStatic?: boolean }[]; }; -type FunctionNode = { +type FunctionMeta = { name: string; returnType?: string; parameters?: { type: string }[]; @@ -66,12 +66,12 @@ export function getInterfaceDecl( return null; } -function getFunctionFromAST(ctx: MethodCallGeneratorContext, name: string): FunctionNode | null { +function getFunctionFromAST(ctx: MethodCallGeneratorContext, name: string): FunctionMeta | null { const len = ctx.getAstFunctionsLength(); for (let i = 0; i < len; i++) { const funcName = ctx.getAstFunctionNameAt(i); if (funcName === name) { - return ctx.getAstFunctionAt(i) as FunctionNode | null; + return ctx.getAstFunctionAt(i) as FunctionMeta | null; } } return null; diff --git a/src/compiler.ts b/src/compiler.ts index 1309ce81..e69d7e24 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -696,6 +696,11 @@ function compileMultiFile( body: fn.body, returnType: fn.returnType, paramTypes: fn.paramTypes, + async: fn.async, + parameters: fn.parameters, + loc: fn.loc, + declare: false, + typeParameters: fn.typeParameters, }; importedAST.functions.push(aliasFn); break; @@ -780,13 +785,17 @@ function compileMultiFile( for (let fi = 0; fi < importedAST.functions.length; fi++) { const fn = importedAST.functions[fi]; if (fn.name === defExported) { - // Match native parser FunctionNode field order exactly const aliasFn: FunctionNode = { name: defLocal, params: fn.params, body: fn.body, returnType: fn.returnType, paramTypes: fn.paramTypes, + async: fn.async, + parameters: fn.parameters, + loc: fn.loc, + declare: false, + typeParameters: fn.typeParameters, }; importedAST.functions.push(aliasFn); break; diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 033edc73..ce6f91df 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -318,6 +318,7 @@ function handleAmbientDeclaration(node: TreeSitterNode, ast: AST): void { parameters: undefined, loc: undefined, declare: true, + typeParameters: undefined, }; ast.functions.push(func); } From 9b9d5a7aebedd44b984808fc8224493905314cd5 Mon Sep 17 00:00:00 2001 From: cs01 Date: Wed, 4 Mar 2026 03:54:59 -0800 Subject: [PATCH 09/10] fix type alias switch case block scope causing native compiler to skip case --- src/codegen/expressions/method-calls/class-dispatch.ts | 7 +++++-- src/compiler.ts | 4 ++++ src/parser-native/transformer.ts | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/codegen/expressions/method-calls/class-dispatch.ts b/src/codegen/expressions/method-calls/class-dispatch.ts index 7c1ea314..15eecf23 100644 --- a/src/codegen/expressions/method-calls/class-dispatch.ts +++ b/src/codegen/expressions/method-calls/class-dispatch.ts @@ -16,13 +16,16 @@ interface InterfaceDefInfo { properties: { name: string; type: string }[]; } -type NewNode = { type: "new"; className: string }; +type NewMeta = { type: "new"; className: string }; type ObjectNode = { type: "object"; properties: { key: string }[] }; type ClassNode = { name: string; extends?: string; implements?: string[]; + fields: { name: string; type: string }[]; methods: { name: string; isConstructor?: boolean; isStatic?: boolean }[]; + loc?: { line: number; column: number }; + typeParameters?: string[]; }; type FunctionMeta = { name: string; @@ -477,7 +480,7 @@ export function handleClassMethods( } } } else if (exprObjBase.type === "new") { - const newExpr = expr.object as NewNode; + const newExpr = expr.object as NewMeta; className = newExpr.className; instancePtr = ctx.generateExpression(expr.object, params); } else if (exprObjBase.type === "this") { diff --git a/src/compiler.ts b/src/compiler.ts index e69d7e24..238b8ce5 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -682,6 +682,8 @@ function compileMultiFile( implements: cls.implements, fields: cls.fields, methods: cls.methods, + loc: cls.loc, + typeParameters: cls.typeParameters, }; importedAST.classes.push(aliasClass); break; @@ -777,6 +779,8 @@ function compileMultiFile( implements: cls.implements, fields: cls.fields, methods: cls.methods, + loc: cls.loc, + typeParameters: cls.typeParameters, }; importedAST.classes.push(aliasClass); break; diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index ce6f91df..72d4b4fa 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -153,7 +153,7 @@ function transformTopLevelNode(node: TreeSitterNode, ast: AST): void { } break; - case "type_alias_declaration": { + case "type_alias_declaration": const objIface = transformObjectTypeAlias(node); if (objIface) { ast.interfaces.push(objIface); @@ -162,7 +162,6 @@ function transformTopLevelNode(node: TreeSitterNode, ast: AST): void { if (typeAlias) ast.typeAliases.push(typeAlias); } break; - } case "enum_declaration": const enumDecl = transformEnumDeclaration(node); @@ -2711,6 +2710,7 @@ function transformClassDeclaration(node: TreeSitterNode): ClassNode | null { implements: implementsClause, fields, methods, + loc: undefined, typeParameters, }; } From eb39218ca63fd09a2f78350ccd7aab23aab15570 Mon Sep 17 00:00:00 2001 From: cs01 Date: Wed, 4 Mar 2026 08:16:19 -0800 Subject: [PATCH 10/10] fix FunctionMeta wrong field order causing segfault in native; use FunctionNode directly --- .../expressions/method-calls/class-dispatch.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/codegen/expressions/method-calls/class-dispatch.ts b/src/codegen/expressions/method-calls/class-dispatch.ts index 15eecf23..efbd082e 100644 --- a/src/codegen/expressions/method-calls/class-dispatch.ts +++ b/src/codegen/expressions/method-calls/class-dispatch.ts @@ -6,6 +6,8 @@ import { TypeAssertionNode, InterfaceDeclaration, InterfaceField, + FunctionNode, + FunctionParameter, } from "../../../ast/types.js"; import type { MethodCallGeneratorContext } from "../method-calls.js"; @@ -27,12 +29,6 @@ type ClassNode = { loc?: { line: number; column: number }; typeParameters?: string[]; }; -type FunctionMeta = { - name: string; - returnType?: string; - parameters?: { type: string }[]; - paramTypes?: string[]; -}; export function getInterfaceFromAST( ctx: MethodCallGeneratorContext, @@ -69,12 +65,12 @@ export function getInterfaceDecl( return null; } -function getFunctionFromAST(ctx: MethodCallGeneratorContext, name: string): FunctionMeta | null { +function getFunctionFromAST(ctx: MethodCallGeneratorContext, name: string): FunctionNode | null { const len = ctx.getAstFunctionsLength(); for (let i = 0; i < len; i++) { const funcName = ctx.getAstFunctionNameAt(i); if (funcName === name) { - return ctx.getAstFunctionAt(i) as FunctionMeta | null; + return ctx.getAstFunctionAt(i) as FunctionNode | null; } } return null;