feat: add generics support with monomorphization

- Add ekGenericCall to AST for generic function calls (Max<int>)
- 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<T>(a: T, b: T) -> T {
      if a > b { return a; }
      else { return b; }
  }

  let m: int = Max<int>(10, 20);  // Generates Max_int
This commit is contained in:
2026-05-31 00:22:03 +03:00
parent cf074bec89
commit aa3433b5a9
5 changed files with 245 additions and 23 deletions
+26
View File
@@ -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<T>(a: T, b: T) -> T {
if a > b {
return a;
} else {
return b;
}
}
func Main() -> int {
let m1: int = Max<int>(10, 20);
let m2: int = Max<int>(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;
}
+4
View File
@@ -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
+145
View File
@@ -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
@@ -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<int>(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,12 +409,19 @@ 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:
# 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()
@@ -412,6 +438,10 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
if decl.declFuncReturnType != nil:
case decl.declFuncReturnType.kind
of tekNamed:
# 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()
@@ -428,6 +458,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)
var funcs: seq[HirFunc] = @[]
@@ -436,9 +469,121 @@ 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.declFuncTypeParams.len == 0: # Skip generic functions
if decl.declFuncBody != nil:
funcs.add(ctx.lowerFunc(decl))
else:
+14
View File
@@ -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<int>(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()
+33
View File
@@ -332,6 +332,29 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
sema.emitError(expr.loc, "internal error: nil callee in call expression")
return makeUnknown()
# Check for generic function call: Max<int>(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<int>(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)