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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export interface CallNode {
name: string;
args: Expression[];
loc?: SourceLocation;
typeArgs?: string[];
}

export interface MethodCallNode {
Expand Down Expand Up @@ -441,6 +442,7 @@ export interface ClassNode {
fields: ClassField[]; // Explicit field declarations
methods: ClassMethod[];
loc?: SourceLocation;
typeParameters?: string[];
}

export interface ImportSpecifier {
Expand Down
12 changes: 10 additions & 2 deletions src/codegen/expressions/method-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1244,9 +1251,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);
Expand Down
89 changes: 89 additions & 0 deletions src/codegen/infrastructure/variable-allocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
TypeAliasDeclaration,
MethodCallNode,
CallNode,
ClassNode,
FunctionNode,
CommonField,
BinaryNode,
MapNode,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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`,
Expand Down
75 changes: 73 additions & 2 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -2196,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
}
}
Expand Down Expand Up @@ -2269,6 +2305,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.
Expand Down Expand Up @@ -3415,6 +3458,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)
Expand Down
59 changes: 58 additions & 1 deletion src/codegen/types/collections/array/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,28 @@ 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") {
const varNode = expr.object as VariableNode;
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 {
Expand Down Expand Up @@ -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*");

Expand Down
Loading
Loading