feat: defer statement support in both compilers

- Add  statement for function-level deferred execution
- Deferred expressions run in LIFO order on function exit (return or implicit)
- Bootstrap: desugar defers in hir_lower.nim (inject before return + end of func)
- Selfhost: emit defers in c_backend.bux via CEmitter defer stack
- Both: add tkDefer token, skDefer AST node, hDefer HIR node
- Parser, lexer, sema, token files updated in both bootstrap/ and src/
This commit is contained in:
2026-06-08 20:59:27 +03:00
parent cf92b63ba6
commit a67271b08c
15 changed files with 385 additions and 4 deletions
+3
View File
@@ -238,6 +238,7 @@ type
skStaticAssert
skComptime
skEmit
skDefer
skDecl
ElseIf* = object
@@ -298,6 +299,8 @@ type
of skEmit:
stmtEmitExpr*: Expr
stmtEmitEvaluated*: string ## filled by sema CTFE
of skDefer:
stmtDeferBody*: Expr
of skDecl:
stmtDecl*: Decl
+40
View File
@@ -8,6 +8,7 @@ type
varCounter*: int
declaredVars*: seq[string]
sliceTypeDefs*: seq[tuple[name: string, elem: string]] ## Generated Slice_<T> typedefs
deferStack*: seq[seq[string]] ## Function-level deferred statements (LIFO)
proc cEscape(s: string): string =
## Escape a string for use as a C string literal.
@@ -45,6 +46,28 @@ proc freshVar(be: var CBackend): string =
inc be.varCounter
result = &"__tmp_{be.varCounter}"
# ---------------------------------------------------------------------------
# Defer helpers
# ---------------------------------------------------------------------------
proc pushDeferScope(be: var CBackend) =
be.deferStack.add(@[])
proc popDeferScope(be: var CBackend) =
if be.deferStack.len > 0:
be.deferStack.setLen(be.deferStack.len - 1)
proc emitCurrentDefers(be: var CBackend) =
## Emit all deferred statements in current scope, LIFO order
if be.deferStack.len == 0: return
let idx = be.deferStack.len - 1
for i in countdown(be.deferStack[idx].len - 1, 0):
be.emitLine(be.deferStack[idx][i])
proc clearCurrentDefers(be: var CBackend) =
if be.deferStack.len > 0:
let idx = be.deferStack.len - 1
be.deferStack[idx].setLen(0)
# Type conversion: Bux Type → C type string
proc typeToC*(be: var CBackend, typ: Type): string =
if typ == nil: return "void"
@@ -358,7 +381,14 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
proc emitStmt(be: var CBackend, node: HirNode) =
if node == nil: return
case node.kind
of hDefer:
let expr = be.emitExpr(node.deferBody)
if be.deferStack.len > 0:
let idx = be.deferStack.len - 1
be.deferStack[idx].add(expr & ";")
of hReturn:
be.emitCurrentDefers()
if node.returnValue != nil:
let val = be.emitExpr(node.returnValue)
be.emitLine(&"return {val};")
@@ -394,9 +424,11 @@ proc emitStmt(be: var CBackend, node: HirNode) =
be.emitLine("}")
of hBreak:
be.emitCurrentDefers()
be.emitLine("break;")
of hContinue:
be.emitCurrentDefers()
be.emitLine("continue;")
of hEmit:
@@ -453,15 +485,23 @@ proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
let paramsStr = params.join(", ")
be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{")
inc be.indent
be.pushDeferScope()
if hfunc.body != nil:
if hfunc.body.kind == hBlock:
for stmt in hfunc.body.blockStmts:
be.emitStmt(stmt)
if hfunc.body.blockExpr != nil and hfunc.retType.kind != tkVoid:
be.emitCurrentDefers()
let val = be.emitExpr(hfunc.body.blockExpr)
be.emitLine(&"return {val};")
else:
be.emitCurrentDefers()
else:
be.emitStmt(hfunc.body)
be.emitCurrentDefers()
else:
be.emitCurrentDefers()
be.popDeferScope()
dec be.indent
be.emitLine("}")
be.emitLine("")
+6
View File
@@ -17,6 +17,7 @@ type
hBreak
hContinue
hReturn
hDefer
# Memory
hAlloca
hLoad
@@ -84,6 +85,8 @@ type
continueLabel*: string
of hReturn:
returnValue*: HirNode
of hDefer:
deferBody*: HirNode
of hAlloca:
allocaType*: Type
allocaName*: string
@@ -200,6 +203,9 @@ proc hirCall*(callee: string, args: seq[HirNode], typ: Type, loc: SourceLocation
proc hirReturn*(value: HirNode, loc: SourceLocation): HirNode =
HirNode(kind: hReturn, returnValue: value, typ: makeVoid(), loc: loc)
proc hirDefer*(body: HirNode, loc: SourceLocation): HirNode =
HirNode(kind: hDefer, deferBody: body, typ: makeVoid(), loc: loc)
proc hirBlock*(stmts: seq[HirNode], expr: HirNode, typ: Type, loc: SourceLocation, isScope: bool = false): HirNode =
HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr, typ: typ, loc: loc, isScope: isScope)
+31 -2
View File
@@ -11,6 +11,7 @@ type
varCounter*: int
tryCounter*: int
pendingStmts*: seq[HirNode]
deferStmts*: seq[HirNode]
typeSubst*: Table[string, Type] # Type parameter substitution for generics
importTable*: Table[string, string] # Local name → fully qualified name for imports
genericStructs*: Table[string, Decl] # Generic struct declarations
@@ -1005,7 +1006,13 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
of skReturn:
let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil
return ctx.flushPending(hirReturn(value, loc))
var stmts = ctx.pendingStmts
ctx.pendingStmts = @[]
# Add defers in reverse order (LIFO)
for i in countdown(ctx.deferStmts.len - 1, 0):
stmts.add(ctx.deferStmts[i])
stmts.add(hirReturn(value, loc))
return hirBlock(stmts, nil, makeVoid(), loc)
of skIf:
let cond = ctx.lowerExpr(stmt.stmtIfCond)
@@ -1120,6 +1127,11 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
return ctx.flushPending(HirNode(kind: hMatch, matchSubject: subject, matchArms: arms,
typ: makeVoid(), loc: loc))
of skDefer:
let body = ctx.lowerExpr(stmt.stmtDeferBody)
ctx.deferStmts.add(body)
return nil
of skDecl:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
typ: makeVoid(), loc: loc)
@@ -1188,7 +1200,24 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
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
var body = if funcBody != nil: ctx.lowerBlock(funcBody) else: nil
# Inject remaining defers at end of function (for implicit return)
if ctx.deferStmts.len > 0 and body != nil and body.kind == hBlock:
# Only add if last statement is not already a return (defers already injected there)
var hasReturn = false
if body.blockStmts.len > 0 and body.blockStmts[^1].kind == hReturn:
hasReturn = true
elif body.blockStmts.len > 0 and body.blockStmts[^1].kind == hBlock:
# Check nested block's last statement
let last = body.blockStmts[^1]
if last.blockStmts.len > 0 and last.blockStmts[^1].kind == hReturn:
hasReturn = true
if not hasReturn:
for i in countdown(ctx.deferStmts.len - 1, 0):
body.blockStmts.add(ctx.deferStmts[i])
ctx.deferStmts = @[]
ctx.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType
ctx.varTypeExprs = oldVarTypeExprs
+6
View File
@@ -959,6 +959,12 @@ proc parseStmt(p: var Parser): Stmt =
# No-op: emit literal 0 as expression statement
let zeroTok = Token(kind: tkIntLiteral, text: "0", loc: loc)
return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekLiteral, loc: loc, exprLit: zeroTok))
of tkDefer:
discard p.advance()
let expr = p.parseExpr()
if p.check(tkSemicolon):
discard p.advance()
return Stmt(kind: skDefer, loc: loc, stmtDeferBody: expr)
of tkFunc, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend, tkModule,
tkImport, tkConst, tkType, tkExtern, tkPub:
# Local declaration
+3
View File
@@ -1335,6 +1335,9 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
elif not exprType.isUnknown and exprType.kind != tkStr:
sema.emitError(stmt.loc, "#emit requires a string expression")
return makeVoid()
of skDefer:
discard sema.checkExpr(stmt.stmtDeferBody, scope)
return makeVoid()
of skDecl:
# Local declaration inside block
case stmt.stmtDecl.kind
+4 -1
View File
@@ -59,6 +59,7 @@ type
tkStaticAssert # static_assert
tkComptime # comptime
tkDyn # dyn
tkDefer # defer
tkLifetime # 'a (lifetime parameter)
##Punctuation
@@ -152,7 +153,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, tkBorrow, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn:
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper, tkOwn, tkMut, tkBorrow, tkDiscard, tkAsync, tkAwait, tkSpawn, tkStaticAssert, tkComptime, tkDyn, tkDefer:
true
else:
false
@@ -216,6 +217,7 @@ proc keywordKind*(text: string): TokenKind =
of "static_assert": tkStaticAssert
of "comptime": tkComptime
of "dyn": tkDyn
of "defer": tkDefer
of "true", "false": tkBoolLiteral
else: tkIdent
@@ -269,6 +271,7 @@ proc tokenKindName*(kind: TokenKind): string =
of tkStaticAssert: "'static_assert'"
of tkComptime: "'comptime'"
of tkDyn: "'dyn'"
of tkDefer: "'defer'"
of tkLifetime: "lifetime"
of tkLParen: "'('"
of tkRParen: "')'"
+192
View File
@@ -0,0 +1,192 @@
# Bux Language Roadmap — New Constructs
> **Updated:** 2026-06-08 | **Status:** In Progress
This document tracks planned language constructs beyond Phase 8 strategy.
---
## P0 — Critical (Unlocks Major Use Cases)
### 1. `defer` Statement
**Why:** No GC + no destructors = manual `Free` everywhere. `defer` is the pragmatic fix.
**Syntax:**
```bux
func ReadFile(path: String) -> String {
let fd: int = Open(path);
defer Close(fd); // runs on any exit from scope
defer PrintLine("done"); // LIFO order
let data: String = ReadAll(fd);
return data; // both defers run before return
}
```
**Implementation Steps:**
1. Add `tkDefer` token (or reuse `tkDefer` if exists)
2. Add `DeferStmt` AST node (`child: *Stmt`)
3. Parser: parse `defer <expr>;` or `defer { <block> }`
4. C backend: collect all `defer`s per block, emit cleanup code before every exit point (return, break, continue)
5. Handle LIFO ordering for multiple defers in same scope
**Complexity:** Low — localized to parser + C backend. No type system changes.
---
### 2. Closures / Anonymous Functions
**Why:** Callbacks, iterators, functional APIs. Currently only named functions exist.
**Syntax:**
```bux
let add: func(int, int) -> int = |a, b| { return a + b; };
let nums: Array<int> = Array_New<int>();
Array_Filter(nums, |x| { return x > 10; });
```
**Implementation Steps:**
1. New AST node: `ClosureExpr` with `params`, `body`, `captures`
2. Parser: parse `|param1, param2| -> Type { body }`
3. Type system: closure type as `func(Args) -> Ret` + implicit capture struct
4. C backend: generate struct with captured vars + function pointer
5. Lifetime: ensure captures outlive closure usage
**Complexity:** High — touches parser, sema, type system, C backend.
---
### 3. `for x in collection` Iterator Loops
**Why:** Currently only `for i in 0..10` works. No way to iterate arrays/channels/maps.
**Syntax:**
```bux
for item in arr {
PrintLine(item);
}
for msg in channel {
Process(msg);
}
```
**Implementation Steps:**
1. Parser: extend `for` to accept `for <ident> in <expr> { ... }`
2. Desugar to while loop using `Iter_HasNext` / `Iter_Next` or trait-based iterator
3. C backend: generate standard while loop with iterator state
**Complexity:** Medium — needs either trait system enhancement or hardcoded desugaring.
---
## P1 — High Impact
### 4. Operator Overloading
**Why:** Can't write `arr[i]`, `a + b`, `s1 == s2` for user types.
**Syntax:**
```bux
extend Array<T> {
func operator[](self: Array<T>, idx: uint) -> T { ... }
func operator+(self: Array<T>, other: Array<T>) -> Array<T> { ... }
}
```
**Implementation Steps:**
1. Parser: allow `operator[]`, `operator+`, etc. as function names
2. Sema: resolve operator calls to user-defined functions when available
3. C backend: emit regular function call
**Complexity:** Medium — mainly sema + parser changes.
---
### 5. Destructors / `Drop` Trait
**Why:** `own T` exists but nothing cleans up automatically. Complements `defer`.
**Syntax:**
```bux
extend Array<T> {
func Drop(self: own Array<T>) {
Array_Free(self);
}
}
```
**Implementation Steps:**
1. Define `Drop` interface in stdlib
2. C backend: emit `Drop(value)` before variable goes out of scope
3. Respect move semantics — don't drop moved values
**Complexity:** High — needs ownership tracking + move semantics.
---
### 6. String Interpolation
**Why:** `Fmt_Fmt1("hello {0}", name)` is verbose.
**Syntax:**
```bux
let name: String = "Bux";
let msg: String = "Hello, {name}!";
let num: int = 42;
let msg2: String = "Count: {num}";
```
**Implementation Steps:**
1. Lexer: detect `{` inside string literals, parse interpolation expressions
2. Parser: create string concatenation AST node
3. Desugar to `String_Concat` calls or `Fmt_FmtN`
**Complexity:** Low — lexer/parser changes only.
---
## P2 — Nice to Have
### 7. Native `switch` / `case`
**Why:** `match` is powerful but overkill for simple integer dispatch. Jump tables are faster.
**Syntax:**
```bux
switch statusCode {
case 200: PrintLine("OK");
case 404: PrintLine("Not Found");
case 500: PrintLine("Server Error");
default: PrintLine("Unknown");
}
```
**Implementation Steps:**
1. Parser: `switch expr { case literal: stmts ... default: stmts }`
2. C backend: emit `switch(expr) { case N: ... }`
**Complexity:** Low — straightforward C mapping.
---
### 8. Named / Default Parameters
**Why:** API ergonomics.
**Syntax:**
```bux
func HttpResponse(code: int = 200, contentType: String = "text/plain", body: String = "") -> Response { ... }
let r: Response = HttpResponse(body: "hello"); // code=200, contentType=default
```
**Implementation Steps:**
1. Parser: allow `param: Type = defaultExpr`
2. Sema: fill missing args at call sites
3. C backend: emit args in correct order with defaults
**Complexity:** Medium — sema changes for call resolution.
---
## Recommended Order
1. **`defer`** — Low complexity, huge impact, unlocks safe resource management immediately.
2. **String interpolation** — Low complexity, big ergonomics win.
3. **`switch`/`case`** — Low complexity, complements `match` for numeric dispatch.
4. **Named/default parameters** — Medium complexity, improves stdlib APIs.
5. **Operator overloading** — Medium complexity, transforms stdlib ergonomics.
6. **Closures** — High complexity, unlocks iterators and functional style.
7. **`for x in collection`** — Depends on closures or trait system.
8. **Destructors / Drop** — High complexity, needs ownership + move semantics.
+1
View File
@@ -157,6 +157,7 @@ const skReturn: int = 8;
const skBreak: int = 9;
const skContinue: int = 10;
const skDecl: int = 11;
const skDefer: int = 12;
struct ElseIf {
line: uint32;
+74 -1
View File
@@ -66,6 +66,15 @@ struct CEmitter {
sb: StringBuilder,
indent: int,
mod: *HirModule,
deferCount: int,
defer0: *HirNode,
defer1: *HirNode,
defer2: *HirNode,
defer3: *HirNode,
defer4: *HirNode,
defer5: *HirNode,
defer6: *HirNode,
defer7: *HirNode,
}
func CBE_Init() -> CEmitter {
@@ -73,6 +82,49 @@ func CBE_Init() -> CEmitter {
return CEmitter { sb: sb, indent: 0, mod: null as *HirModule };
}
func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) {
if cbe.deferCount == 0 { cbe.defer0 = node; }
if cbe.deferCount == 1 { cbe.defer1 = node; }
if cbe.deferCount == 2 { cbe.defer2 = node; }
if cbe.deferCount == 3 { cbe.defer3 = node; }
if cbe.deferCount == 4 { cbe.defer4 = node; }
if cbe.deferCount == 5 { cbe.defer5 = node; }
if cbe.deferCount == 6 { cbe.defer6 = node; }
if cbe.deferCount == 7 { cbe.defer7 = node; }
cbe.deferCount = cbe.deferCount + 1;
}
func CBE_EmitDefers(cbe: *CEmitter) -> int {
if cbe.deferCount == 0 { return 0; }
var i: int = cbe.deferCount - 1;
while i >= 0 {
StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0;
while sp < cbe.indent {
StringBuilder_Append(&cbe.sb, " ");
sp = sp + 1;
}
var dn: *HirNode = null as *HirNode;
if i == 0 { dn = cbe.defer0; }
if i == 1 { dn = cbe.defer1; }
if i == 2 { dn = cbe.defer2; }
if i == 3 { dn = cbe.defer3; }
if i == 4 { dn = cbe.defer4; }
if i == 5 { dn = cbe.defer5; }
if i == 6 { dn = cbe.defer6; }
if i == 7 { dn = cbe.defer7; }
CBE_EmitExpr(cbe, dn);
StringBuilder_Append(&cbe.sb, ";");
i = i - 1;
}
cbe.deferCount = 0;
return 1;
}
func CBE_ClearDefers(cbe: *CEmitter) {
cbe.deferCount = 0;
}
func CBE_Emit(cbe: *CEmitter, text: String) {
var i: int = 0;
while i < cbe.indent {
@@ -231,8 +283,23 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
return;
}
// Defer
if kind == hDefer {
CBE_PushDefer(cbe, node.child1);
return;
}
// Return
if kind == hReturn {
let hadDefers: int = CBE_EmitDefers(cbe);
if hadDefers != 0 {
StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0;
while sp < cbe.indent {
StringBuilder_Append(&cbe.sb, " ");
sp = sp + 1;
}
}
StringBuilder_Append(&cbe.sb, "return");
if node.child1 != null as *HirNode {
StringBuilder_Append(&cbe.sb, " ");
@@ -500,6 +567,11 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
if kind == hBlock {
var child: *HirNode = node.child1;
while child != null as *HirNode {
if child.kind == hDefer {
CBE_EmitExpr(cbe, child);
child = child.child3;
continue;
}
// Indent
var sp: int = 0;
while sp < cbe.indent {
@@ -971,6 +1043,7 @@ func CBackend_Generate(mod: *HirModule) -> String {
var hasReturn: bool = false;
cbe.indent = 1;
CBE_EmitExpr(cbe, body);
CBE_EmitDefers(cbe);
cbe.indent = 0;
// Check if body has a return statement
var stmt: *HirNode = body.child1;
@@ -982,7 +1055,7 @@ func CBackend_Generate(mod: *HirModule) -> String {
if String_Eq(mod.funcs[i].retTypeName, "int") && !hasReturn {
StringBuilder_Append(&cbe.sb, " return 0;\n");
}
StringBuilder_Append(&cbe.sb, "}\n\n");
StringBuilder_Append(&cbe.sb, "\n}\n\n");
i = i + 1;
}
+1
View File
@@ -33,6 +33,7 @@ const hTupleInit: int = 27;
const hMatch: int = 28;
const hSpawn: int = 29;
const hAwait: int = 30;
const hDefer: int = 31;
// ---------------------------------------------------------------------------
// HirArgList — linked list for call arguments beyond 2
+9
View File
@@ -482,6 +482,15 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
return n;
}
// Defer
if kind == skDefer {
n.kind = hDefer;
if stmt.child1 != null as *Expr {
n.child1 = Lcx_LowerExpr(ctx, stmt.child1);
}
return n;
}
return n;
}
+1
View File
@@ -301,6 +301,7 @@ func lexKeywordKind(text: String) -> int {
if String_Eq(text, "super") { return tkSuper; }
if String_Eq(text, "sizeof") { return tkSizeOf; }
if String_Eq(text, "discard") { return tkDiscard; }
if String_Eq(text, "defer") { return tkDefer; }
if String_Eq(text, "async") { return tkAsync; }
if String_Eq(text, "await") { return tkAwait; }
if String_Eq(text, "spawn") { return tkSpawn; }
+13
View File
@@ -704,6 +704,19 @@ func parserParseStmt(p: *Parser) -> *Stmt {
return s;
}
// defer expr
if kind == tkDefer {
discard parserAdvance(p);
let val: *Expr = parserParseExpr(p);
parserMatch(p, tkSemicolon);
let s: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt;
s.kind = skDefer;
s.line = line;
s.column = col;
s.child1 = val;
return s;
}
// if
if kind == tkIf {
discard parserAdvance(p);
+1
View File
@@ -138,6 +138,7 @@ const tkAsync: int = 102;
const tkAwait: int = 103;
const tkSpawn: int = 104;
const tkDiscard: int = 105;
const tkDefer: int = 106;
// ---------------------------------------------------------------------------
// Token struct