boot: defer, str.bytes()

defer runs when its scope ends and before return, break and continue leave it,
newest first, and sees only the variables declared before it. Not allowed
inside a defer: defer, return, break, continue.

boot.sh: 14 passed, 0 failed, 8 skipped.
This commit is contained in:
Nexoniarz 2026-10-25 18:47:24 +01:00
commit 84693aa655
3 changed files with 96 additions and 9 deletions

View file

@ -10,15 +10,21 @@ struct Var {
ast.Type ty ast.Type ty
str slot str slot
bool readonly bool readonly
int order // how many variables were declared before this one (for defer)
} }
// A scope's variables, and the defers that run when it ends, newest first.
// A defer sees only the variables declared before it: `orders` says how many.
struct Scope { struct Scope {
[]Var vars []Var vars
[]ast.Node defers
[]int orders
} }
struct Loop { struct Loop {
str cont str cont
str brk str brk
int depth // the scope depth at the loop: break and continue unwind down to it
} }
struct FnSig { struct FnSig {
@ -83,6 +89,10 @@ struct Gen {
[]StructDef structs []StructDef structs
[]str done []str done
[]str mm // names of methods that change self, for the mutation analysis []str mm // names of methods that change self, for the mutation analysis
int nvars // variables declared so far in this function
bool deferOn // running a defer: variables declared after it are out of sight
int deferLimit
int inDefer
} }
// ---------- basics ---------- // ---------- basics ----------
@ -776,6 +786,9 @@ fn lookup(ptr[Gen] g, str name) -> int {
s := g.scopes.len - 1 - k s := g.scopes.len - 1 - k
for i in 0..g.scopes[s].vars.len { for i in 0..g.scopes[s].vars.len {
if g.scopes[s].vars[i].name == name { if g.scopes[s].vars[i].name == name {
if g.deferOn && g.scopes[s].vars[i].order >= g.deferLimit {
continue
}
return s * 100000 + i return s * 100000 + i
} }
} }
@ -824,7 +837,8 @@ fn addVar(ptr[Gen] g, ast.Node at, str name, ast.Type t, str init, bool readonly
} }
slot := alloca(g, llty(g, t)) slot := alloca(g, llty(g, t))
store(g, llty(g, t), init, slot) store(g, llty(g, t), init, slot)
g.scopes[g.scopes.len - 1].vars.push(Var(name: name, ty: t, slot: slot, readonly: readonly)) g.scopes[g.scopes.len - 1].vars.push(Var(name: name, ty: t, slot: slot, readonly: readonly, order: g.nvars))
g.nvars += 1
return slot return slot
} }
@ -836,6 +850,43 @@ fn popScope(ptr[Gen] g) {
g.scopes.pop() g.scopes.pop()
} }
// Run the defers of scope `idx`, newest first, as if written where the scope ends.
fn runDefers(ptr[Gen] g, int idx) {
sc := g.scopes[idx]
k := sc.defers.len
while k > 0 {
k -= 1
savedOn := g.deferOn
savedLimit := g.deferLimit
g.deferOn = true
g.deferLimit = sc.orders[k]
g.inDefer += 1
pushScope(g)
stmt(g, sc.defers[k])
popScope(g)
g.inDefer -= 1
g.deferOn = savedOn
g.deferLimit = savedLimit
}
}
// The defers of every scope from the innermost down to `depth`, for return, break and continue.
fn unwind(ptr[Gen] g, int depth) {
i := g.scopes.len
while i > depth {
i -= 1
runDefers(g, i)
}
}
// The end of a block: its defers run, unless the block has already left by return, break or continue.
fn endScope(ptr[Gen] g) {
if !g.term {
runDefers(g, g.scopes.len - 1)
}
g.scopes.pop()
}
// ---------- mutation analysis: does a function change a parameter? ---------- // ---------- mutation analysis: does a function change a parameter? ----------
fn rootVar(ast.Node e) -> str { fn rootVar(ast.Node e) -> str {
@ -992,6 +1043,9 @@ fn define(ptr[Gen] g, int si) {
g.allocas = empty g.allocas = empty
g.tmp = 0 g.tmp = 0
g.term = false g.term = false
g.nvars = 0
g.deferOn = false
g.inDefer = 0
[]Scope none []Scope none
g.scopes = none g.scopes = none
pushScope(g) pushScope(g)
@ -1015,6 +1069,7 @@ fn define(ptr[Gen] g, int si) {
if g.cur.ret.kind != "void" { if g.cur.ret.kind != "void" {
fail(g, f, "function '" + f.name + "' can reach its end without returning " + ast.show(g.cur.ret)) fail(g, f, "function '" + f.name + "' can reach its end without returning " + ast.show(g.cur.ret))
} }
runDefers(g, 0)
emit(g, "ret void") emit(g, "ret void")
} }
g.out.push("define internal " + llty(g, g.cur.ret) + " " + g.cur.ll + "(" + params.join(", ") + ") {\nentry:\n" + g.out.push("define internal " + llty(g, g.cur.ret) + " " + g.cur.ll + "(" + params.join(", ") + ") {\nentry:\n" +
@ -1034,7 +1089,7 @@ fn block(ptr[Gen] g, ast.Node b, bool scope) {
stmt(g, s) stmt(g, s)
} }
if scope { if scope {
popScope(g) endScope(g)
} }
} }
@ -1058,11 +1113,22 @@ fn stmt(ptr[Gen] g, ast.Node s) {
eachStmt(g, s) eachStmt(g, s)
} else if k == "return" { } else if k == "return" {
returnStmt(g, s) returnStmt(g, s)
} else if k == "defer" {
if g.inDefer > 0 {
fail(g, s, "defer inside defer is not allowed")
}
sc := g.scopes.len - 1
g.scopes[sc].defers.push(s.kids[0])
g.scopes[sc].orders.push(g.nvars)
} else if k == "break" || k == "continue" { } else if k == "break" || k == "continue" {
if g.inDefer > 0 {
fail(g, s, k + " can't be used inside defer")
}
if g.loops.len == 0 { if g.loops.len == 0 {
fail(g, s, k + " can only be used inside a loop") fail(g, s, k + " can only be used inside a loop")
} }
l := g.loops[g.loops.len - 1] l := g.loops[g.loops.len - 1]
unwind(g, l.depth)
if k == "break" { if k == "break" {
br(g, l.brk) br(g, l.brk)
} else { } else {
@ -1187,7 +1253,7 @@ fn whileStmt(ptr[Gen] g, ast.Node s) {
condBr(g, c, body, end) condBr(g, c, body, end)
} }
label(g, body) label(g, body)
g.loops.push(Loop(cont: cond, brk: end)) g.loops.push(Loop(cont: cond, brk: end, depth: g.scopes.len))
breaks := countBreaks(s.kids[1]) breaks := countBreaks(s.kids[1])
block(g, s.kids[1], true) block(g, s.kids[1], true)
g.loops.pop() g.loops.pop()
@ -1228,7 +1294,7 @@ fn forStmt(ptr[Gen] g, ast.Node s) {
c := op(g, "icmp slt i64 " + iv + ", " + to) c := op(g, "icmp slt i64 " + iv + ", " + to)
condBr(g, c, body, end) condBr(g, c, body, end)
label(g, body) label(g, body)
g.loops.push(Loop(cont: step, brk: end)) g.loops.push(Loop(cont: step, brk: end, depth: g.scopes.len))
block(g, s.kids[2], true) block(g, s.kids[2], true)
g.loops.pop() g.loops.pop()
br(g, step) br(g, step)
@ -1279,7 +1345,7 @@ fn eachStmt(ptr[Gen] g, ast.Node s) {
if copyElems { if copyElems {
elem = copyValue(g, elem, et) elem = copyValue(g, elem, et)
} }
g.loops.push(Loop(cont: step, brk: end)) g.loops.push(Loop(cont: step, brk: end, depth: g.scopes.len))
pushScope(g) pushScope(g)
addVar(g, s, s.name, et, elem, true) addVar(g, s, s.name, et, elem, true)
block(g, s.kids[1], true) block(g, s.kids[1], true)
@ -1295,10 +1361,14 @@ fn eachStmt(ptr[Gen] g, ast.Node s) {
} }
fn returnStmt(ptr[Gen] g, ast.Node s) { fn returnStmt(ptr[Gen] g, ast.Node s) {
if g.inDefer > 0 {
fail(g, s, "return can't be used inside defer")
}
if g.cur.ret.kind == "void" { if g.cur.ret.kind == "void" {
if s.kids.len > 0 { if s.kids.len > 0 {
fail(g, s.kids[0], "'" + g.cur.name + "' doesn't return a value (it has no '-> type')") fail(g, s.kids[0], "'" + g.cur.name + "' doesn't return a value (it has no '-> type')")
} }
unwind(g, 0)
emit(g, "ret void") emit(g, "ret void")
g.term = true g.term = true
return return
@ -1317,6 +1387,7 @@ fn returnStmt(ptr[Gen] g, ast.Node s) {
if !isLocal { if !isLocal {
out = own(g, v) out = own(g, v)
} }
unwind(g, 0)
emit(g, "ret " + llty(g, g.cur.ret) + " " + out) emit(g, "ret " + llty(g, g.cur.ret) + " " + out)
g.term = true g.term = true
} }
@ -2433,7 +2504,13 @@ fn strMethod(ptr[Gen] g, ast.Node m, str s) -> Val {
emit(g, "call void @finch_str_repeat(ptr " + out + ", ptr " + s + ", i64 " + k + ")") emit(g, "call void @finch_str_repeat(ptr " + out + ", ptr " + s + ", i64 " + k + ")")
return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true) return Val(v: load(g, "{ ptr, i64, i64 }", out), ty: T("str"), fresh: true)
} }
fail(g, m, "a str has no method '" + n + "' (it has: sub, find, contains, starts_with, ends_with, split, trim, upper, lower, replace, repeat)") if n == "bytes" {
need(g, m, 0)
ao := strOut(g)
emit(g, "call void @finch_bytes_from_str(ptr " + ao + ", ptr " + s + ")")
return Val(v: load(g, "{ ptr, i64, i64 }", ao), ty: ast.arrayOf(T("u8")), fresh: true)
}
fail(g, m, "a str has no method '" + n + "' (it has: sub, find, contains, starts_with, ends_with, split, trim, upper, lower, replace, repeat, bytes)")
return Val() return Val()
} }
@ -2476,6 +2553,7 @@ fn runtimeDecls() -> str {
"declare void @finch_str_split(ptr, ptr, ptr)", "declare void @finch_str_split(ptr, ptr, ptr)",
"declare void @finch_str_join(ptr, ptr, ptr)", "declare void @finch_str_join(ptr, ptr, ptr)",
"declare void @finch_str_repeat(ptr, ptr, i64)", "declare void @finch_str_repeat(ptr, ptr, i64)",
"declare void @finch_bytes_from_str(ptr, ptr)",
"declare void @finch_arr_reserve(ptr, i64, i64)", "declare void @finch_arr_reserve(ptr, i64, i64)",
"declare void @finch_arr_clone_raw(ptr, ptr, i64)", "declare void @finch_arr_clone_raw(ptr, ptr, i64)",
"declare void @finch_arr_make(ptr, i64, i64)", "declare void @finch_arr_make(ptr, i64, i64)",

View file

@ -283,7 +283,16 @@ fn parseStatement(ptr[Parser] p) -> ast.Node {
if k == "{" { if k == "{" {
return parseBlock(p) return parseBlock(p)
} }
if k == "fn" || k == "struct" || k == "import" || k == "defer" || k == "link" { if k == "defer" {
s := node(p, "defer")
next(p)
if at(p, "defer") {
fail(p, cur(p), "defer inside defer is not allowed")
}
s.kids.push(parseStatement(p))
return s
}
if k == "fn" || k == "struct" || k == "import" || k == "link" {
fail(p, cur(p), "'" + k + "' can't be used here in the self-hosted compiler") fail(p, cur(p), "'" + k + "' can't be used here in the self-hosted compiler")
} }

View file

@ -21,11 +21,11 @@ fi
pass=0; failed=0; skipped=0 pass=0; failed=0; skipped=0
for f in tests/run/*.fch; do for f in tests/run/*.fch; do
grep -q "^fn main" "$f" || continue grep -q "^fn main" "$f" || continue
# the bootstrap compiler doesn't do C imports, defer, maps or errors as values # the bootstrap compiler doesn't do C imports, maps or errors as values
# (checked in the test and in the modules it imports) # (checked in the test and in the modules it imports)
files="$f" files="$f"
for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done for m in $(sed -n 's/^import \([a-z_]*\)$/\1/p' "$f"); do files="$files tests/run/$m.fch"; done
if grep -qE '^import "|^link |defer | or |try |map\[|!\s*\{|for [a-z_]+, ' $files; then if grep -qE '^import "|^link | or |try |map\[|!\s*\{|for [a-z_]+, ' $files; then
skipped=$((skipped + 1)) skipped=$((skipped + 1))
continue continue
fi fi