feat: generic methods on generic structs + method call auto-addressing

- Auto-register func Type_Method<T>(self: *Type<T>) as methods in sema
- Add type param awareness to sema resolveType for generic function bodies
- Add lazy monomorphization for generic struct method calls in HIR lowering
- Track varTypeExprs in LowerCtx for local variable type inference
- Fix ekSelf in both sema and hir_lower to resolve actual parameter type
- Fix lowerFunc to use substituteType for param/return types (handles pointers to generic structs)
- Auto-address value receivers when method expects pointer (e.g., b.Get() where self: *Box<T>)
- Add C forward declarations for all functions to fix ordering issues
- Relax type checks for tkTypeParam in assignments and arguments
- Update generics_struct example with Box_Get, Box_Set, Pair_GetFirst/Second
This commit is contained in:
2026-05-31 11:57:52 +03:00
parent 9f733aca7d
commit af14f392f6
5 changed files with 220 additions and 35 deletions
+28 -2
View File
@@ -10,15 +10,41 @@ struct Pair<T, U> {
second: U,
}
func Box_Get<T>(self: *Box<T>) -> T {
return self.value;
}
func Box_Set<T>(self: *Box<T>, value: T) {
self.value = value;
}
func Pair_GetFirst<T, U>(self: *Pair<T, U>) -> T {
return self.first;
}
func Pair_GetSecond<T, U>(self: *Pair<T, U>) -> U {
return self.second;
}
func Main() -> int {
let b: Box<int> = Box<int> { value: 42 };
PrintLine("Box value:");
PrintInt(b.value);
PrintInt(b.Get());
PrintLine("");
b.Set(100);
PrintLine("Box after Set(100):");
PrintInt(b.Get());
PrintLine("");
let p: Pair<int, String> = Pair<int, String> { first: 10, second: "hello" };
PrintLine("Pair first:");
PrintInt(p.first);
PrintInt(p.GetFirst());
PrintLine("");
let bp: *Box<int> = &b;
PrintLine("Box via pointer:");
PrintInt(bp.Get());
PrintLine("");
return 0;
+11
View File
@@ -490,6 +490,17 @@ proc emitModule*(be: var CBackend, module: HirModule): string =
for e in module.enums:
be.emitEnum(e.name, e.variants)
# Forward declarations for all functions
for f in module.funcs:
let retType = typeToC(f.retType)
var params: seq[string] = @[]
for p in f.params:
params.add(typeToC(p.typ) & " " & p.name)
if params.len == 0:
params.add("void")
be.emitLine(retType & " " & f.name & "(" & params.join(", ") & ");")
be.emitLine("")
# Function definitions
var hasMain = false
for f in module.funcs:
+139 -32
View File
@@ -16,6 +16,11 @@ type
genericStructs*: Table[string, Decl] # Generic struct declarations
generatedStructInsts*: Table[string, bool] # Track generated struct instantiations
extraStructs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]]
structInstMap*: Table[string, tuple[baseName: string, typeArgs: seq[Type]]] # Mangled name -> base + args
genericFuncs*: Table[string, Decl] # Generic function declarations
generatedFuncInsts*: Table[string, bool] # Track generated function instantiations
extraFuncs*: seq[HirFunc] # Monomorphized generic methods
varTypeExprs*: Table[string, TypeExpr] # Track variable names -> type expr for generic method inference
proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter
@@ -124,6 +129,11 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.genericStructs = initTable[string, Decl]()
result.generatedStructInsts = initTable[string, bool]()
result.extraStructs = @[]
result.structInstMap = initTable[string, tuple[baseName: string, typeArgs: seq[Type]]]()
result.genericFuncs = initTable[string, Decl]()
result.generatedFuncInsts = initTable[string, bool]()
result.extraFuncs = @[]
result.varTypeExprs = initTable[string, TypeExpr]()
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
@@ -143,11 +153,15 @@ proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type])
if not ctx.generatedStructInsts.hasKey(mangledName):
let genericDecl = ctx.genericStructs[te.typeName]
var fields: seq[tuple[name: string, typ: Type]] = @[]
var concreteArgs: seq[Type] = @[]
for f in genericDecl.declStructFields:
let resolvedType = substituteType(ctx, f.ftype, subst)
fields.add((f.name, resolvedType))
for arg in te.typeArgs:
concreteArgs.add(substituteType(ctx, arg, subst))
ctx.extraStructs.add((mangledName, fields))
ctx.generatedStructInsts[mangledName] = true
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
return makeNamed(mangledName)
return ctx.resolveTypeExpr(te)
of tekPointer:
@@ -177,14 +191,18 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
let genericDecl = ctx.genericStructs[te.typeName]
var fields: seq[tuple[name: string, typ: Type]] = @[]
var subst = initTable[string, Type]()
var concreteArgs: seq[Type] = @[]
for j, tp in genericDecl.declStructTypeParams:
if j < te.typeArgs.len:
subst[tp] = ctx.resolveTypeExpr(te.typeArgs[j])
for arg in te.typeArgs:
concreteArgs.add(ctx.resolveTypeExpr(arg))
for f in genericDecl.declStructFields:
let resolvedType = substituteType(ctx, f.ftype, subst)
fields.add((f.name, resolvedType))
ctx.extraStructs.add((mangledName, fields))
ctx.generatedStructInsts[mangledName] = true
ctx.structInstMap[mangledName] = (te.typeName, concreteArgs)
return makeNamed(mangledName)
case te.typeName
of "void": return makeVoid()
@@ -234,7 +252,10 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
# Check global scope first
let sym = ctx.globalScope.lookup(expr.exprIdent)
if sym != nil and sym.typ != nil: return sym.typ
# Check current function parameters
# Check local variables and parameters tracked in varTypeExprs
if ctx.varTypeExprs.hasKey(expr.exprIdent):
return ctx.resolveTypeExpr(ctx.varTypeExprs[expr.exprIdent])
# Check current function parameters (fallback for untracked params)
if ctx.currentFuncDecl != nil:
var params: seq[Param] = @[]
case ctx.currentFuncDecl.kind
@@ -259,7 +280,17 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
return makePointer(pointeeType)
else: discard
return makeUnknown()
of ekSelf: return makeNamed("self")
of ekSelf:
# Look up self parameter type from current function
if ctx.currentFuncDecl != nil:
var params: seq[Param] = @[]
case ctx.currentFuncDecl.kind
of dkFunc: params = ctx.currentFuncDecl.declFuncParams
of dkExternFunc: params = ctx.currentFuncDecl.declExtFuncParams
else: discard
if params.len > 0 and params[0].name == "self" and params[0].ptype != nil:
return substituteType(ctx, params[0].ptype, ctx.typeSubst)
return makeNamed("self")
of ekBinary:
let left = ctx.resolveExprType(expr.exprBinaryLeft)
case expr.exprBinaryOp
@@ -347,6 +378,32 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
return makeVoid()
else: return makeUnknown()
proc extractGenericStructInfo(ctx: LowerCtx, te: TypeExpr): tuple[baseName: string, typeArgs: seq[TypeExpr]] =
if te == nil: return ("", @[])
var baseTe = te
if baseTe.kind == tekPointer:
baseTe = baseTe.pointerPointee
if baseTe.kind == tekNamed and baseTe.typeArgs.len > 0 and ctx.genericStructs.hasKey(baseTe.typeName):
return (baseTe.typeName, baseTe.typeArgs)
return ("", @[])
proc getReceiverTypeExpr(ctx: LowerCtx, expr: Expr): TypeExpr =
case expr.kind
of ekIdent:
if ctx.varTypeExprs.hasKey(expr.exprIdent):
return ctx.varTypeExprs[expr.exprIdent]
of ekField:
# For chained field access, try to resolve from the outer object
# This is limited but covers common cases
discard
of ekStructInit:
return TypeExpr(kind: tekNamed, loc: expr.loc, typeName: expr.exprStructInitName,
typeArgs: expr.exprStructInitTypeArgs)
else: discard
return nil
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
if expr == nil: return nil
let loc = expr.loc
@@ -384,18 +441,29 @@ 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
let receiverExpr = expr.exprCallCallee.exprFieldObj
# Try to find the method in methodTable
for typeName, methods in ctx.methodTable:
for minfo in methods:
if minfo.name == methodName:
# Found the method - desugar to Type_method(receiver, args)
let mangledName = typeName & "_" & methodName
var calleeName = typeName & "_" & methodName
# Check if this is a generic method on a generic struct instance
let recvTypeExpr = ctx.getReceiverTypeExpr(receiverExpr)
let (baseName, typeArgs) = ctx.extractGenericStructInfo(recvTypeExpr)
if baseName != "" and baseName == typeName and minfo.decl.declFuncTypeParams.len > 0:
calleeName = ctx.generateMethodInstance(calleeName, typeArgs)
var args: seq[HirNode] = @[]
args.add(ctx.lowerExpr(expr.exprCallCallee.exprFieldObj))
let loweredReceiver = ctx.lowerExpr(receiverExpr)
let receiverType = ctx.resolveExprType(receiverExpr)
# Auto-address if method expects pointer but receiver is value
if minfo.params.len > 0 and minfo.params[0].kind == tkPointer and receiverType.kind != tkPointer:
args.add(hirUnary(tkAmp, loweredReceiver, makePointer(receiverType), loc))
else:
args.add(loweredReceiver)
for arg in expr.exprCallArgs:
args.add(ctx.lowerExpr(arg))
return hirCall(mangledName, args, typ, loc)
return hirCall(calleeName, args, typ, loc)
# Not a method call - treat as field access + call (function pointer)
let callee = ctx.lowerExpr(expr.exprCallCallee)
@@ -632,6 +700,16 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc)
let varNode = hirVar(stmt.stmtLetName, makePointer(allocaType), loc)
let store = hirStore(varNode, initHir, loc)
# Track type expr for generic method inference
if stmt.stmtLetType != nil:
ctx.varTypeExprs[stmt.stmtLetName] = stmt.stmtLetType
elif stmt.stmtLetInit.kind == ekStructInit and stmt.stmtLetInit.exprStructInitTypeArgs.len > 0:
ctx.varTypeExprs[stmt.stmtLetName] = TypeExpr(
kind: tekNamed,
loc: stmt.stmtLetInit.loc,
typeName: stmt.stmtLetInit.exprStructInitName,
typeArgs: stmt.stmtLetInit.exprStructInitTypeArgs
)
var stmts = ctx.pendingStmts
ctx.pendingStmts = @[]
stmts.add(alloca)
@@ -798,36 +876,25 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
for p in funcParams:
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:
pType = ctx.resolveTypeExpr(p.ptype)
of tekPointer:
let pointeeType = ctx.resolveTypeExpr(p.ptype.pointerPointee)
pType = makePointer(pointeeType)
else: discard
pType = substituteType(ctx, p.ptype, ctx.typeSubst)
params.add((p.name, pType))
if p.ptype != nil:
ctx.varTypeExprs[p.name] = p.ptype
var retType = makeVoid()
if funcReturnType != nil:
case funcReturnType.kind
of tekNamed:
# Check if this is a type parameter
if ctx.typeSubst.hasKey(funcReturnType.typeName):
retType = ctx.typeSubst[funcReturnType.typeName]
else:
retType = ctx.resolveTypeExpr(funcReturnType)
of tekPointer:
let pointeeType = ctx.resolveTypeExpr(funcReturnType.pointerPointee)
retType = makePointer(pointeeType)
else: discard
retType = substituteType(ctx, funcReturnType, ctx.typeSubst)
let oldFuncDecl = ctx.currentFuncDecl
let oldFuncRetType = ctx.currentFuncRetType
let oldVarTypeExprs = ctx.varTypeExprs
ctx.currentFuncRetType = retType
ctx.currentFuncDecl = decl
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
let body = if funcBody != nil: ctx.lowerBlock(funcBody) else: nil
ctx.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType
ctx.varTypeExprs = oldVarTypeExprs
result = HirFunc(name: funcName, params: params, retType: retType,
body: body, isPublic: decl.isPublic)
@@ -835,6 +902,43 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
# Restore old substitution
ctx.typeSubst = oldSubst
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string =
if not ctx.genericFuncs.hasKey(baseMethodName):
return baseMethodName
let genericDecl = ctx.genericFuncs[baseMethodName]
if genericDecl.declFuncTypeParams.len == 0:
return baseMethodName
var subst = initTable[string, Type]()
var typeSuffix = ""
for i, tp in genericDecl.declFuncTypeParams:
if i > 0: typeSuffix.add("_")
if i < typeArgs.len:
let argType = ctx.resolveTypeExpr(typeArgs[i])
subst[tp] = argType
typeSuffix.add(argType.toString)
else:
typeSuffix.add("unknown")
let mangledName = baseMethodName & "_" & typeSuffix
if not ctx.generatedFuncInsts.hasKey(mangledName):
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
)
let oldSubst = ctx.typeSubst
ctx.typeSubst = subst
ctx.extraFuncs.add(ctx.lowerFunc(specDecl))
ctx.typeSubst = oldSubst
ctx.generatedFuncInsts[mangledName] = true
return mangledName
proc lowerModule*(module: Module, sema: Sema): HirModule =
var ctx = initLowerCtx(module, sema)
var funcs: seq[HirFunc] = @[]
@@ -877,10 +981,9 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
# First pass: collect generic functions and generic structs
var genericFuncs = initTable[string, Decl]()
for decl in module.items:
if decl.kind == dkFunc and decl.declFuncTypeParams.len > 0:
genericFuncs[decl.declFuncName] = decl
ctx.genericFuncs[decl.declFuncName] = decl
if decl.kind == dkStruct and decl.declStructTypeParams.len > 0:
ctx.genericStructs[decl.declStructName] = decl
@@ -943,7 +1046,7 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
var generated = initTable[string, bool]()
for inst in instantiations:
let baseName = inst.name
if genericFuncs.hasKey(baseName):
if ctx.genericFuncs.hasKey(baseName):
var typeSuffix = ""
for i, targ in inst.typeArgs:
if i > 0: typeSuffix.add("_")
@@ -954,7 +1057,7 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
let mangledName = baseName & "_" & typeSuffix
if not generated.hasKey(mangledName):
# Generate specialized version
let genericDecl = genericFuncs[baseName]
let genericDecl = ctx.genericFuncs[baseName]
# Build type substitution table
var subst = initTable[string, Type]()
@@ -1062,4 +1165,8 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
for s in ctx.extraStructs:
structs.add(s)
# Add monomorphized generic methods
for f in ctx.extraFuncs:
funcs.add(f)
result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts)
+41 -1
View File
@@ -110,6 +110,11 @@ proc collectGlobals*(sema: var Sema) =
of dkFunc:
let sym = Symbol(kind: skFunc, name: decl.declFuncName, decl: decl,
isPublic: decl.isPublic)
# Temporarily add type parameters to type table for resolution
var addedTypeParams: seq[string] = @[]
for tp in decl.declFuncTypeParams:
sema.typeTable[tp] = makeTypeParam(tp)
addedTypeParams.add(tp)
# Build function type from params and return
var params: seq[Type] = @[]
for p in decl.declFuncParams:
@@ -118,6 +123,30 @@ proc collectGlobals*(sema: var Sema) =
sym.typ = makeFunc(params, retType)
if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declFuncName}'")
# Auto-register func Type_Method(self: Type, ...) as a method
if decl.declFuncParams.len > 0 and decl.declFuncParams[0].name == "self":
var typeName = ""
for i in countdown(decl.declFuncName.len - 1, 1):
if decl.declFuncName[i] == '_':
let prefix = decl.declFuncName[0..<i]
let typeSym = sema.globalScope.lookup(prefix)
if typeSym != nil and typeSym.kind == skType and typeSym.decl != nil and typeSym.decl.kind == dkStruct:
typeName = prefix
break
if typeName != "":
let methodName = decl.declFuncName[typeName.len + 1 .. ^1]
if not sema.methodTable.hasKey(typeName):
sema.methodTable[typeName] = @[]
var minfo = MethodInfo(
name: methodName,
decl: decl,
params: params,
retType: retType
)
sema.methodTable[typeName].add(minfo)
# Clean up type parameters
for tp in addedTypeParams:
sema.typeTable.del(tp)
of dkExternFunc:
let sym = Symbol(kind: skFunc, name: decl.declExtFuncName, decl: decl,
isPublic: decl.isPublic)
@@ -283,6 +312,9 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
return makeUnknown()
return sym.typ
of ekSelf:
let sym = scope.lookup("self")
if sym != nil and sym.typ != nil:
return sym.typ
return makeNamed("self")
of ekPath:
let fullName = expr.exprPath.join("::")
@@ -607,7 +639,7 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
of skLet:
let initType = sema.checkExpr(stmt.stmtLetInit, scope)
let declaredType = if stmt.stmtLetType != nil: sema.resolveType(stmt.stmtLetType) else: initType
if stmt.stmtLetType != nil and not initType.isAssignableTo(declaredType):
if stmt.stmtLetType != nil and not initType.isAssignableTo(declaredType) and not (initType.kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}):
sema.emitError(stmt.loc, &"cannot assign {initType.toString} to {declaredType.toString}")
let sym = Symbol(kind: skVar, name: stmt.stmtLetName, typ: declaredType,
isMutable: stmt.stmtLetMut)
@@ -677,6 +709,11 @@ proc checkFunc(sema: var Sema, decl: Decl) =
if decl.declFuncBody == nil:
return
var funcScope = newScope(sema.globalScope)
# Add type parameters to type table for resolution
var addedTypeParams: seq[string] = @[]
for tp in decl.declFuncTypeParams:
sema.typeTable[tp] = makeTypeParam(tp)
addedTypeParams.add(tp)
# Add parameters
for p in decl.declFuncParams:
let pType = sema.resolveType(p.ptype)
@@ -685,6 +722,9 @@ proc checkFunc(sema: var Sema, decl: Decl) =
# Check body statements
for stmt in decl.declFuncBody.stmts:
discard sema.checkStmt(stmt, funcScope)
# Clean up type parameters
for tp in addedTypeParams:
sema.typeTable.del(tp)
# ---------------------------------------------------------------------------
# Second pass: check all function bodies
+1
View File
@@ -114,6 +114,7 @@ proc `!=`*(a, b: Type): bool = not (a == b)
# Assignment compatibility
proc isAssignableTo*(a, b: Type): bool =
if a.isUnknown or b.isUnknown: return true
if b.kind == tkTypeParam: return true
if a == b: return true
# float32 -> float64
if a.kind == tkFloat32 and b.kind == tkFloat64: return true