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");