feat: multi-instance closures, richer stdlib, and Rust-style diagnostics

Introduce fat function pointers (BuxFn {code, env}) so capturing closures
are heap-allocated per value in both bootstrap and selfhost. Expand
Array/Map/Set/String/Test/Result APIs, add proper tuple codegen and
error snippets with multi-char underlines, golden diagnostic tests, and
LSP diagnostics via buxc check.
This commit is contained in:
2026-07-15 16:00:21 +03:00
parent 94e6806dda
commit 61ac06ab5f
48 changed files with 2789 additions and 362 deletions
+30 -6
View File
@@ -128,8 +128,31 @@ proc typeToC*(be: var CBackend, typ: Type): string =
of "float64": return "double"
of "bool": return "bool"
else: return typ.name
of tkTuple: return "void*" # TODO: proper tuple struct
of tkFunc: return "void*" # TODO: function pointer
of tkTuple:
if typ.inner.len == 0:
return "Tuple_Empty"
var parts: seq[string] = @[]
for e in typ.inner:
var p = typeToC(be, e)
p = p.replace("const char*", "cstr").replace("unsigned int", "uint")
p = p.replace(" ", "_").replace("*", "Ptr").replace("(", "").replace(")", "").replace(",", "_")
parts.add(p)
let tname = "Tuple_" & parts.join("_")
# Ensure typedef is collected alongside slices
var already = false
for d in be.sliceTypeDefs:
if d.name == tname:
already = true
break
if not already:
# Reuse sliceTypeDefs as a generic "extra typedef" bag: elem holds field list markup
be.sliceTypeDefs.add((name: tname, elem: "/*tuple*/"))
return tname
of tkFunc:
if typ.inner.len == 0: return "void (*)(void)"
let params = typ.inner[0..^2].mapIt(typeToC(be, it)).join(", ")
let ret = typeToC(be, typ.inner[^1])
return ret & " (*)(" & params & ")"
else:
when defined(release):
return "void*"
@@ -311,10 +334,11 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
return &"({base}).data[{idx}]"
of hTupleInit:
var elems: seq[string] = @[]
for e in node.tupleInitElements:
elems.add(be.emitExpr(e))
return &"{{{elems.join(\", \")}}}"
let typeName = typeToC(be, node.typ)
var fields: seq[string] = @[]
for i, e in node.tupleInitElements:
fields.add(&"._{i} = {be.emitExpr(e)}")
return &"(({typeName}){{{fields.join(\", \")}}})"
of hCast:
let operand = be.emitExpr(node.castOperand)
+202 -10
View File
@@ -1,5 +1,6 @@
import std/[os, strutils, terminal, strformat, osproc, sets]
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
import source_location
type
ColorMode* = enum
@@ -89,6 +90,203 @@ proc printInfo(msg: string, useColor: bool) =
else:
echo("info: " & msg)
# ---------------------------------------------------------------------------
# Rust-style diagnostics (snippet + optional help hint)
# ---------------------------------------------------------------------------
proc getSourceLine(path: string, lineNum: uint32): string =
## Read a single 1-based line from path. Empty if unavailable.
if path.len == 0 or lineNum == 0 or not fileExists(path):
return ""
try:
let content = readFile(path)
var n: uint32 = 1
for line in content.splitLines():
if n == lineNum:
return line
inc n
except CatchableError:
discard
return ""
proc extractQuotedName(msg: string): string =
## Pull the first 'name' from messages like: undeclared identifier 'foo'
let a = msg.find('\'')
if a < 0: return ""
let b = msg.find('\'', a + 1)
if b <= a + 1: return ""
return msg[a + 1 .. b - 1]
proc underlineLength(lineText: string, col: uint32, message: string): int =
## Multi-character underline under the token at `col` (1-based).
## Falls back to scanning a source token, or matching a quoted name in the message.
if lineText.len == 0:
return 1
let start = if col > 0: int(col) - 1 else: 0
if start < 0 or start >= lineText.len:
return 1
# Prefer highlighting the quoted identifier/token from the message when it
# appears on this line (e.g. undeclared identifier 'foo').
let quoted = extractQuotedName(message)
if quoted.len > 0:
let idx = lineText.find(quoted)
if idx >= 0:
# If caret is on/near that token, use its full length
if abs(idx - start) <= quoted.len:
return quoted.len
let c0 = lineText[start]
# String / char / backtick literals
if c0 == '"' or c0 == '\'' or c0 == '`':
let quote = c0
var i = start + 1
while i < lineText.len:
if lineText[i] == '\\' and i + 1 < lineText.len:
i += 2
continue
if lineText[i] == quote:
return i - start + 1
inc i
return max(1, lineText.len - start)
# Identifier or keyword
if c0.isAlphaAscii or c0 == '_':
var i = start
while i < lineText.len and (lineText[i].isAlphaNumeric or lineText[i] == '_'):
inc i
return max(1, i - start)
# Number literal
if c0.isDigit:
var i = start
while i < lineText.len and (lineText[i].isDigit or lineText[i] in {'.', 'x', 'X', 'b', 'B', 'o', 'O', 'a'..'f', 'A'..'F', '_'}):
inc i
# optional type suffix: 42i64, 1.0f
while i < lineText.len and lineText[i] in {'i', 'u', 'f', 'I', 'U', 'F', '0'..'9'}:
inc i
return max(1, i - start)
# Multi-char operators starting at caret
const multiOps = ["<<=", ">>=", "**", "++", "--", "==", "!=", "<=", ">=",
"&&", "||", "<<", ">>", "+=", "-=", "*=", "/=", "%=",
"&=", "|=", "^=", "=>", "..", "->"]
for op in multiOps:
if start + op.len <= lineText.len and lineText[start .. start + op.len - 1] == op:
return op.len
return 1
proc hintForMessage(msg: string): string =
## Actionable help text for common compiler errors.
let m = msg.toLowerAscii()
if "cannot assign" in m:
return "ensure the right-hand side type matches the left-hand side"
if "undeclared identifier" in m:
return "check the spelling, or import the symbol from the right module"
if "too few arguments" in m or "too many arguments" in m:
return "compare the call with the function's parameter list"
if "missing argument for parameter" in m:
return "provide the missing argument (positional or named)"
if "use of moved value" in m:
return "the value was moved; clone it or restructure ownership"
if "shared reference" in m or "checked function" in m:
return "use '&mut T' for mutation, or drop @[Checked] for unchecked code"
if "double mutable borrow" in m or "already mutably borrowed" in m:
return "only one active '&mut' borrow is allowed at a time"
if "expected expression" in m:
return "the previous statement may be incomplete (missing value or ';')"
if "expected type" in m:
return "write a type name such as 'int', 'String', or 'Array<int>'"
if "expected field name" in m:
return "after '.' use an identifier or a tuple index (.0, .1, ...)"
if "does not implement trait" in m:
return "add an 'extend Type for Trait { ... }' block, or pick another type"
if "duplicate symbol" in m:
return "rename one of the definitions or remove the duplicate"
if "unterminated" in m:
return "check for a missing closing quote, backtick, or comment delimiter"
return ""
proc printDiagnostic*(severity: string, message: string, loc: SourceLocation,
useColor: bool, fallbackFile: string = "") =
## Print a Rust-style diagnostic:
## error: message
## --> file:line:col
## |
## 42 | source line
## | ^
## = help: hint
let isError = severity == "error"
if useColor:
stdout.setForegroundColor(if isError: fgRed else: fgYellow)
stdout.write(severity & ": ")
stdout.resetAttributes()
stdout.writeLine(message)
else:
let stream = if isError: stderr else: stdout
stream.writeLine(severity & ": " & message)
let file = if loc.file.len > 0: loc.file else: fallbackFile
if loc.line > 0:
let locStr = if file.len > 0:
&"{file}:{loc.line}:{loc.column}"
else:
&"{loc.line}:{loc.column}"
if useColor:
stdout.setForegroundColor(fgCyan)
stdout.write(" --> ")
stdout.resetAttributes()
stdout.writeLine(locStr)
else:
stderr.writeLine(" --> " & locStr)
let lineText = getSourceLine(file, loc.line)
if lineText.len > 0:
let gutter = $loc.line
let pad = " ".repeat(max(gutter.len, 3))
# If message names a quoted token, prefer caret at that token's start
var col = loc.column
let quoted = extractQuotedName(message)
if quoted.len > 0:
let idx = lineText.find(quoted)
if idx >= 0:
col = uint32(idx + 1)
let ulen = underlineLength(lineText, col, message)
stdout.writeLine(pad & " |")
stdout.writeLine(" " & gutter & " | " & lineText)
# Multi-char underline under the token (1-based column)
var caretPad = ""
if col > 0:
caretPad = " ".repeat(int(col) - 1)
let marks = "^".repeat(max(1, ulen))
stdout.writeLine(pad & " | " & caretPad & marks)
let hint = hintForMessage(message)
if hint.len > 0:
if useColor:
stdout.setForegroundColor(fgGreen)
stdout.write(" = help: ")
stdout.resetAttributes()
stdout.writeLine(hint)
else:
stdout.writeLine(" = help: " & hint)
proc printLexerDiags(diags: seq[LexerDiagnostic], useColor: bool, fallbackFile = "") =
for d in diags:
let sev = if d.severity == ldsError: "error" else: "warning"
printDiagnostic(sev, d.message, d.loc, useColor, fallbackFile)
proc printParserDiags(diags: seq[ParserDiagnostic], useColor: bool, fallbackFile = "") =
for d in diags:
let sev = if d.severity == pdsError: "error" else: "warning"
printDiagnostic(sev, d.message, d.loc, useColor, fallbackFile)
proc printSemaDiags(diags: seq[SemaDiagnostic], useColor: bool, fallbackFile = "") =
for d in diags:
let sev = if d.severity == sdsError: "error" else: "warning"
printDiagnostic(sev, d.message, d.loc, useColor, fallbackFile)
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
@@ -304,14 +502,12 @@ proc prepareProject(root: string, useColor: bool, opts: GlobalOptions): (Project
let lexRes = tokenize(source, path)
if lexRes.hasErrors:
printError(&"lex errors in {path}", useColor)
for d in lexRes.diagnostics:
echo $d
printLexerDiags(lexRes.diagnostics, useColor, path)
return (pctx, 1)
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0:
printError(&"parse errors in {path}", useColor)
for d in parseRes.diagnostics:
echo &"error: {d.message} at {d.loc}"
printParserDiags(parseRes.diagnostics, useColor, path)
return (pctx, 1)
for decl in parseRes.module.items:
if decl.kind == dkModule:
@@ -345,9 +541,7 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
let semaRes = analyze(unifiedModule)
if semaRes.hasErrors:
printError("type errors in project", useColor)
for d in semaRes.diagnostics:
let sev = if d.severity == sdsError: "error" else: "warning"
echo &"{sev}: {d.message} at {d.loc}"
printSemaDiags(semaRes.diagnostics, useColor)
return 1
if not opts.quiet:
printInfo("check passed", useColor)
@@ -448,9 +642,7 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let (semaRes, semaCtx) = analyzeFull(unifiedModule)
if semaRes.hasErrors:
printError("type errors in project", useColor)
for d in semaRes.diagnostics:
let sev = if d.severity == sdsError: "error" else: "warning"
echo &"{sev}: {d.message} at {d.loc}"
printSemaDiags(semaRes.diagnostics, useColor)
return 1
let hirMod = lowerModule(unifiedModule, semaCtx)
+4
View File
@@ -189,6 +189,10 @@ type
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]]
## Named functions used as values → need __adapt_ wrappers for fat-func ABI
funcAdapters*: seq[tuple[name: string, typ: Type]]
## Extra func types seen in locals/closures that need BuxFn_* typedefs
seenFatTypes*: seq[Type]
# Constructor helpers
proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode =
+163 -40
View File
@@ -1,4 +1,4 @@
import std/[tables, sets, strutils]
import std/[tables, sets, strutils, strformat]
import ast, types, token, source_location, hir, sema, scope
type
@@ -25,6 +25,11 @@ type
closureDepth*: int
currentClosureExpr*: Expr
envInstanceName*: string
## Named functions that must be wrapped as fat func values (multi-instance ABI)
funcAdapters*: HashSet[string]
funcAdapterSigs*: Table[string, Type]
## All func types that need BuxFn_* typedefs (including locals)
seenFatTypes*: seq[Type]
proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter
@@ -159,6 +164,50 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.generatedFuncInsts = initTable[string, bool]()
result.extraFuncs = @[]
result.varTypeExprs = initTable[string, TypeExpr]()
result.funcAdapters = initHashSet[string]()
result.funcAdapterSigs = initTable[string, Type]()
result.seenFatTypes = @[]
proc sanitizeFatPart(s: string): string =
result = s.replace("const char*", "cstr").replace("unsigned int", "uint")
result = result.replace(" ", "_").replace("*", "Ptr").replace("(", "").replace(")", "").replace(",", "_").replace(".", "_")
proc typeNameForFat(typ: Type): string
proc hirFuncFatTypeName*(typ: Type): string
proc typeNameForFat(typ: Type): string =
## Lightweight C-ish name for fat-func mangling (mirrors lir typeToCStr subset).
if typ == nil: return "void"
case typ.kind
of tkVoid: return "void"
of tkBool, tkBool8, tkBool16, tkBool32: return "bool"
of tkStr: return "cstr"
of tkInt, tkInt8, tkInt16, tkInt32, tkInt64: return "int"
of tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64: return "uint"
of tkFloat32: return "float"
of tkFloat64: return "double"
of tkPointer, tkRef, tkMutRef:
if typ.inner.len > 0: return sanitizeFatPart(typeNameForFat(typ.inner[0]) & "Ptr")
return "voidPtr"
of tkNamed:
case typ.name
of "String", "str": return "cstr"
else: return sanitizeFatPart(typ.name)
of tkFunc:
return hirFuncFatTypeName(typ)
else:
return "int"
proc hirFuncFatTypeName*(typ: Type): string =
if typ == nil or typ.kind != tkFunc: return "BuxFn_void"
let ret = if typ.inner.len > 0: typeNameForFat(typ.inner[^1]) else: "void"
var parts: seq[string] = @[sanitizeFatPart(ret)]
if typ.inner.len > 1:
for p in typ.inner[0 ..^ 2]:
parts.add(sanitizeFatPart(typeNameForFat(p)))
else:
parts.add("void")
return "BuxFn_" & parts.join("_")
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
@@ -291,7 +340,14 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
of tekOwn: return ctx.resolveTypeExpr(te.pointerPointee)
of tekDynRef: return makeDynRef(te.dynInterface)
of tekPointer: return makePointer(ctx.resolveTypeExpr(te.pointerPointee))
of tekRef: return makeRef(ctx.resolveTypeExpr(te.pointerPointee))
of tekMutRef: return makeMutRef(ctx.resolveTypeExpr(te.pointerPointee))
of tekSlice: return makeSlice(ctx.resolveTypeExpr(te.sliceElement))
of tekTuple:
var elems: seq[Type] = @[]
for e in te.tupleElements:
elems.add(ctx.resolveTypeExpr(e))
return makeTuple(elems)
of tekFunc:
var params: seq[Type] = @[]
for p in te.funcParams:
@@ -523,6 +579,18 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
return makeVoid()
of ekBorrow:
return ctx.resolveExprType(expr.exprBorrowOperand)
of ekClosure:
var params: seq[Type] = @[]
for p in expr.exprClosureParams:
if p.ptype != nil:
params.add(ctx.resolveTypeExpr(p.ptype))
else:
params.add(makeUnknown())
let ret = if expr.exprClosureReturnType != nil:
ctx.resolveTypeExpr(expr.exprClosureReturnType)
else:
makeVoid()
return makeFunc(params, ret)
else: return makeUnknown()
proc extractGenericStructInfo(ctx: LowerCtx, te: TypeExpr): tuple[baseName: string, typeArgs: seq[TypeExpr]] =
@@ -742,9 +810,26 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
let capType = if idx < ctx.currentClosureExpr.captureTypeKinds.len: Type(kind: TypeKind(ctx.currentClosureExpr.captureTypeKinds[idx])) else: makeInt()
let base = hirVar(ctx.envInstanceName, makeNamed(""), loc)
return HirNode(kind: hFieldAccess, fieldAccessName: name, fieldAccessBase: base, typ: capType, loc: loc)
var resolvedName = name
if ctx.importTable.hasKey(name):
return hirVar(ctx.importTable[name], typ, loc)
return hirVar(name, typ, loc)
resolvedName = ctx.importTable[name]
# Named function used as a value → fat function pointer via adapter
if typ != nil and typ.kind == tkFunc:
let sym = ctx.globalScope.lookup(name)
let sym2 = if sym == nil: ctx.globalScope.lookup(resolvedName) else: sym
if sym2 != nil and sym2.kind == skFunc:
let adaptName = "__adapt_" & resolvedName
ctx.funcAdapters.incl(resolvedName)
ctx.funcAdapterSigs[resolvedName] = typ
let fatName = hirFuncFatTypeName(typ)
let nullEnv = HirNode(kind: hCast,
castOperand: hirLit(Token(kind: tkIntLiteral, text: "0", loc: loc), makeInt(), loc),
castType: makePointer(makeVoid()), typ: makePointer(makeVoid()), loc: loc)
return HirNode(kind: hStructInit, structInitName: fatName, structInitFields: @[
(name: "code", value: hirVar(adaptName, makePointer(makeVoid()), loc)),
(name: "env", value: nullEnv)
], typ: typ, loc: loc)
return hirVar(resolvedName, typ, loc)
of ekPath:
# Handle enum variants: Color::Red → Color_Red
@@ -756,6 +841,32 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
return hirSelf(typ, loc)
of ekUnary:
# &NamedFunc used as func value → fat adapter (not a raw C function pointer)
if expr.exprUnaryOp == tkAmp and expr.exprUnaryOperand != nil and
expr.exprUnaryOperand.kind == ekIdent:
let fname = expr.exprUnaryOperand.exprIdent
var resolved = fname
if ctx.importTable.hasKey(fname):
resolved = ctx.importTable[fname]
let sym = ctx.globalScope.lookup(resolved)
if sym != nil and sym.kind == skFunc:
# Prefer declared func type on the symbol; fall back to expression type
var ftyp = if sym.typ != nil and sym.typ.kind == tkFunc: sym.typ else: typ
if ftyp == nil or ftyp.kind != tkFunc:
ftyp = ctx.resolveExprType(expr.exprUnaryOperand)
if ftyp != nil and ftyp.kind == tkFunc:
let adaptName = "__adapt_" & resolved
ctx.funcAdapters.incl(resolved)
ctx.funcAdapterSigs[resolved] = ftyp
ctx.seenFatTypes.add(ftyp)
let fatName = hirFuncFatTypeName(ftyp)
let nullEnv = HirNode(kind: hCast,
castOperand: hirLit(Token(kind: tkIntLiteral, text: "0", loc: loc), makeInt(), loc),
castType: makePointer(makeVoid()), typ: makePointer(makeVoid()), loc: loc)
return HirNode(kind: hStructInit, structInitName: fatName, structInitFields: @[
(name: "code", value: hirVar(adaptName, makePointer(makeVoid()), loc)),
(name: "env", value: nullEnv)
], typ: ftyp, loc: loc)
let operand = ctx.lowerExpr(expr.exprUnaryOperand)
return hirUnary(expr.exprUnaryOp, operand, typ, loc)
@@ -883,6 +994,16 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
calleeName = expr.exprCallCallee.exprPath.join("_")
let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs)
if calleeName != "":
# Named global function → direct call
let sym = ctx.globalScope.lookup(calleeName)
if sym != nil and sym.kind == skFunc:
return hirCall(calleeName, args, typ, loc)
# Variable holding a fat function pointer → indirect call
let ct = ctx.resolveExprType(expr.exprCallCallee)
if ct != nil and ct.kind == tkFunc:
let callee = hirVar(calleeName, ct, loc)
return HirNode(kind: hCallIndirect, callIndirectCallee: callee,
callIndirectArgs: args, typ: typ, loc: loc)
return hirCall(calleeName, args, typ, loc)
else:
let callee = ctx.lowerExpr(expr.exprCallCallee)
@@ -1236,7 +1357,32 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekClosure:
let f = ctx.lowerClosureFunc(expr)
return hirUnary(tkAmp, hirVar(f.name, makeFunc(@[], makeVoid()), loc), typ, loc)
if typ != nil and typ.kind == tkFunc:
ctx.seenFatTypes.add(typ)
let fatName = hirFuncFatTypeName(typ)
if expr.captureCount > 0 and f.envStructName.len > 0:
# Heap-allocate a fresh env so each closure value is independent
let envTmp = "__envp_" & $ctx.varCounter
inc ctx.varCounter
let fatTmp = "__fat_" & $ctx.varCounter
inc ctx.varCounter
var code = ""
code.add(&"{f.envStructName}* {envTmp} = ({f.envStructName}*)bux_alloc(sizeof({f.envStructName}));\n")
for i in 0 ..< expr.captureCount:
let capName = expr.captureNames[i]
code.add(&"{envTmp}->{capName} = {capName};\n")
code.add(&"{fatName} {fatTmp} = {{ .code = {f.name}, .env = {envTmp} }};")
ctx.pendingStmts.add(HirNode(kind: hEmit, emitCode: code, typ: makeVoid(), loc: loc))
return hirVar(fatTmp, typ, loc)
else:
# Capture-less: fat pointer with NULL env
let nullEnv = HirNode(kind: hCast,
castOperand: hirLit(Token(kind: tkIntLiteral, text: "0", loc: loc), makeInt(), loc),
castType: makePointer(makeVoid()), typ: makePointer(makeVoid()), loc: loc)
return HirNode(kind: hStructInit, structInitName: fatName, structInitFields: @[
(name: "code", value: hirVar(f.name, makePointer(makeVoid()), loc)),
(name: "env", value: nullEnv)
], typ: typ, loc: loc)
else:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
@@ -1255,28 +1401,14 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if stmt.stmtLetInit != nil:
initHir = ctx.lowerExpr(stmt.stmtLetInit)
let allocaType = if stmt.stmtLetType != nil:
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)
of tekSlice:
let elemType = ctx.resolveTypeExpr(stmt.stmtLetType.sliceElement)
makeSlice(elemType)
of tekFunc:
var params: seq[Type] = @[]
for p in stmt.stmtLetType.funcParams:
params.add(ctx.resolveTypeExpr(p))
let ret = if stmt.stmtLetType.funcRet != nil: ctx.resolveTypeExpr(stmt.stmtLetType.funcRet) else: makeVoid()
makeFunc(params, ret)
else: makeUnknown()
# Full resolve covers named, pointer, slice, tuple, func, refs, etc.
ctx.resolveTypeExpr(stmt.stmtLetType)
elif stmt.stmtLetInit != nil:
ctx.resolveExprType(stmt.stmtLetInit)
else:
makeUnknown()
if allocaType != nil and allocaType.kind == tkFunc:
ctx.seenFatTypes.add(allocaType)
let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc)
let varNode = hirVar(stmt.stmtLetName, makePointer(allocaType), loc)
@@ -1296,22 +1428,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
if initHir != nil:
let store = hirStore(varNode, initHir, loc)
stmts.add(store)
# If init is a closure with captures, emit capture assignments
if stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekClosure and stmt.stmtLetInit.captureCount > 0:
let closureIdx = ctx.varCounter - 1
let envInst = "__closure_env_instance_" & $closureIdx
var capStmts: seq[HirNode] = @[]
for i in 0 ..< stmt.stmtLetInit.captureCount:
let capName = stmt.stmtLetInit.captureNames[i]
let capType = if i < stmt.stmtLetInit.captureTypeKinds.len: Type(kind: TypeKind(stmt.stmtLetInit.captureTypeKinds[i])) else: makeInt()
let base = hirVar(envInst, makeNamed(""), loc)
let field = HirNode(kind: hFieldAccess, fieldAccessName: capName, fieldAccessBase: base, typ: capType, loc: loc)
let val = hirVar(capName, capType, loc)
capStmts.add(hirAssign(field, val, loc))
# Prepend capture assignments before the let
var allStmts = capStmts
allStmts.add(stmts)
return hirBlock(allStmts, nil, makeVoid(), loc)
# Capture filling for closures is done at the ekClosure site (heap env).
return hirBlock(stmts, nil, makeVoid(), loc)
of skReturn:
@@ -1723,6 +1840,8 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
let name = "__closure_" & $ctx.varCounter
inc ctx.varCounter
var f = HirFunc(name: name, isPublic: false)
# Always take a leading env pointer (fat-func ABI); may be unused.
f.params.add((name: "__env", typ: makePointer(makeVoid())))
# Copy capture metadata
if expr.captureCount > 0:
f.captureNames = expr.captureNames
@@ -1730,7 +1849,7 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
f.captureTypes.add(Type(kind: TypeKind(tk)))
f.envStructName = "__closure_env_" & $(ctx.varCounter - 1)
f.envInstanceName = "__closure_env_instance_" & $(ctx.varCounter - 1)
# Params
# User params
for p in expr.exprClosureParams:
f.params.add((name: p.name, typ: if p.ptype != nil: ctx.resolveTypeExpr(p.ptype) else: makeUnknown()))
# Return type
@@ -1935,4 +2054,8 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
if allFound:
vtableInfos.add((ifaceName, typeName, methodNames, hasAssoc))
result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts, interfaces: ifaceInfos, vtables: vtableInfos)
var adapters: seq[tuple[name: string, typ: Type]] = @[]
for name in ctx.funcAdapters:
let t = if ctx.funcAdapterSigs.hasKey(name): ctx.funcAdapterSigs[name] else: makeFunc(@[makeInt()], makeInt())
adapters.add((name, t))
result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts, interfaces: ifaceInfos, vtables: vtableInfos, funcAdapters: adapters, seenFatTypes: ctx.seenFatTypes)
+2 -1
View File
@@ -70,7 +70,8 @@ proc matchStr(lex: var Lexer, s: string): bool =
return true
proc currentLocation(lex: Lexer): SourceLocation =
result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos))
result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos),
file: lex.sourceName)
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
+199 -17
View File
@@ -149,14 +149,22 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
be.emitLine(&"{v(instr.src)}({argsStr});")
of lirCallIndirect:
## Fat function pointer call: f.code(f.env, args...)
var argsStr = ""
for i, arg in instr.extra:
if i > 0: argsStr.add(", ")
argsStr.add(v(arg))
let callee = v(instr.src)
if instr.dst.kind != lvkVoid:
be.emitLine(&"{v(instr.dst)} = ({v(instr.src)})({argsStr});")
if argsStr.len > 0:
be.emitLine(&"{v(instr.dst)} = ({callee}.code)({callee}.env, {argsStr});")
else:
be.emitLine(&"{v(instr.dst)} = ({callee}.code)({callee}.env);")
else:
be.emitLine(&"({v(instr.src)})({argsStr});")
if argsStr.len > 0:
be.emitLine(&"({callee}.code)({callee}.env, {argsStr});")
else:
be.emitLine(&"({callee}.code)({callee}.env);")
# ── Return ──
of lirRet:
@@ -348,6 +356,21 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin
# ── Struct/Enum emission (from HIR module) ──
proc sanitizeCTypeNamePart(s: string): string =
result = s
result = result.replace("const char*", "cstr")
result = result.replace("unsigned int", "uint")
result = result.replace(" ", "_")
result = result.replace("*", "Ptr")
result = result.replace("(", "")
result = result.replace(")", "")
result = result.replace(",", "_")
result = result.replace(".", "_")
proc typeToCStr(typ: Type): string
proc funcFatTypeName(typ: Type): string
proc funcCodePtrType(typ: Type): string
proc typeToCStr(typ: Type): string =
## Duplicate from lir_lower for self-containedness
if typ == nil: return "int"
@@ -396,13 +419,39 @@ proc typeToCStr(typ: Type): string =
of "float64": return "double"
of "bool": return "bool"
else: return typ.name
of tkTuple:
if typ.inner.len == 0:
return "Tuple_Empty"
var parts: seq[string] = @[]
for e in typ.inner:
parts.add(sanitizeCTypeNamePart(typeToCStr(e)))
return "Tuple_" & parts.join("_")
of tkFunc:
if typ.inner.len == 0: return "void (*)(void)"
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ")
let ret = typeToCStr(typ.inner[^1])
return ret & " (*)(" & params & ")"
return funcFatTypeName(typ)
else: return "int"
proc funcFatTypeName(typ: Type): string =
if typ == nil or typ.kind != tkFunc:
return "BuxFn_void"
let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void"
var parts: seq[string] = @[sanitizeCTypeNamePart(ret)]
if typ.inner.len > 1:
for p in typ.inner[0 ..^ 2]:
parts.add(sanitizeCTypeNamePart(typeToCStr(p)))
else:
parts.add("void")
return "BuxFn_" & parts.join("_")
proc funcCodePtrType(typ: Type): string =
if typ == nil or typ.kind != tkFunc:
return "void (*)(void*)"
let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void"
var params: seq[string] = @["void* env"]
if typ.inner.len > 1:
for p in typ.inner[0 ..^ 2]:
params.add(typeToCStr(p))
return ret & " (*)(" & params.join(", ") & ")"
proc emitStructDef(be: var LirCBackend, name: string, fields: seq[tuple[name: string, typ: Type]]) =
be.emitLine(&"typedef struct {name} {{")
be.indent += 1
@@ -481,11 +530,36 @@ proc collectValueDeps(typ: Type): seq[string] =
return @[typ.name]
of tkSlice:
return @[typeToCStr(typ)]
of tkPointer, tkRef, tkMutRef, tkTuple, tkFunc:
of tkTuple:
var deps: seq[string] = @[]
for e in typ.inner:
for d in collectValueDeps(e):
if d notin deps:
deps.add(d)
if e != nil and e.kind == tkTuple:
let tn = typeToCStr(e)
if tn notin deps:
deps.add(tn)
return deps
of tkPointer, tkRef, tkMutRef, tkFunc:
return @[]
else:
return @[]
proc emitTupleDef(be: var LirCBackend, typ: Type) =
## typedef struct { T0 _0; T1 _1; ... } Tuple_...;
let name = typeToCStr(typ)
be.emitLine(&"typedef struct {name} {{")
be.indent += 1
if typ.inner.len == 0:
be.emitLine("char _pad;")
else:
for i, e in typ.inner:
be.emitLine(&"{typeToCStr(e)} _{i};")
be.indent -= 1
be.emitLine(&"}} {name};")
be.emitLine("")
proc emitSliceTypeDef(be: var LirCBackend, name: string, elem: string) =
be.emitLine(&"typedef struct {{ {elem}* data; size_t len; }} {name};")
@@ -663,6 +737,104 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
elif sliceMap.hasKey(name):
be.emitSliceTypeDef(name, sliceMap[name])
# Collect and emit tuple typedefs used in the module (and nested tuples first).
var tupleTypes: seq[Type] = @[]
var tupleNames: HashSet[string]
proc registerTuple(t: Type) =
if t == nil: return
case t.kind
of tkTuple:
for e in t.inner:
registerTuple(e)
let name = typeToCStr(t)
if not tupleNames.contains(name):
tupleNames.incl(name)
tupleTypes.add(t)
of tkPointer, tkRef, tkMutRef, tkSlice:
if t.inner.len > 0:
registerTuple(t.inner[0])
of tkFunc:
for e in t.inner:
registerTuple(e)
else:
discard
for f in module.funcs:
registerTuple(f.retType)
for p in f.params:
registerTuple(p.typ)
for ef in module.externFuncs:
registerTuple(ef.retType)
for p in ef.params:
registerTuple(p.typ)
for s in module.structs:
for f in s.fields:
registerTuple(f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
registerTuple(ft)
for nf in v.namedFields:
registerTuple(nf.typ)
if tupleTypes.len > 0:
be.emitLine("/* Tuple types */")
for tt in tupleTypes:
be.emitTupleDef(tt)
# Fat function-pointer typedefs (BuxFn_*) — before forward decls that use them
var fatTypes: seq[Type] = @[]
var fatNames: HashSet[string]
proc registerFat(t: Type) =
if t == nil: return
case t.kind
of tkFunc:
for e in t.inner: registerFat(e)
let n = funcFatTypeName(t)
if not fatNames.contains(n):
fatNames.incl(n)
fatTypes.add(t)
of tkPointer, tkRef, tkMutRef, tkSlice:
if t.inner.len > 0: registerFat(t.inner[0])
of tkTuple:
for e in t.inner: registerFat(e)
else: discard
for f in module.funcs:
registerFat(f.retType)
for p in f.params: registerFat(p.typ)
for ef in module.externFuncs:
registerFat(ef.retType)
for p in ef.params: registerFat(p.typ)
for a in module.funcAdapters:
registerFat(a.typ)
for t in module.seenFatTypes:
registerFat(t)
if fatTypes.len > 0:
be.emitLine("/* Fat function pointer types (code + env) */")
for ft in fatTypes:
let n = funcFatTypeName(ft)
let codeT = funcCodePtrType(ft)
be.emitLine(&"typedef struct {n} {{")
be.indent += 1
be.emitLine(cParamDecl(codeT, "code") & ";")
be.emitLine("void* env;")
be.indent -= 1
be.emitLine(&"}} {n};")
be.emitLine("")
# Env structs for closures with captures (heap-allocated per value)
for f in module.funcs:
if f.captureNames.len > 0 and f.envStructName != "":
be.emitLine(&"typedef struct {f.envStructName} {{")
be.indent += 1
for i in 0 ..< f.captureNames.len:
let capName = f.captureNames[i]
let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int"
be.emitLine(&"{capType} {capName};")
be.indent -= 1
be.emitLine(&"}} {f.envStructName};")
be.emitLine("")
# Forward function declarations
for f in module.funcs:
let rt = typeToCStr(f.retType)
@@ -707,18 +879,28 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
be.emitLine("};")
be.emitLine("")
# Emit env structs for closures with captures
for f in module.funcs:
if f.captureNames.len > 0 and f.envStructName != "":
be.emitLine(&"struct {f.envStructName} {{")
# Adapters for named functions used as fat-func values (after forward decls)
if module.funcAdapters.len > 0:
be.emitLine("/* Fat-func adapters for named functions */")
for a in module.funcAdapters:
let ret = if a.typ.inner.len > 0: typeToCStr(a.typ.inner[^1]) else: "void"
var params: seq[string] = @["void* env"]
var argNames: seq[string] = @[]
if a.typ.inner.len > 1:
for i, p in a.typ.inner[0 ..^ 2]:
let pn = "a" & $i
params.add(typeToCStr(p) & " " & pn)
argNames.add(pn)
let argsStr = argNames.join(", ")
be.emitLine(&"static {ret} __adapt_{a.name}({params.join(\", \")}) {{")
be.indent += 1
for i in 0 ..< f.captureNames.len:
let capName = f.captureNames[i]
let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int"
be.emitLine(&"{capType} {capName};")
be.emitLine("(void)env;")
if ret == "void":
be.emitLine(&"{a.name}({argsStr});")
else:
be.emitLine(&"return {a.name}({argsStr});")
be.indent -= 1
be.emitLine("};")
be.emitLine(&"static struct {f.envStructName} {f.envInstanceName};")
be.emitLine("}")
be.emitLine("")
# Emit all LIR functions
+64 -5
View File
@@ -57,6 +57,22 @@ proc cEscape(s: string): string =
of '\0': result.add("\\0")
else: result.add(c)
proc sanitizeCTypeNamePart(s: string): string =
## Make a C type string safe for use inside a typedef name.
result = s
result = result.replace("const char*", "cstr")
result = result.replace("unsigned int", "uint")
result = result.replace(" ", "_")
result = result.replace("*", "Ptr")
result = result.replace("(", "")
result = result.replace(")", "")
result = result.replace(",", "_")
result = result.replace(".", "_")
proc typeToCStr(typ: Type): string
proc funcFatTypeName*(typ: Type): string
proc funcCodePtrType*(typ: Type): string
proc typeToCStr(typ: Type): string =
## Convert a Bux Type to a C type string.
if typ == nil: return "int"
@@ -105,13 +121,44 @@ proc typeToCStr(typ: Type): string =
of "float64": return "double"
of "bool": return "bool"
else: return typ.name
of tkTuple:
## (T, U) → typedef struct { T _0; U _1; } Tuple_T_U;
if typ.inner.len == 0:
return "Tuple_Empty"
var parts: seq[string] = @[]
for e in typ.inner:
parts.add(sanitizeCTypeNamePart(typeToCStr(e)))
return "Tuple_" & parts.join("_")
of tkFunc:
if typ.inner.len == 0: return "void (*)(void)"
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ")
let ret = typeToCStr(typ.inner[^1])
return ret & " (*)(" & params & ")"
## Fat function pointer: { code(env, args...), env }
## Enables multi-instance closures with captures.
return funcFatTypeName(typ)
else: return "int"
proc funcFatTypeName*(typ: Type): string =
## BuxFn_<ret>_<params...> (sanitized)
if typ == nil or typ.kind != tkFunc:
return "BuxFn_void"
let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void"
var parts: seq[string] = @[sanitizeCTypeNamePart(ret)]
if typ.inner.len > 1:
for p in typ.inner[0 ..^ 2]:
parts.add(sanitizeCTypeNamePart(typeToCStr(p)))
else:
parts.add("void")
return "BuxFn_" & parts.join("_")
proc funcCodePtrType*(typ: Type): string =
## C type of the .code field: ret (*)(void* env, params...)
if typ == nil or typ.kind != tkFunc:
return "void (*)(void*)"
let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void"
var params: seq[string] = @["void* env"]
if typ.inner.len > 1:
for i, p in typ.inner[0 ..^ 2]:
params.add(typeToCStr(p))
return ret & " (*)(" & params.join(", ") & ")"
proc hirTypeToC(ctx: var LowerToLirCtx, node: HirNode): string =
if node == nil: return "int"
result = typeToCStr(node.typ)
@@ -498,7 +545,12 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
for e in node.tupleInitElements:
elems.add(lowerExpr(ctx, e))
let t = b.freshTemp()
b.emitRawC(&"/* tuple */ {t.strVal} = {{{elems.mapIt($it).join(\", \")}}};")
let typeName = typeToCStr(node.typ)
b.emitAlloca(t.strVal, typeName)
var fields: seq[string] = @[]
for i, e in elems:
fields.add(&"._{i} = {lirValToC(e)}")
b.emitRawC(&"{t.strVal} = ({typeName}){{{fields.join(\", \")}}};")
return t
# ── If expression (ternary) ──
@@ -780,6 +832,13 @@ proc lowerModuleToLir*(hirMod: HirModule): LirBuilder =
ctx.funcRetType = retCT
ctx.builder.beginFunc(f.name, params, retCT, f.isPublic)
# Closure thunks: materialize env from fat-func env pointer (by-value copy)
if f.captureNames.len > 0 and f.envStructName.len > 0 and f.envInstanceName.len > 0:
ctx.builder.emitRawC(&"struct {f.envStructName} {f.envInstanceName} = *((struct {f.envStructName}*)__env);")
elif f.params.len > 0 and f.params[0].name == "__env":
# Capture-less closure thunk still receives env
ctx.builder.emitRawC("(void)__env;")
if f.body != nil:
if f.body.kind == hBlock:
for stmt in f.body.blockStmts:
+5 -1
View File
@@ -660,11 +660,15 @@ proc parsePostfix(p: var Parser): Expr =
discard p.expect(tkRBracket, "expected ']' to close index")
left = Expr(kind: ekIndex, loc: loc, exprIndexObj: left, exprIndexIdx: idx, exprIndexBoundsCheck: false)
of tkDot:
# Field expression or .await
# Field expression, tuple index (.0, .1), or .await
discard p.advance()
if p.check(tkAwait):
discard p.advance()
left = Expr(kind: ekAwait, loc: loc, exprAwaitOperand: left)
elif p.check(tkIntLiteral):
# Tuple element access: t.0 → field "_0"
let idxText = p.advance().text
left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: "_" & idxText)
else:
let fieldName = p.expectIdentOrKeyword("expected field name after '.'").text
left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: fieldName)
+16 -1
View File
@@ -1249,6 +1249,20 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
# Auto-dereference pointer/reference types for field access
if objType.kind in {tkPointer, tkRef, tkMutRef} and objType.inner.len > 0:
objType = objType.inner[0]
if objType.kind == tkTuple:
# Tuple fields: .0 / .1 → stored as "_0" / "_1"
var idx = -1
let fname = expr.exprFieldName
if fname.len > 0 and fname[0] == '_':
try: idx = parseInt(fname[1..^1])
except ValueError: idx = -1
else:
try: idx = parseInt(fname)
except ValueError: idx = -1
if idx >= 0 and idx < objType.inner.len:
return objType.inner[idx]
sema.emitError(expr.loc, &"tuple has no element '{fname}' (tuple arity {objType.inner.len})")
return makeUnknown()
if objType.kind == tkNamed:
# Check if this is a _Data union field access
if objType.name.endsWith("_Data"):
@@ -1468,7 +1482,8 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
initType = sema.checkExpr(stmt.stmtLetInit, scope)
let declaredType = if stmt.stmtLetType != nil: sema.resolveType(stmt.stmtLetType) else: initType
if stmt.stmtLetInit != nil and 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}")
# Point at the initializer expression for a clearer caret
sema.emitError(stmt.stmtLetInit.loc, &"cannot assign {initType.toString} to {declaredType.toString}")
if stmt.stmtLetInit == nil and stmt.stmtLetType == nil:
sema.emitError(stmt.loc, "variable must have either type annotation or initializer")
let isOwnVar = stmt.stmtLetType != nil and stmt.stmtLetType.kind == tekOwn
+5 -1
View File
@@ -3,6 +3,10 @@ type
line*: uint32 ## 1-based
column*: uint32 ## 1-based (UTF-8 byte offset in line)
offset*: uint32 ## byte offset from start of file
file*: string ## source file path (empty if unknown)
proc `$`*(loc: SourceLocation): string =
$loc.line & ":" & $loc.column
if loc.file.len > 0:
loc.file & ":" & $loc.line & ":" & $loc.column
else:
$loc.line & ":" & $loc.column