diff --git a/src/ast/types.ts b/src/ast/types.ts index 98abb3bb..fa6d71bf 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 { @@ -405,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 { @@ -441,6 +442,7 @@ export interface ClassNode { fields: ClassField[]; // Explicit field declarations methods: ClassMethod[]; loc?: SourceLocation; + typeParameters?: string[]; } export interface ImportSpecifier { 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/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/expressions/method-calls/class-dispatch.ts b/src/codegen/expressions/method-calls/class-dispatch.ts index 255aba28..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"; @@ -16,19 +18,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 }[]; -}; -type FunctionNode = { - name: string; - returnType?: string; - parameters?: { type: string }[]; - paramTypes?: string[]; + loc?: { line: number; column: number }; + typeParameters?: string[]; }; export function getInterfaceFromAST( @@ -477,7 +476,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/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index b4c73668..15278d25 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -19,6 +19,8 @@ import { TypeAliasDeclaration, MethodCallNode, CallNode, + ClassNode, + FunctionNode, CommonField, BinaryNode, MapNode, @@ -263,6 +265,77 @@ 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 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 !== 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; + 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); } @@ -728,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; @@ -751,6 +827,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, @@ -864,6 +947,12 @@ 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") { + 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}' — ` + `if this is wrong, add a type annotation`, diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index 52919bc5..03423db6 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, { @@ -2196,6 +2213,47 @@ 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; + } + // 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 } } @@ -2269,6 +2327,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" && !stmt.declaredType) { + 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. @@ -3415,6 +3480,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) 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/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/src/compiler.ts b/src/compiler.ts index 1309ce81..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; @@ -696,6 +698,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; @@ -772,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; @@ -780,13 +789,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 36a30a8c..72d4b4fa 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -154,9 +154,12 @@ function transformTopLevelNode(node: TreeSitterNode, ast: AST): void { break; case "type_alias_declaration": - const typeAlias = transformTypeAliasDeclaration(node); - if (typeAlias) { - ast.typeAliases.push(typeAlias); + const objIface = transformObjectTypeAlias(node); + if (objIface) { + ast.interfaces.push(objIface); + } else { + const typeAlias = transformTypeAliasDeclaration(node); + if (typeAlias) ast.typeAliases.push(typeAlias); } break; @@ -310,11 +313,11 @@ function handleAmbientDeclaration(node: TreeSitterNode, ast: AST): void { body: createEmptyBlock(), returnType: returnType, paramTypes: paramTypesList, - typeParameters: undefined, async: undefined, parameters: undefined, loc: undefined, declare: true, + typeParameters: undefined, }; ast.functions.push(func); } @@ -398,9 +401,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); @@ -973,7 +979,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 { @@ -2339,11 +2356,11 @@ function transformFunctionDeclaration(node: TreeSitterNode): FunctionNode | null body, returnType, paramTypes, - typeParameters, async: isAsync || undefined, parameters, loc: undefined, declare: false, + typeParameters, }; } @@ -2666,7 +2683,36 @@ 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, + loc: undefined, + typeParameters, + }; } function transformClassField(node: TreeSitterNode): ClassField | null { @@ -2923,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/handlers/declarations.ts b/src/parser-ts/handlers/declarations.ts index eb33316c..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, }; } @@ -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/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/src/parser-ts/transformer.ts b/src/parser-ts/transformer.ts index a54b4d74..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); } @@ -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/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); } } 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-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-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/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"); 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"); 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"); 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"); 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");