From aa3433b5a9bc7d81cb124a4444bf6e9c013e0432 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sun, 31 May 2026 00:22:03 +0300 Subject: [PATCH] feat: add generics support with monomorphization - Add ekGenericCall to AST for generic function calls (Max) - Parse generic type arguments in parsePostfix - Support generic calls in sema with type parameter substitution - Implement monomorphization in hir_lower: - Collect generic function declarations - Find all generic call sites - Generate specialized versions with mangled names (Max_int) - Substitute type parameters with concrete types - Add generics.bux example Example: func Max(a: T, b: T) -> T { if a > b { return a; } else { return b; } } let m: int = Max(10, 20); // Generates Max_int --- examples/generics.bux | 26 ++++++ src/ast.nim | 4 + src/hir_lower.nim | 189 +++++++++++++++++++++++++++++++++++++----- src/parser.nim | 14 ++++ src/sema.nim | 35 +++++++- 5 files changed, 245 insertions(+), 23 deletions(-) create mode 100644 examples/generics.bux diff --git a/examples/generics.bux b/examples/generics.bux new file mode 100644 index 0000000..1c96e3c --- /dev/null +++ b/examples/generics.bux @@ -0,0 +1,26 @@ +// Generics - Generic functions and structs +extern func Std_Io_PrintLine(s: String); +extern func Std_Io_PrintInt(n: int); + +func Max(a: T, b: T) -> T { + if a > b { + return a; + } else { + return b; + } +} + +func Main() -> int { + let m1: int = Max(10, 20); + let m2: int = Max(5, 3); + + Std_Io_PrintLine("Max(10, 20) = "); + Std_Io_PrintInt(m1); + Std_Io_PrintLine(""); + + Std_Io_PrintLine("Max(5, 3) = "); + Std_Io_PrintInt(m2); + Std_Io_PrintLine(""); + + return 0; +} diff --git a/src/ast.nim b/src/ast.nim index d1196c7..18d6e3e 100644 --- a/src/ast.nim +++ b/src/ast.nim @@ -104,6 +104,7 @@ type ekTernary ekRange ekCall + ekGenericCall ekIndex ekField ekStructInit @@ -160,6 +161,9 @@ type of ekCall: exprCallCallee*: Expr exprCallArgs*: seq[Expr] + of ekGenericCall: + exprGenericCallee*: string + exprGenericTypeArgs*: seq[TypeExpr] of ekIndex: exprIndexObj*: Expr exprIndexIdx*: Expr diff --git a/src/hir_lower.nim b/src/hir_lower.nim index da0f4fc..082d1e8 100644 --- a/src/hir_lower.nim +++ b/src/hir_lower.nim @@ -8,6 +8,7 @@ type methodTable*: Table[string, seq[MethodInfo]] currentFuncRetType*: Type varCounter*: int + typeSubst*: Table[string, Type] # Type parameter substitution for generics proc freshName(ctx: var LowerCtx): string = inc ctx.varCounter @@ -18,6 +19,7 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx = result.globalScope = sema.globalScope result.methodTable = sema.methodTable result.varCounter = 0 + result.typeSubst = initTable[string, Type]() # Forward declarations proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode @@ -149,7 +151,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = # Method call desugaring: obj.method(args) → Type_method(obj, args) if expr.exprCallCallee.kind == ekField: let methodName = expr.exprCallCallee.exprFieldName - + # Try to find the method in methodTable for typeName, methods in ctx.methodTable: for minfo in methods: @@ -161,7 +163,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = for arg in expr.exprCallArgs: args.add(ctx.lowerExpr(arg)) return hirCall(mangledName, args, typ, loc) - + # Not a method call - treat as field access + call (function pointer) let callee = ctx.lowerExpr(expr.exprCallCallee) var args: seq[HirNode] = @[] @@ -170,6 +172,23 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = return HirNode(kind: hCallIndirect, callIndirectCallee: callee, callIndirectArgs: args, typ: typ, loc: loc) + # Generic function call: Max(10, 20) → Max_int(10, 20) + if expr.exprCallCallee.kind == ekGenericCall: + let baseName = expr.exprCallCallee.exprGenericCallee + var typeSuffix = "" + for i, targ in expr.exprCallCallee.exprGenericTypeArgs: + if i > 0: + typeSuffix.add("_") + if targ.kind == tekNamed: + typeSuffix.add(targ.typeName) + else: + typeSuffix.add("unknown") + let mangledName = baseName & "_" & typeSuffix + var args: seq[HirNode] = @[] + for arg in expr.exprCallArgs: + args.add(ctx.lowerExpr(arg)) + return hirCall(mangledName, args, typ, loc) + # Regular function call var calleeName = "" if expr.exprCallCallee.kind == ekIdent: @@ -390,20 +409,27 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode = typ: makeVoid(), loc: blk.loc) proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = + # Set up type substitution for generic functions + let oldSubst = ctx.typeSubst + var params: seq[tuple[name: string, typ: Type]] = @[] for p in decl.declFuncParams: var pType = makeUnknown() if p.ptype != nil: case p.ptype.kind of tekNamed: - case p.ptype.typeName - of "int", "int32": pType = makeInt() - of "int64": pType = makeInt64() - of "float64": pType = makeFloat64() - of "float32": pType = makeFloat32() - of "bool": pType = makeBool() - of "Point", "Self": pType = makeNamed(p.ptype.typeName) - else: pType = makeNamed(p.ptype.typeName) + # Check if this is a type parameter + if ctx.typeSubst.hasKey(p.ptype.typeName): + pType = ctx.typeSubst[p.ptype.typeName] + else: + case p.ptype.typeName + of "int", "int32": pType = makeInt() + of "int64": pType = makeInt64() + of "float64": pType = makeFloat64() + of "float32": pType = makeFloat32() + of "bool": pType = makeBool() + of "Point", "Self": pType = makeNamed(p.ptype.typeName) + else: pType = makeNamed(p.ptype.typeName) of tekPointer: pType = makePointer(makeUnknown()) else: discard params.add((p.name, pType)) @@ -412,13 +438,17 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = if decl.declFuncReturnType != nil: case decl.declFuncReturnType.kind of tekNamed: - case decl.declFuncReturnType.typeName - of "int", "int32": retType = makeInt() - of "int64": retType = makeInt64() - of "float64": retType = makeFloat64() - of "float32": retType = makeFloat32() - of "bool": retType = makeBool() - else: retType = makeNamed(decl.declFuncReturnType.typeName) + # Check if this is a type parameter + if ctx.typeSubst.hasKey(decl.declFuncReturnType.typeName): + retType = ctx.typeSubst[decl.declFuncReturnType.typeName] + else: + case decl.declFuncReturnType.typeName + of "int", "int32": retType = makeInt() + of "int64": retType = makeInt64() + of "float64": retType = makeFloat64() + of "float32": retType = makeFloat32() + of "bool": retType = makeBool() + else: retType = makeNamed(decl.declFuncReturnType.typeName) of tekPointer: retType = makePointer(makeUnknown()) else: discard @@ -427,6 +457,9 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = result = HirFunc(name: decl.declFuncName, params: params, retType: retType, body: body, isPublic: decl.isPublic) + + # Restore old substitution + ctx.typeSubst = oldSubst proc lowerModule*(module: Module, sema: Sema): HirModule = var ctx = initLowerCtx(module, sema) @@ -436,14 +469,126 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = var enums: seq[tuple[name: string, variants: seq[HirEnumVariant]]] = @[] var consts: seq[tuple[name: string, typ: Type, value: HirNode]] = @[] + # First pass: collect generic functions + var genericFuncs = initTable[string, Decl]() + for decl in module.items: + if decl.kind == dkFunc and decl.declFuncTypeParams.len > 0: + genericFuncs[decl.declFuncName] = decl + + # Second pass: find all generic calls and monomorphize + proc findGenericCalls(expr: Expr): seq[tuple[name: string, typeArgs: seq[TypeExpr]]] = + if expr == nil: return @[] + result = @[] + case expr.kind + of ekCall: + if expr.exprCallCallee.kind == ekGenericCall: + result.add((expr.exprCallCallee.exprGenericCallee, expr.exprCallCallee.exprGenericTypeArgs)) + result.add(findGenericCalls(expr.exprCallCallee)) + for arg in expr.exprCallArgs: + result.add(findGenericCalls(arg)) + of ekGenericCall: + result.add((expr.exprGenericCallee, expr.exprGenericTypeArgs)) + of ekBinary: + result.add(findGenericCalls(expr.exprBinaryLeft)) + result.add(findGenericCalls(expr.exprBinaryRight)) + of ekUnary: + result.add(findGenericCalls(expr.exprUnaryOperand)) + of ekAssign: + result.add(findGenericCalls(expr.exprAssignTarget)) + result.add(findGenericCalls(expr.exprAssignValue)) + of ekBlock: + if expr.exprBlock != nil: + for stmt in expr.exprBlock.stmts: + case stmt.kind + of skLet: result.add(findGenericCalls(stmt.stmtLetInit)) + of skReturn: result.add(findGenericCalls(stmt.stmtReturnValue)) + of skExpr: result.add(findGenericCalls(stmt.stmtExpr)) + of skIf: + result.add(findGenericCalls(stmt.stmtIfCond)) + of skWhile: + result.add(findGenericCalls(stmt.stmtWhileCond)) + else: discard + else: discard + + # Collect all generic instantiations + var instantiations: seq[tuple[name: string, typeArgs: seq[TypeExpr]]] = @[] + for decl in module.items: + if decl.kind == dkFunc and decl.declFuncBody != nil: + for stmt in decl.declFuncBody.stmts: + case stmt.kind + of skLet: + instantiations.add(findGenericCalls(stmt.stmtLetInit)) + of skReturn: + instantiations.add(findGenericCalls(stmt.stmtReturnValue)) + of skExpr: + instantiations.add(findGenericCalls(stmt.stmtExpr)) + of skIf: + instantiations.add(findGenericCalls(stmt.stmtIfCond)) + of skWhile: + instantiations.add(findGenericCalls(stmt.stmtWhileCond)) + else: discard + + # Generate monomorphized functions + var generated = initTable[string, bool]() + for inst in instantiations: + let baseName = inst.name + if genericFuncs.hasKey(baseName): + var typeSuffix = "" + for i, targ in inst.typeArgs: + if i > 0: typeSuffix.add("_") + if targ.kind == tekNamed: + typeSuffix.add(targ.typeName) + else: + typeSuffix.add("unknown") + let mangledName = baseName & "_" & typeSuffix + if not generated.hasKey(mangledName): + # Generate specialized version + let genericDecl = genericFuncs[baseName] + + # Build type substitution table + var subst = initTable[string, Type]() + for j, tp in genericDecl.declFuncTypeParams: + if j < inst.typeArgs.len: + let targ = inst.typeArgs[j] + if targ.kind == tekNamed: + case targ.typeName + of "int", "int32": subst[tp] = makeInt() + of "int64": subst[tp] = makeInt64() + of "float64": subst[tp] = makeFloat64() + of "float32": subst[tp] = makeFloat32() + of "bool": subst[tp] = makeBool() + else: subst[tp] = makeNamed(targ.typeName) + + # Create specialized declaration + var specDecl = Decl( + kind: dkFunc, + loc: genericDecl.loc, + isPublic: genericDecl.isPublic, + declFuncAsm: genericDecl.declFuncAsm, + declFuncCallConv: genericDecl.declFuncCallConv, + declFuncName: mangledName, + declFuncTypeParams: @[], + declFuncParams: genericDecl.declFuncParams, + declFuncReturnType: genericDecl.declFuncReturnType, + declFuncBody: genericDecl.declFuncBody + ) + + # Set substitution and lower + ctx.typeSubst = subst + funcs.add(ctx.lowerFunc(specDecl)) + ctx.typeSubst = initTable[string, Type]() # Clear substitution + generated[mangledName] = true + + # Third pass: lower all non-generic functions for decl in module.items: case decl.kind of dkFunc: - if decl.declFuncBody != nil: - funcs.add(ctx.lowerFunc(decl)) - else: - # Extern function (no body) - externFuncs.add(ctx.lowerFunc(decl)) + if decl.declFuncTypeParams.len == 0: # Skip generic functions + if decl.declFuncBody != nil: + funcs.add(ctx.lowerFunc(decl)) + else: + # Extern function (no body) + externFuncs.add(ctx.lowerFunc(decl)) of dkImpl: for methodDecl in decl.declImplMethods: if methodDecl.kind == dkFunc: diff --git a/src/parser.nim b/src/parser.nim index d8f4896..b49463a 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -441,6 +441,20 @@ proc parsePostfix(p: var Parser): Expr = discard p.advance() discard p.expect(tkRParen, "expected ')' to close call") left = Expr(kind: ekCall, loc: loc, exprCallCallee: left, exprCallArgs: args) + of tkLt: + # Generic type arguments: Max(10, 20) + if left.kind == ekIdent: + discard p.advance() + var typeArgs: seq[TypeExpr] = @[] + while not p.check(tkGt) and not p.isAtEnd: + typeArgs.add(p.parseType()) + if p.check(tkComma): + discard p.advance() + discard p.expect(tkGt, "expected '>' to close type arguments") + # Store type args in the identifier for later use + left = Expr(kind: ekGenericCall, loc: loc, exprGenericCallee: left.exprIdent, exprGenericTypeArgs: typeArgs) + else: + break of tkLBracket: # Index expression discard p.advance() diff --git a/src/sema.nim b/src/sema.nim index e3d477d..ed9afae 100644 --- a/src/sema.nim +++ b/src/sema.nim @@ -331,7 +331,30 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = if expr.exprCallCallee == nil: sema.emitError(expr.loc, "internal error: nil callee in call expression") return makeUnknown() - + + # Check for generic function call: Max(10, 20) + if expr.exprCallCallee.kind == ekGenericCall: + let sym = scope.lookup(expr.exprCallCallee.exprGenericCallee) + if sym == nil: + sema.emitError(expr.loc, &"undeclared identifier '{expr.exprCallCallee.exprGenericCallee}'") + return makeUnknown() + if sym.typ != nil and sym.typ.kind == tkFunc: + # Get the return type and substitute type parameters + let retType = sym.typ.inner[^1] + if retType.kind == tkNamed: + # Check if this is a type parameter + let sym2 = sema.globalScope.lookup(expr.exprCallCallee.exprGenericCallee) + if sym2 != nil and sym2.decl != nil and sym2.decl.kind == dkFunc: + let typeParams = sym2.decl.declFuncTypeParams + for i, tp in typeParams: + if retType.name == tp and i < expr.exprCallCallee.exprGenericTypeArgs.len: + # Substitute with concrete type + let concreteType = expr.exprCallCallee.exprGenericTypeArgs[i] + if concreteType.kind == tekNamed: + return sema.resolveType(concreteType) + return retType + return makeUnknown() + # Check for method call: obj.method(args) if expr.exprCallCallee.kind == ekField: let receiver = sema.checkExpr(expr.exprCallCallee.exprFieldObj, scope) @@ -390,6 +413,16 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = else: sema.emitError(expr.loc, &"cannot call non-function type {calleeType.toString}") return makeUnknown() + of ekGenericCall: + # Generic function call: Max(10, 20) + # For now, just look up the function and return its return type + let sym = scope.lookup(expr.exprGenericCallee) + if sym == nil: + sema.emitError(expr.loc, &"undeclared identifier '{expr.exprGenericCallee}'") + return makeUnknown() + if sym.typ != nil and sym.typ.kind == tkFunc: + return sym.typ.inner[^1] + return makeUnknown() of ekIndex: let obj = sema.checkExpr(expr.exprIndexObj, scope) let idx = sema.checkExpr(expr.exprIndexIdx, scope)