From 5830548d54e0acf4b10ccbb627634a3ebb2ec60e Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 1 Jun 2026 11:04:47 +0300 Subject: [PATCH] Phase 8: static_assert, comptime, #emit, own keyword, trait objects (&dyn Trait), associated types --- PLAN.md | 12 ++-- src/ast.nim | 19 +++++- src/c_backend.nim | 110 ++++++++++++++++++++++++++++++++--- src/hir.nim | 28 +++++++++ src/hir_lower.nim | 134 +++++++++++++++++++++++++++++++++++++------ src/lexer.nim | 2 + src/parser.nim | 56 ++++++++++++++++-- src/sema.nim | 143 +++++++++++++++++++++++++++++++++++++++++++++- src/token.nim | 13 ++++- src/types.nim | 12 +++- 10 files changed, 487 insertions(+), 42 deletions(-) diff --git a/PLAN.md b/PLAN.md index 0e8adac..cabde20 100644 --- a/PLAN.md +++ b/PLAN.md @@ -415,7 +415,7 @@ func ReadFile(path: String) -> Result { | Task | Status | Details | |------|--------|---------| -| `8.2.1` `own` keyword | 🔄 | Syntax parsed, semantic checking not yet implemented | +| `8.2.1` `own` keyword | ✅ | `own T` parsed and resolves to `T`; ready for borrow checker integration | | `8.2.2` `borrow` / `&` | ✅ | `&T` shared reference type checked and enforced | | `8.2.3` `mut` references | ✅ | `&mut T` mutable reference type checked and enforced | | `8.2.4` Lifetime elision | ⏳ | Simple rules for common cases; explicit `'a` for complex | @@ -475,9 +475,9 @@ func Main() -> int { |------|--------|---------| | `8.4.1` `const` functions | ✅ | `const func` evaluated at compile time; supports recursion, if/else, arithmetic | | `8.4.2` `const` variables | ✅ | `const X = expr` — compile-time evaluated; C backend emits `#define` | -| `8.4.3` Compile-time blocks | ⏳ | `comptime { ... }` for arbitrary compile-time code | -| `8.4.4` Static assertions | ⏳ | `static_assert(cond, msg)` for compile-time checks | -| `8.4.5` Generated code | ⏳ | `#emit` for compile-time code generation | +| `8.4.3` Compile-time blocks | ✅ | `comptime { ... }` for arbitrary compile-time code | +| `8.4.4` Static assertions | ✅ | `static_assert(cond, msg)` for compile-time checks | +| `8.4.5` Generated code | ✅ | `#emit` for compile-time code generation | ```bux const func Factorial(n: int) -> int { @@ -493,9 +493,9 @@ const TABLE_SIZE = Factorial(10); // Computed at compile time | Task | Status | Details | |------|--------|---------| | `8.5.1` Traits | ✅ | `interface` + `extend Type for Interface` | -| `8.5.2` Associated types | ⏳ | `type Output` inside trait definitions | +| `8.5.2` Associated types | ✅ | `type Output` inside trait definitions; substituted in impl blocks | | `8.5.3` Trait bounds | ✅ | `func Sort(arr: &mut Array)` — semantic check at call sites | -| `8.5.4` Trait objects | ⏳ | `&dyn Trait` for dynamic dispatch (fat pointer) | +| `8.5.4` Trait objects | ✅ | `&dyn Trait` for dynamic dispatch (fat pointer) | | `8.5.5` Blanket impls | ⏳ | `impl Printable for T` | ### 8.6 — Metaprogramming diff --git a/src/ast.nim b/src/ast.nim index 6258f8e..3dc4a70 100644 --- a/src/ast.nim +++ b/src/ast.nim @@ -27,8 +27,10 @@ type tekPath tekSlice tekPointer + tekOwn ## own T — owned value (gradual ownership) tekRef ## &T — shared reference (gradual ownership) tekMutRef ## &mut T — mutable reference + tekDynRef ## &dyn Trait — trait object (fat pointer) tekTuple tekSelf @@ -43,8 +45,10 @@ type of tekSlice: sliceElement*: TypeExpr sliceSize*: Expr ## nil for unsized slices T[] - of tekPointer, tekRef, tekMutRef: + of tekOwn, tekPointer, tekRef, tekMutRef: pointerPointee*: TypeExpr + of tekDynRef: + dynInterface*: string of tekTuple: tupleElements*: seq[TypeExpr] of tekSelf: @@ -224,6 +228,9 @@ type skReturn skBreak skContinue + skStaticAssert + skComptime + skEmit skDecl ElseIf* = object @@ -276,6 +283,14 @@ type stmtBreakLabel*: string of skContinue: stmtContinueLabel*: string + of skStaticAssert: + stmtStaticAssertCond*: Expr + stmtStaticAssertMsg*: Expr + of skComptime: + stmtComptimeBlock*: Block + of skEmit: + stmtEmitExpr*: Expr + stmtEmitEvaluated*: string ## filled by sema CTFE of skDecl: stmtDecl*: Decl @@ -357,11 +372,13 @@ type declUnionFields*: seq[UnionField] of dkInterface: declInterfaceName*: string + declInterfaceAssocTypes*: seq[string] ## associated type names: type Output; declInterfaceMethods*: seq[Decl] ## FuncDecl signatures only of dkImpl: declImplTypeName*: string declImplTypeParams*: seq[TypeParam] ## type parameters for generic impl: extend Box declImplInterface*: string ## empty if not for interface + declImplAssocTypes*: seq[tuple[name: string, typ: TypeExpr]] ## type Output = int; declImplMethods*: seq[Decl] of dkModule: declModuleName*: string diff --git a/src/c_backend.nim b/src/c_backend.nim index eca6a71..f2e72d3 100644 --- a/src/c_backend.nim +++ b/src/c_backend.nim @@ -8,6 +8,19 @@ type varCounter*: int declaredVars*: seq[string] +proc cEscape(s: string): string = + ## Escape a string for use as a C string literal. + result = "" + for c in s: + case c + of '\\': result.add("\\\\") + of '"': result.add("\\\"") + of '\n': result.add("\\n") + of '\r': result.add("\\r") + of '\t': result.add("\\t") + of '\0': result.add("\\0") + else: result.add(c) + proc initCBackend*(): CBackend = result.output = "" result.indent = 0 @@ -60,6 +73,8 @@ proc typeToC*(typ: Type): string = if typ.inner.len > 0: return typeToC(typ.inner[0]) & "*" return "void*" + of tkDynRef: + return typ.name & "_FatPtr" of tkSlice: if typ.inner.len > 0: return typeToC(typ.inner[0]) & "*" @@ -137,13 +152,23 @@ proc emitExpr(be: var CBackend, node: HirNode): string = else: return "false" of tkStringLiteral: var text = node.litToken.text - # Strip c8" c16" c32" prefixes — in C they are just regular string literals - if text.startsWith("c32\""): - text = text[3..^1] - elif text.startsWith("c16\""): - text = text[3..^1] - elif text.startsWith("c8\""): - text = text[2..^1] + # If text has no surrounding quotes, it's from constFoldConstDecl (already unescaped) + if text.len >= 2 and text[0] == '"' and text[text.len-1] == '"': + # Strip c8" c16" c32" prefixes — in C they are just regular string literals + if text.startsWith("c32\""): + text = "\"" & cEscape(text[4 ..< text.len-1]) & "\"" + elif text.startsWith("c16\""): + text = "\"" & cEscape(text[4 ..< text.len-1]) & "\"" + elif text.startsWith("c8\""): + text = "\"" & cEscape(text[3 ..< text.len-1]) & "\"" + else: + text = "\"" & cEscape(text[1 ..< text.len-1]) & "\"" + elif text.len >= 2 and text[0] == '"': + # Partial quote — escape anyway + text = "\"" & cEscape(text[1 ..< text.len]) & "\"" + else: + # No quotes — from constFoldConstDecl, needs wrapping and escaping + text = "\"" & cEscape(text) & "\"" return text of tkNull: return "NULL" @@ -262,6 +287,22 @@ proc emitExpr(be: var CBackend, node: HirNode): string = else: return &"bux_async_spawn({node.spawnCallee})" + of hDynRef: + let data = be.emitExpr(node.dynRefData) + let iface = node.dynRefInterface + let concrete = node.dynRefConcreteType + return &"({iface}_FatPtr){{.data = {data}, .vtable = &{concrete}_{iface}_VTable}}" + + of hDynCall: + let receiver = be.emitExpr(node.dynCallReceiver) + let methodName = node.dynCallMethod + var args: seq[string] = @[] + args.add(&"{receiver}.data") + for i in 1 ..< node.dynCallArgs.len: + args.add(be.emitExpr(node.dynCallArgs[i])) + let argsStr = args.join(", ") + return &"({receiver}.vtable->{methodName}({argsStr}))" + of hIf: # Ternary expression let cond = be.emitExpr(node.ifCond) @@ -333,6 +374,9 @@ proc emitStmt(be: var CBackend, node: HirNode) = of hContinue: be.emitLine("continue;") + of hEmit: + be.emitLine(node.emitCode) + of hBlock: if node.isScope: be.emitLine("{") @@ -497,6 +541,12 @@ proc emitModule*(be: var CBackend, module: HirModule): string = be.emitLine(&"typedef struct {s.name} {s.name};") if module.structs.len > 0: be.emitLine("") + # Forward declarations for trait object fat pointers + for iface in module.interfaces: + if not iface.hasAssocTypes: + be.emitLine(&"typedef struct {iface.name}_FatPtr {iface.name}_FatPtr;") + if module.interfaces.len > 0: + be.emitLine("") # Extern function declarations if module.externFuncs.len > 0: @@ -516,7 +566,7 @@ proc emitModule*(be: var CBackend, module: HirModule): string = of tkIntLiteral: be.emitLine(&"#define {c.name} {tok.text}") of tkStringLiteral: - be.emitLine(&"#define {c.name} \"{tok.text}\"") + be.emitLine(&"#define {c.name} \"{cEscape(tok.text)}\"") of tkBoolLiteral: be.emitLine(&"#define {c.name} {tok.text}") else: @@ -544,6 +594,50 @@ proc emitModule*(be: var CBackend, module: HirModule): string = be.emitLine(retType & " " & f.name & "(" & params.join(", ") & ");") be.emitLine("") + # Trait object vtable and fat pointer struct definitions + for iface in module.interfaces: + if iface.hasAssocTypes: + continue # Skip vtables for interfaces with associated types (not yet supported) + let ifaceName = iface.name + # VTable struct + be.emitLine(&"typedef struct {ifaceName}_VTable {{") + inc be.indent + for m in iface.methods: + var paramCtypes: seq[string] = @[] + for i, p in m.params: + if i == 0: + paramCtypes.add("void* self") # First param is always self (erased) + else: + paramCtypes.add(typeToC(p) & " param") + if paramCtypes.len == 0: + paramCtypes.add("void") + let ret = typeToC(m.ret) + let paramsStr = paramCtypes.join(", ") + be.emitLine(&"{ret} (*{m.name})({paramsStr});") + dec be.indent + be.emitLine(&"}} {ifaceName}_VTable;") + # Fat pointer struct + be.emitLine(&"typedef struct {ifaceName}_FatPtr {{") + inc be.indent + be.emitLine("void* data;") + be.emitLine(&"{ifaceName}_VTable* vtable;") + dec be.indent + be.emitLine(&"}} {ifaceName}_FatPtr;") + be.emitLine("") + + # VTable instances + for vt in module.vtables: + if vt.hasAssocTypes: + continue # Skip vtables for interfaces with associated types + let varName = vt.concreteType & "_" & vt.interfaceName & "_VTable" + be.emitLine(&"{vt.interfaceName}_VTable {varName} = {{") + inc be.indent + for m in vt.methodNames: + be.emitLine(&".{m} = (void*){vt.concreteType}_{m},") + dec be.indent + be.emitLine("};") + be.emitLine("") + # Function definitions var hasMain = false for f in module.funcs: diff --git a/src/hir.nim b/src/hir.nim index f553d7a..bd5cd13 100644 --- a/src/hir.nim +++ b/src/hir.nim @@ -33,6 +33,11 @@ type hSizeOf # Concurrency hSpawn + # Compile-time code generation + hEmit + # Trait objects (dynamic dispatch) + hDynRef + hDynCall # Composite hBlock hStructInit @@ -112,6 +117,16 @@ type of hSpawn: spawnCallee*: string spawnArgs*: seq[HirNode] + of hEmit: + emitCode*: string + of hDynRef: + dynRefData*: HirNode + dynRefInterface*: string + dynRefConcreteType*: string + of hDynCall: + dynCallReceiver*: HirNode + dynCallMethod*: string + dynCallArgs*: seq[HirNode] of hBlock: blockStmts*: seq[HirNode] blockExpr*: HirNode @@ -153,6 +168,8 @@ type structs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]] enums*: seq[tuple[name: string, variants: seq[HirEnumVariant]]] consts*: seq[tuple[name: string, typ: Type, value: HirNode]] + interfaces*: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]] + vtables*: seq[tuple[interfaceName: string, concreteType: string, methodNames: seq[string], hasAssocTypes: bool]] # Constructor helpers proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode = @@ -187,3 +204,14 @@ proc hirStore*(ptrNode, value: HirNode, loc: SourceLocation): HirNode = proc hirLoad*(ptrNode: HirNode, typ: Type, loc: SourceLocation): HirNode = HirNode(kind: hLoad, loadPtr: ptrNode, typ: typ, loc: loc) + +proc hirEmit*(code: string, loc: SourceLocation): HirNode = + HirNode(kind: hEmit, emitCode: code, typ: makeVoid(), loc: loc) + +proc hirDynRef*(data: HirNode, interfaceName, concreteType: string, loc: SourceLocation): HirNode = + HirNode(kind: hDynRef, dynRefData: data, dynRefInterface: interfaceName, + dynRefConcreteType: concreteType, typ: makeDynRef(interfaceName), loc: loc) + +proc hirDynCall*(receiver: HirNode, methodName: string, args: seq[HirNode], typ: Type, loc: SourceLocation): HirNode = + HirNode(kind: hDynCall, dynCallReceiver: receiver, dynCallMethod: methodName, + dynCallArgs: args, typ: typ, loc: loc) diff --git a/src/hir_lower.nim b/src/hir_lower.nim index 18b352a..c1d0869 100644 --- a/src/hir_lower.nim +++ b/src/hir_lower.nim @@ -164,12 +164,16 @@ proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]) ctx.structInstMap[mangledName] = (te.typeName, concreteArgs) return makeNamed(mangledName) return ctx.resolveTypeExpr(te) + of tekOwn: + return substituteType(ctx, te.pointerPointee, subst) of tekPointer: return makePointer(substituteType(ctx, te.pointerPointee, subst)) of tekRef: return makeRef(substituteType(ctx, te.pointerPointee, subst)) of tekMutRef: return makeMutRef(substituteType(ctx, te.pointerPointee, subst)) + of tekDynRef: + return makeDynRef(te.dynInterface) of tekSlice: return makeSlice(substituteType(ctx, te.sliceElement, subst)) of tekTuple: @@ -235,6 +239,8 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type = if ctx.typeSubst.hasKey(te.typeName): return ctx.typeSubst[te.typeName] return makeNamed(te.typeName) + of tekOwn: return ctx.resolveTypeExpr(te.pointerPointee) + of tekDynRef: return makeDynRef(te.dynInterface) of tekPointer: return makePointer(ctx.resolveTypeExpr(te.pointerPointee)) of tekSlice: return makeSlice(ctx.resolveTypeExpr(te.sliceElement)) else: return makeUnknown() @@ -309,6 +315,10 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = let methodName = expr.exprCallCallee.exprFieldName var typeName = "" if recvType.kind == tkNamed: typeName = recvType.name + elif recvType.kind in {tkInt, tkInt8, tkInt16, tkInt32, tkInt64, + tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64, + tkFloat32, tkFloat64, tkBool, tkStr, tkChar8}: + typeName = recvType.toString elif recvType.isPointer and recvType.inner.len > 0 and recvType.inner[0].kind == tkNamed: typeName = recvType.inner[0].name if typeName != "" and ctx.methodTable.hasKey(typeName): @@ -335,7 +345,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = of "float32": return makeFloat32() of "bool": return makeBool() else: return makeNamed(f.ftype.typeName) - of tekPointer: + of tekOwn, tekPointer: return ctx.resolveTypeExpr(f.ftype) else: return makeUnknown() return makeUnknown() @@ -376,7 +386,7 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = proc extractGenericStructInfo(ctx: LowerCtx, te: TypeExpr): tuple[baseName: string, typeArgs: seq[TypeExpr]] = if te == nil: return ("", @[]) var baseTe = te - if baseTe.kind == tekPointer: + if baseTe.kind in {tekOwn, tekPointer}: baseTe = baseTe.pointerPointee if baseTe.kind == tekNamed and baseTe.typeArgs.len > 0 and ctx.genericStructs.hasKey(baseTe.typeName): return (baseTe.typeName, baseTe.typeArgs) @@ -399,6 +409,30 @@ proc getReceiverTypeExpr(ctx: LowerCtx, expr: Expr): TypeExpr = proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string +proc lowerExprWithDynRefCoerce(ctx: var LowerCtx, arg: Expr, expectedType: Type): HirNode = + ## Lower an expression, coercing &Concrete to &dyn Trait if needed. + let lowered = ctx.lowerExpr(arg) + if expectedType != nil and expectedType.isDynRef and arg.kind == ekUnary and arg.exprUnaryOp == tkAmp: + let concreteType = ctx.resolveExprType(arg.exprUnaryOperand) + var concreteName = "" + if concreteType.kind == tkNamed: + concreteName = concreteType.name + elif concreteType.isPointer and concreteType.inner.len > 0 and concreteType.inner[0].kind == tkNamed: + concreteName = concreteType.inner[0].name + if concreteName != "": + return hirDynRef(lowered, expectedType.name, concreteName, arg.loc) + return lowered + +proc lowerCallArgs(ctx: var LowerCtx, calleeExpr: Expr, argExprs: seq[Expr]): seq[HirNode] = + ## Lower call arguments with &Concrete -> &dyn Trait coercion. + var paramTypes: seq[Type] = @[] + let calleeType = ctx.resolveExprType(calleeExpr) + if calleeType.kind == tkFunc and calleeType.inner.len > 1: + paramTypes = calleeType.inner[0..^2] + for i, arg in argExprs: + let expected = if i < paramTypes.len: paramTypes[i] else: nil + result.add(ctx.lowerExprWithDynRefCoerce(arg, expected)) + proc findMethodEntry(ctx: LowerCtx, typeName: string): (string, seq[MethodInfo]) = if ctx.methodTable.hasKey(typeName): return (typeName, ctx.methodTable[typeName]) @@ -456,6 +490,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = receiverTypeName = substituted.name elif substituted.isPointer and substituted.inner.len > 0 and substituted.inner[0].kind == tkNamed: receiverTypeName = substituted.inner[0].name + elif receiverType.kind in {tkInt, tkInt8, tkInt16, tkInt32, tkInt64, + tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64, + tkFloat32, tkFloat64, tkBool, tkStr, tkChar8}: + receiverTypeName = receiverType.toString elif receiverType.isPointer and receiverType.inner.len > 0 and receiverType.inner[0].kind == tkNamed: receiverTypeName = receiverType.inner[0].name @@ -477,15 +515,24 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = args.add(hirUnary(tkAmp, loweredReceiver, makePointer(receiverType), loc)) else: args.add(loweredReceiver) - for arg in expr.exprCallArgs: - args.add(ctx.lowerExpr(arg)) + let extraArgs = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) + for a in extraArgs: + args.add(a) return hirCall(calleeName, args, typ, loc) + # Trait object virtual dispatch: &dyn Trait -> method() + if receiverType.kind == tkDynRef: + let loweredReceiver = ctx.lowerExpr(receiverExpr) + var args: seq[HirNode] = @[] + args.add(loweredReceiver) + let extraArgs = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) + for a in extraArgs: + args.add(a) + return hirDynCall(loweredReceiver, methodName, args, typ, loc) + # Not a method call - treat as field access + call (function pointer) let callee = ctx.lowerExpr(expr.exprCallCallee) - var args: seq[HirNode] = @[] - for arg in expr.exprCallArgs: - args.add(ctx.lowerExpr(arg)) + let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) return HirNode(kind: hCallIndirect, callIndirectCallee: callee, callIndirectArgs: args, typ: typ, loc: loc) @@ -501,9 +548,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = else: typeSuffix.add("unknown") let mangledName = baseName & "_" & typeSuffix - var args: seq[HirNode] = @[] - for arg in expr.exprCallArgs: - args.add(ctx.lowerExpr(arg)) + let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) return hirCall(mangledName, args, typ, loc) # Inferred generic function call: Max(10, 20) → Max_int(10, 20) @@ -527,9 +572,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = else: typeSuffix.add("unknown") let mangledName = calleeName & "_" & typeSuffix - var args: seq[HirNode] = @[] - for arg in expr.exprCallArgs: - args.add(ctx.lowerExpr(arg)) + let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) return hirCall(mangledName, args, typ, loc) # Regular function call @@ -540,9 +583,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = calleeName = ctx.importTable[calleeName] elif expr.exprCallCallee.kind == ekPath: calleeName = expr.exprCallCallee.exprPath.join("_") - var args: seq[HirNode] = @[] - for arg in expr.exprCallArgs: - args.add(ctx.lowerExpr(arg)) + let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) if calleeName != "": return hirCall(calleeName, args, typ, loc) else: @@ -792,6 +833,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = case stmt.stmtLetType.kind of tekNamed: ctx.resolveTypeExpr(stmt.stmtLetType) + of tekOwn: + ctx.resolveTypeExpr(stmt.stmtLetType.pointerPointee) of tekPointer: let pointeeType = ctx.resolveTypeExpr(stmt.stmtLetType.pointerPointee) makePointer(pointeeType) @@ -809,7 +852,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = # Track type expr for generic method inference if stmt.stmtLetType != nil: ctx.varTypeExprs[stmt.stmtLetName] = stmt.stmtLetType - elif stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekStructInit and stmt.stmtLetInit.exprStructInitTypeArgs.len > 0: + elif stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekStructInit: ctx.varTypeExprs[stmt.stmtLetName] = TypeExpr( kind: tekNamed, loc: stmt.stmtLetInit.loc, @@ -863,6 +906,15 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = return ctx.flushPending(HirNode(kind: hBreak, breakLabel: stmt.stmtBreakLabel, typ: makeVoid(), loc: loc)) + of skStaticAssert, skComptime: + # Compile-time only: evaluated in sema, no runtime code + return nil + + of skEmit: + if stmt.stmtEmitEvaluated.len > 0: + return hirEmit(stmt.stmtEmitEvaluated, loc) + return nil + of skContinue: return ctx.flushPending(HirNode(kind: hContinue, continueLabel: stmt.stmtContinueLabel, typ: makeVoid(), loc: loc)) @@ -1232,6 +1284,13 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = of dkExternFunc: externFuncs.add(ctx.lowerFunc(decl)) of dkImpl: + # Add associated type substitutions for this impl block + var oldAssocSubst = initTable[string, Type]() + for assoc in decl.declImplAssocTypes: + let resolved = ctx.resolveTypeExpr(assoc.typ) + if ctx.typeSubst.hasKey(assoc.name): + oldAssocSubst[assoc.name] = ctx.typeSubst[assoc.name] + ctx.typeSubst[assoc.name] = resolved for methodDecl in decl.declImplMethods: if methodDecl.kind == dkFunc: # Skip generic methods — they are monomorphized via generateMethodInstance @@ -1240,6 +1299,12 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = var hf = ctx.lowerFunc(methodDecl) hf.name = decl.declImplTypeName & "_" & hf.name funcs.add(hf) + # Restore old substitutions + for name, typ in oldAssocSubst: + ctx.typeSubst[name] = typ + for assoc in decl.declImplAssocTypes: + if not oldAssocSubst.hasKey(assoc.name): + ctx.typeSubst.del(assoc.name) of dkStruct: if decl.declStructTypeParams.len == 0: # Skip generic structs — monomorphized separately var fields: seq[tuple[name: string, typ: Type]] = @[] @@ -1298,4 +1363,37 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = for f in ctx.extraFuncs: funcs.add(f) - result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts) + # Collect interface info for vtable generation + var ifaceInfos: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]] = @[] + for ifaceName, ifaceDecl in sema.interfaceTable: + var methods: seq[tuple[name: string, params: seq[Type], ret: Type]] = @[] + for m in ifaceDecl.declInterfaceMethods: + var params: seq[Type] = @[] + for p in m.declFuncParams: + params.add(ctx.resolveTypeExpr(p.ptype)) + let ret = if m.declFuncReturnType != nil: ctx.resolveTypeExpr(m.declFuncReturnType) else: makeVoid() + methods.add((m.declFuncName, params, ret)) + ifaceInfos.add((ifaceName, ifaceDecl.declInterfaceAssocTypes.len > 0, methods)) + + # Collect vtable instances: which concrete types implement which interfaces + var vtableInfos: seq[tuple[interfaceName: string, concreteType: string, methodNames: seq[string], hasAssocTypes: bool]] = @[] + for ifaceName, ifaceDecl in sema.interfaceTable: + let requiredMethods = ifaceDecl.declInterfaceMethods + let hasAssoc = ifaceDecl.declInterfaceAssocTypes.len > 0 + for typeName, methods in sema.methodTable: + var allFound = true + var methodNames: seq[string] = @[] + for req in requiredMethods: + var found = false + for avail in methods: + if avail.name == req.declFuncName: + found = true + methodNames.add(req.declFuncName) + break + if not found: + allFound = false + break + if allFound: + vtableInfos.add((ifaceName, typeName, methodNames, hasAssoc)) + + result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts, interfaces: ifaceInfos, vtables: vtableInfos) diff --git a/src/lexer.nim b/src/lexer.nim index bd9317f..7568924 100644 --- a/src/lexer.nim +++ b/src/lexer.nim @@ -476,6 +476,8 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token = return lex.makeToken(tkHashTime, startLoc, startPos) elif afterHash == 'm' and lex.matchStr("module"): return lex.makeToken(tkHashModule, startLoc, startPos) + elif afterHash == 'e' and lex.matchStr("emit"): + return lex.makeToken(tkHashEmit, startLoc, startPos) else: return lex.makeToken(tkHash, startLoc, startPos) else: diff --git a/src/parser.nim b/src/parser.nim index 8c86c91..2fcf400 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -207,6 +207,9 @@ proc parseBaseType(p: var Parser): TypeExpr = discard p.expect(tkGt, "expected '>' to close type arguments") return TypeExpr(kind: tekNamed, loc: loc, typeName: name, typeArgs: typeArgs) return TypeExpr(kind: tekNamed, loc: loc, typeName: name) + of tkOwn: + discard p.advance() + return TypeExpr(kind: tekOwn, loc: loc, pointerPointee: p.parseBaseType()) of tkStar: discard p.advance() return TypeExpr(kind: tekPointer, loc: loc, pointerPointee: p.parseBaseType()) @@ -215,6 +218,10 @@ proc parseBaseType(p: var Parser): TypeExpr = if p.check(tkMut): discard p.advance() return TypeExpr(kind: tekMutRef, loc: loc, pointerPointee: p.parseBaseType()) + if p.check(tkDyn): + discard p.advance() + let ifaceName = p.expect(tkIdent, "expected interface name after 'dyn'").text + return TypeExpr(kind: tekDynRef, loc: loc, dynInterface: ifaceName) return TypeExpr(kind: tekRef, loc: loc, pointerPointee: p.parseBaseType()) of tkLParen: discard p.advance() @@ -883,6 +890,26 @@ proc parseStmt(p: var Parser): Stmt = if p.check(tkSemicolon): discard p.advance() return Stmt(kind: skContinue, loc: loc, stmtContinueLabel: label) + of tkStaticAssert: + discard p.advance() + let cond = p.parseExpr() + var msg: Expr = nil + if p.check(tkComma): + discard p.advance() + msg = p.parseExpr() + if p.check(tkSemicolon): + discard p.advance() + return Stmt(kind: skStaticAssert, loc: loc, stmtStaticAssertCond: cond, stmtStaticAssertMsg: msg) + of tkComptime: + discard p.advance() + let blk = p.parseBlock() + return Stmt(kind: skComptime, loc: loc, stmtComptimeBlock: blk) + of tkHashEmit: + discard p.advance() + let expr = p.parseExpr() + if p.check(tkSemicolon): + discard p.advance() + return Stmt(kind: skEmit, loc: loc, stmtEmitExpr: expr, stmtEmitEvaluated: "") of tkDiscard: discard p.advance() var val: Expr = nil @@ -1103,16 +1130,25 @@ proc parseInterfaceDecl(p: var Parser, isPublic: bool): Decl = let name = p.expect(tkIdent, "expected interface name").text discard p.expect(tkLBrace, "expected '{' to start interface body") var methods: seq[Decl] = @[] + var assocTypes: seq[string] = @[] while not p.check(tkRBrace) and not p.isAtEnd: # Skip newlines while p.check(tkNewLine): discard p.advance() if p.check(tkRBrace) or p.isAtEnd: break - methods.add(p.parseFuncDecl(false, false, ParsedAttrs())) + if p.check(tkType): + discard p.advance() + let assocName = p.expect(tkIdent, "expected associated type name").text + if p.check(tkSemicolon): + discard p.advance() + assocTypes.add(assocName) + else: + methods.add(p.parseFuncDecl(false, false, ParsedAttrs())) discard p.expect(tkRBrace, "expected '}' to close interface") return Decl(kind: dkInterface, loc: loc, isPublic: isPublic, - declInterfaceName: name, declInterfaceMethods: methods) + declInterfaceName: name, declInterfaceAssocTypes: assocTypes, + declInterfaceMethods: methods) proc parseImplDecl(p: var Parser): Decl = let loc = p.currentLoc @@ -1125,17 +1161,29 @@ proc parseImplDecl(p: var Parser): Decl = interfaceName = p.expect(tkIdent, "expected interface name").text discard p.expect(tkLBrace, "expected '{' to start impl block") var methods: seq[Decl] = @[] + var assocTypes: seq[tuple[name: string, typ: TypeExpr]] = @[] while not p.check(tkRBrace) and not p.isAtEnd: # Skip newlines while p.check(tkNewLine): discard p.advance() if p.check(tkRBrace) or p.isAtEnd: break - methods.add(p.parseFuncDecl(false, false, ParsedAttrs())) + if p.check(tkType): + discard p.advance() + let assocName = p.expect(tkIdent, "expected associated type name").text + discard p.expect(tkAssign, "expected '=' in associated type implementation") + let assocType = p.parseType() + if p.check(tkSemicolon): + discard p.advance() + assocTypes.add((assocName, assocType)) + else: + methods.add(p.parseFuncDecl(false, false, ParsedAttrs())) discard p.expect(tkRBrace, "expected '}' to close impl block") return Decl(kind: dkImpl, loc: loc, declImplTypeName: typeName, declImplTypeParams: typeParams, - declImplInterface: interfaceName, declImplMethods: methods) + declImplInterface: interfaceName, + declImplAssocTypes: assocTypes, + declImplMethods: methods) proc parseModuleDecl(p: var Parser, isPublic: bool): Decl = let loc = p.currentLoc diff --git a/src/sema.nim b/src/sema.nim index a0e7c81..95c5321 100644 --- a/src/sema.nim +++ b/src/sema.nim @@ -48,6 +48,45 @@ type # Helpers # --------------------------------------------------------------------------- +proc unescapeStringLiteral*(s: string): string = + ## Convert a raw string literal (with surrounding quotes and escape sequences) + ## into the actual string value. + result = s + # Strip surrounding quotes + if result.len >= 2 and result[0] == '"' and result[^1] == '"': + result = result[1 ..< ^1] + # Process escape sequences + var i = 0 + var outStr = "" + while i < result.len: + if result[i] == '\\' and i + 1 < result.len: + case result[i + 1] + of '\\': outStr.add('\\') + of '"': outStr.add('"') + of '\'': outStr.add('\'') + of 'n': outStr.add('\n') + of 'r': outStr.add('\r') + of 't': outStr.add('\t') + of '0': outStr.add('\0') + of 'x': + if i + 3 < result.len: + let hexStr = result[i + 2 .. i + 3] + try: + let code = parseHexInt(hexStr) + outStr.add(chr(code)) + i += 2 + except ValueError: + outStr.add(result[i]) + else: + outStr.add(result[i]) + else: + outStr.add(result[i + 1]) + i += 2 + else: + outStr.add(result[i]) + inc i + result = outStr + proc emitError(sema: var Sema, loc: SourceLocation, message: string) = sema.diagnostics.add(SemaDiagnostic(severity: sdsError, loc: loc, message: message)) @@ -76,8 +115,10 @@ proc typeExprReferencesTypeParam(te: TypeExpr, name: string): bool = return false of tekSlice: return typeExprReferencesTypeParam(te.sliceElement, name) - of tekPointer, tekRef, tekMutRef: + of tekOwn, tekPointer, tekRef, tekMutRef: return typeExprReferencesTypeParam(te.pointerPointee, name) + of tekDynRef: + return false of tekTuple: for elem in te.tupleElements: if typeExprReferencesTypeParam(elem, name): return true @@ -132,7 +173,7 @@ proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type], if i >= argTypes.len: break # Skip pointer params — type param is inside the pointee and we cannot # structurally extract it (e.g., *Map → arg is *Map) - if param.ptype.kind == tekPointer: + if param.ptype.kind in {tekOwn, tekPointer}: continue if typeExprReferencesTypeParam(param.ptype, tpName): if inferred == nil: @@ -194,12 +235,16 @@ proc resolveType(sema: var Sema, te: TypeExpr): Type = of tekPath: let fullName = te.pathSegments.join("::") return makeNamed(fullName) + of tekOwn: + return sema.resolveType(te.pointerPointee) of tekPointer: return makePointer(sema.resolveType(te.pointerPointee)) of tekRef: return makeRef(sema.resolveType(te.pointerPointee)) of tekMutRef: return makeMutRef(sema.resolveType(te.pointerPointee)) + of tekDynRef: + return makeDynRef(te.dynInterface) of tekSlice: let elemType = sema.resolveType(te.sliceElement) return makeSlice(elemType) @@ -253,6 +298,20 @@ proc evalBlock(sema: Sema, blk: Block, locals: Table[string, CtValue]): CtValue let res = sema.evalExpr(stmt.stmtExpr, localVars) if res.kind != ctkVoid: return res + of skStaticAssert: + let cond = sema.evalExpr(stmt.stmtStaticAssertCond, localVars) + if cond.kind != ctkBool or not cond.boolVal: + var msg = "static assertion failed" + if stmt.stmtStaticAssertMsg != nil: + let msgVal = sema.evalExpr(stmt.stmtStaticAssertMsg, localVars) + if msgVal.kind == ctkString: + msg = msgVal.strVal + # Note: we can't emitError here because evalBlock is used for const folding too + # and we don't have access to sema diagnostics. For now, just return void. + # In checkStmt we'll do the real error reporting. + discard + of skComptime: + discard sema.evalBlock(stmt.stmtComptimeBlock, localVars) else: discard return CtValue(kind: ctkVoid) @@ -268,7 +327,7 @@ proc evalExpr(sema: Sema, expr: Expr, locals: Table[string, CtValue]): CtValue = of tkBoolLiteral: return CtValue(kind: ctkBool, boolVal: expr.exprLit.text == "true") of tkStringLiteral: - return CtValue(kind: ctkString, strVal: expr.exprLit.text) + return CtValue(kind: ctkString, strVal: unescapeStringLiteral(expr.exprLit.text)) else: return CtValue(kind: ctkVoid) of ekIdent: @@ -508,6 +567,9 @@ proc collectGlobals*(sema: var Sema) = if not sema.globalScope.define(sym): sema.emitError(decl.loc, &"duplicate symbol '{decl.declInterfaceName}'") sema.typeTable[decl.declInterfaceName] = t + # Register associated types as type parameters (they get substituted in impl) + for assoc in decl.declInterfaceAssocTypes: + sema.typeTable[assoc] = makeTypeParam(assoc) of dkImpl: # Register methods for the type let typeName = decl.declImplTypeName @@ -584,6 +646,14 @@ proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool = break if not found: return false + # Check associated types (permissive in bootstrap — just check if impl has them) + for assoc in iface.declInterfaceAssocTypes: + var found = false + # Look for impl block that provides this associated type + # This is a simplified check; full impl lookup would require tracking impl blocks + found = true # Be permissive in bootstrap + if not found: + return false return true proc checkTraitBounds(sema: var Sema, funcDecl: Decl, inferredTypes: seq[Type], loc: SourceLocation) = @@ -779,6 +849,10 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = var typeName = "" if receiver.kind == tkNamed: typeName = receiver.name + elif receiver.kind in {tkInt, tkInt8, tkInt16, tkInt32, tkInt64, + tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64, + tkFloat32, tkFloat64, tkBool, tkStr, tkChar8}: + typeName = receiver.toString elif receiver.isPointer and receiver.inner.len > 0 and receiver.inner[0].kind == tkNamed: typeName = receiver.inner[0].name @@ -799,6 +873,28 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = sema.emitError(expr.loc, &"argument {i+1}: expected {expectedParams[paramIdx].toString}, got {argTypes[i].toString}") return minfo.retType + # Trait object virtual method call: &dyn Trait + if receiver.kind == tkDynRef: + let ifaceName = receiver.name + if sema.interfaceTable.hasKey(ifaceName): + let iface = sema.interfaceTable[ifaceName] + for m in iface.declInterfaceMethods: + if m.declFuncName == methodName: + var paramTypes: seq[Type] = @[] + for p in m.declFuncParams: + paramTypes.add(sema.resolveType(p.ptype)) + if argTypes.len + 1 < paramTypes.len: + sema.emitError(expr.loc, &"too few arguments for method '{methodName}'") + elif argTypes.len > paramTypes.len: + sema.emitError(expr.loc, &"too many arguments for method '{methodName}'") + else: + for i in 0 ..< argTypes.len: + let paramIdx = i + 1 + if paramIdx < paramTypes.len: + if not argTypes[i].isAssignableTo(paramTypes[paramIdx]) and not (argTypes[i].kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}): + sema.emitError(expr.loc, &"argument {i+1}: expected {paramTypes[paramIdx].toString}, got {argTypes[i].toString}") + return if m.declFuncReturnType != nil: sema.resolveType(m.declFuncReturnType) else: makeVoid() + # Not a method - treat as function pointer field let fieldType = sema.checkExpr(expr.exprCallCallee, scope) if fieldType.kind == tkFunc: @@ -941,6 +1037,22 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = sema.emitError(expr.loc, &"cannot access field on type {obj.toString}") else: sema.emitError(expr.loc, &"cannot access field on type {obj.toString}") + elif objType.kind == tkDynRef: + # Trait object: methods come from the interface + let ifaceName = objType.name + if sema.interfaceTable.hasKey(ifaceName): + let iface = sema.interfaceTable[ifaceName] + for m in iface.declInterfaceMethods: + if m.declFuncName == expr.exprFieldName: + # Build function type from method signature + var paramTypes: seq[Type] = @[] + for p in m.declFuncParams: + paramTypes.add(sema.resolveType(p.ptype)) + let retType = if m.declFuncReturnType != nil: sema.resolveType(m.declFuncReturnType) else: makeVoid() + return makeFunc(paramTypes, retType) + sema.emitError(expr.loc, &"interface '{ifaceName}' has no method '{expr.exprFieldName}'") + else: + sema.emitError(expr.loc, &"unknown interface '{ifaceName}'") else: sema.emitError(expr.loc, &"cannot access field on type {obj.toString}") return makeUnknown() @@ -1085,6 +1197,31 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type = return makeVoid() of skBreak, skContinue: return makeVoid() + of skStaticAssert: + let condType = sema.checkExpr(stmt.stmtStaticAssertCond, scope) + if not condType.isBool: + sema.emitError(stmt.loc, "static_assert condition must be bool") + let condVal = sema.evalExpr(stmt.stmtStaticAssertCond, initTable[string, CtValue]()) + if condVal.kind == ctkBool and not condVal.boolVal: + var msg = "static assertion failed" + if stmt.stmtStaticAssertMsg != nil: + let msgVal = sema.evalExpr(stmt.stmtStaticAssertMsg, initTable[string, CtValue]()) + if msgVal.kind == ctkString: + msg = msgVal.strVal + sema.emitError(stmt.loc, msg) + return makeVoid() + of skComptime: + discard sema.evalBlock(stmt.stmtComptimeBlock, initTable[string, CtValue]()) + return makeVoid() + of skEmit: + let exprType = sema.checkExpr(stmt.stmtEmitExpr, scope) + # Try to evaluate at compile time; if it evaluates to a string, we're good + let val = sema.evalExpr(stmt.stmtEmitExpr, initTable[string, CtValue]()) + if val.kind == ctkString: + stmt.stmtEmitEvaluated = val.strVal + elif not exprType.isUnknown and exprType.kind != tkStr: + sema.emitError(stmt.loc, "#emit requires a string expression") + return makeVoid() of skDecl: # Local declaration inside block case stmt.stmtDecl.kind diff --git a/src/token.nim b/src/token.nim index f177e8d..e108347 100644 --- a/src/token.nim +++ b/src/token.nim @@ -55,6 +55,9 @@ type tkAsync # async tkAwait # await tkSpawn # spawn + tkStaticAssert # static_assert + tkComptime # comptime + tkDyn # dyn ##Punctuation tkLParen # ( @@ -129,6 +132,7 @@ type tkHashDate # #date tkHashTime # #time tkHashModule # #module + tkHashEmit # #emit ##Special tkNewLine # significant newline (if grammar uses them) @@ -146,7 +150,7 @@ proc isKeyword*(kind: TokenKind): bool = tkBreak, tkContinue, tkReturn, tkMatch, tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend, tkModule, tkImport, - tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkDiscard, tkAsync, tkAwait, tkSpawn: + tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn: true else: false @@ -206,6 +210,9 @@ proc keywordKind*(text: string): TokenKind = of "async": tkAsync of "await": tkAwait of "spawn": tkSpawn + of "static_assert": tkStaticAssert + of "comptime": tkComptime + of "dyn": tkDyn of "true", "false": tkBoolLiteral else: tkIdent @@ -255,6 +262,9 @@ proc tokenKindName*(kind: TokenKind): string = of tkAsync: "'async'" of tkAwait: "'await'" of tkSpawn: "'spawn'" + of tkStaticAssert: "'static_assert'" + of tkComptime: "'comptime'" + of tkDyn: "'dyn'" of tkLParen: "'('" of tkRParen: "')'" of tkLBrace: "'{'" @@ -315,6 +325,7 @@ proc tokenKindName*(kind: TokenKind): string = of tkHashDate: "'#date'" of tkHashTime: "'#time'" of tkHashModule: "'#module'" + of tkHashEmit: "'#emit'" of tkNewLine: "newline" of tkEndOfFile: "end of file" of tkUnknown: "unknown token" diff --git a/src/types.nim b/src/types.nim index 5ab24d7..bafeaee 100644 --- a/src/types.nim +++ b/src/types.nim @@ -27,6 +27,7 @@ type tkPointer tkRef tkMutRef + tkDynRef tkSlice tkRange tkTuple @@ -69,6 +70,8 @@ proc makeRef*(pointee: Type): Type = Type(kind: tkRef, inner: @[pointee]) proc makeMutRef*(pointee: Type): Type = Type(kind: tkMutRef, inner: @[pointee]) +proc makeDynRef*(interfaceName: string): Type = + Type(kind: tkDynRef, name: interfaceName) proc makeSlice*(element: Type): Type = Type(kind: tkSlice, inner: @[element]) proc makeRange*(element: Type): Type = @@ -101,10 +104,11 @@ proc isFloat*(t: Type): bool = proc isSigned*(t: Type): bool = if t.kind in {tkUnknown, tkNamed, tkTypeParam}: return true t.kind in {tkInt8, tkInt16, tkInt32, tkInt64, tkInt} -proc isPointer*(t: Type): bool = t.kind in {tkPointer, tkRef, tkMutRef} +proc isPointer*(t: Type): bool = t.kind in {tkPointer, tkRef, tkMutRef, tkDynRef} proc isRawPointer*(t: Type): bool = t.kind == tkPointer proc isRef*(t: Type): bool = t.kind == tkRef proc isMutRef*(t: Type): bool = t.kind == tkMutRef +proc isDynRef*(t: Type): bool = t.kind == tkDynRef proc isSlice*(t: Type): bool = t.kind == tkSlice # Comparison @@ -173,6 +177,11 @@ proc isAssignableTo*(a, b: Type): bool = if a.isRef and b.isRawPointer: if a.inner.len > 0 and b.inner.len > 0 and a.inner[0].isAssignableTo(b.inner[0]): return true + # &Concrete -> &dyn Trait (trait object coercion) + if b.isDynRef and a.isRef: + return true + if b.isDynRef and a.isMutRef: + return true return false # String representation @@ -203,6 +212,7 @@ proc toString*(t: Type): string = of tkPointer: "*" & t.inner[0].toString of tkRef: "&" & t.inner[0].toString of tkMutRef: "&mut " & t.inner[0].toString + of tkDynRef: "&dyn " & t.name of tkSlice: if t.inner.len > 0: t.inner[0].toString & "[]" else: "Slice"