feat: capture-less anonymous functions (closures)

Implement MVP closures — anonymous functions without captures.

Syntax:
  |a: int, b: int| -> int { return a + b; }

Changes:
- ast.bux + ast.nim: add ekClosure AST node
- parser.bux + parser.nim: parse |params| -> Ret { body }
- sema.bux + sema.nim: type-check closure params/body, return tyFunc
- hir_lower.bux + hir_lower.nim: generate __closure_N function + hAddrOf
- lir_c_backend.nim: fix function-pointer variable declaration (cParamDecl)
- C backend: closures compile to global functions with unique names

Test: _test_closure/src/Main.bux
- Closure as variable
- Closure passed to higher-order function
- Address of named function as function pointer

Both bootstrap and selfhost compilers build and pass the test.
This commit is contained in:
2026-06-09 20:24:10 +03:00
parent 34504d1647
commit c83f6d5994
16 changed files with 2290 additions and 8 deletions
+5
View File
@@ -132,6 +132,7 @@ type
ekBlock
ekMatch
ekStringInterp
ekClosure
MatchArm* = object
loc*: SourceLocation
@@ -228,6 +229,10 @@ type
of ekStringInterp:
exprInterpTexts*: seq[string]
exprInterpExprs*: seq[Expr]
of ekClosure:
exprClosureParams*: seq[Param]
exprClosureBody*: Block
exprClosureReturnType*: TypeExpr
# ---------------------------------------------------------------------------
# Statements
+30
View File
@@ -276,6 +276,7 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode
proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
if expr == nil: return makeUnknown()
@@ -1139,6 +1140,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
resultNode = hirCall("String_Concat", @[resultNode, lastTextNode], makeStr(), loc)
return resultNode
of ekClosure:
let f = ctx.lowerClosureFunc(expr)
return hirUnary(tkAmp, hirVar(f.name, makeFunc(@[], makeVoid()), loc), typ, loc)
else:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
@@ -1167,6 +1172,12 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
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()
elif stmt.stmtLetInit != nil:
ctx.resolveExprType(stmt.stmtLetInit)
@@ -1475,6 +1486,25 @@ proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs:
ctx.generatedFuncInsts[mangledName] = true
return mangledName
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
let loc = expr.loc
let name = "__closure_" & $ctx.varCounter
inc ctx.varCounter
var f = HirFunc(name: name, isPublic: false)
# 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
if expr.exprClosureReturnType != nil:
f.retType = ctx.resolveTypeExpr(expr.exprClosureReturnType)
else:
f.retType = makeVoid()
# Body
if expr.exprClosureBody != nil:
f.body = ctx.lowerBlock(expr.exprClosureBody)
ctx.extraFuncs.add(f)
return f
proc lowerModule*(module: Module, sema: Sema): HirModule =
var ctx = initLowerCtx(module, sema)
var funcs: seq[HirFunc] = @[]
+8 -8
View File
@@ -58,6 +58,13 @@ proc typeFromValue(be: var LirCBackend, v: LirValue): string =
proc setTempType(be: var LirCBackend, temp: string, cType: string) =
be.tempTypes[temp] = cType
proc cParamDecl(cType, name: string): string =
## Emit a C parameter declaration, handling function-pointer syntax.
if cType.contains("(*)"):
return cType.replace("(*)", "(*" & name & ")")
else:
return cType & " " & name
# ── Per-instruction emission ──
proc emitInstr(be: var LirCBackend, instr: LirInstr) =
@@ -183,7 +190,7 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
let inferred = be.tempTypes[instr.dst.strVal]
if inferred != "" and inferred != ct:
ct = inferred
be.emitLine(&"{ct} {v(instr.dst)};")
be.emitLine(cParamDecl(ct, v(instr.dst)) & ";")
# ── Pointers ──
of lirAddrOf:
@@ -241,13 +248,6 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
# ── Function emission ──
proc cParamDecl(cType, name: string): string =
## Emit a C parameter declaration, handling function-pointer syntax.
if cType.contains("(*)"):
return cType.replace("(*)", "(*" & name & ")")
else:
return cType & " " & name
proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, string], funcPtrTypes: Table[string, string]) =
var paramsStr = ""
for i, p in f.params:
+20
View File
@@ -587,6 +587,26 @@ proc parsePrimary(p: var Parser): Expr =
of tkNull:
discard p.advance()
return newLiteralExpr(Token(kind: tkNull, text: "null", loc: loc))
of tkPipe:
# Closure: |params| -> Ret { body }
discard p.advance() # |
var params: seq[Param] = @[]
while not p.check(tkPipe) and not p.isAtEnd:
let nameTok = p.expect(tkIdent, "expected parameter name in closure")
discard p.expect(tkColon, "expected ':' in closure parameter")
let ptype = p.parseType()
params.add(Param(name: nameTok.text, ptype: ptype))
if p.check(tkComma):
discard p.advance()
else:
break
discard p.expect(tkPipe, "expected '|' to close closure params")
var retType: TypeExpr = nil
if p.check(tkArrow):
discard p.advance() # ->
retType = p.parseType()
let body = p.parseBlock()
return Expr(kind: ekClosure, loc: loc, exprClosureParams: params, exprClosureBody: body, exprClosureReturnType: retType)
else:
p.emitError(loc, "expected expression")
discard p.advance()
+20
View File
@@ -44,6 +44,7 @@ type
checkedFunc*: bool ## true inside @[Checked] function
currentFuncIsAsync*: bool ## true inside async func
movedVars*: seq[string] ## variables moved in current checked function
currentRetType*: Type ## return type of the function being checked
# ---------------------------------------------------------------------------
# Helpers
@@ -1337,6 +1338,25 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
for e in expr.exprInterpExprs:
discard sema.checkExpr(e, scope)
return makeStr()
of ekClosure:
let savedRetType = sema.currentRetType
let childScope = Scope(parent: scope)
sema.currentRetType = if expr.exprClosureReturnType != nil: sema.resolveType(expr.exprClosureReturnType) else: makeUnknown()
# Register params
for p in expr.exprClosureParams:
let ptype = if p.ptype != nil: sema.resolveType(p.ptype) else: makeUnknown()
discard childScope.define(Symbol(kind: skVar, name: p.name, typ: ptype))
# Check body
if expr.exprClosureBody != nil:
for stmt in expr.exprClosureBody.stmts:
discard sema.checkStmt(stmt, childScope)
sema.currentRetType = savedRetType
# Build function type
var params: seq[Type] = @[]
for p in expr.exprClosureParams:
params.add(if p.ptype != nil: sema.resolveType(p.ptype) else: makeUnknown())
let retType = if expr.exprClosureReturnType != nil: sema.resolveType(expr.exprClosureReturnType) else: makeVoid()
return makeFunc(params, retType)
# ---------------------------------------------------------------------------
# Statement type checking