-g: debug info, in the compiler written in Finch

DWARF as LLVM metadata: a compile unit, a subprogram per function, a location on every call, a variable
for each local and parameter with types a debugger can open (structs with their fields, arrays and text
as ptr/len/cap, maps, enums with their variants, pointers, fixed arrays). Methods show self.

Use it with -O0, as the docs say: the optimizer inlines the program into main, which has no debug info.
boot.sh builds ten programs with -g -O0 and checks they still behave. 51 passed.
This commit is contained in:
Nexoniarz 2026-10-25 21:08:13 +01:00
commit 8c3b5b15b9
3 changed files with 341 additions and 10 deletions

View file

@ -16,6 +16,13 @@ struct Var {
// A scope's variables, and the defers that run when it ends, newest first. // 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. // A defer sees only the variables declared before it: `orders` says how many.
// What the driver asks for besides the program itself.
struct Options {
bool shared // finch build --shared
bool debug // -g
str cwd // the folder the compiler was started in (for the debug info)
}
// A generic struct or function, kept as written until something asks for a version of it. // A generic struct or function, kept as written until something asks for a version of it.
struct Template { struct Template {
str module str module
@ -138,6 +145,15 @@ struct Gen {
int nvars // variables declared so far in this function int nvars // variables declared so far in this function
cimport.CImports ci cimport.CImports ci
bool shared // building a library (finch build --shared): no main, its export fns are the way in bool shared // building a library (finch build --shared): no main, its export fns are the way in
bool debug // -g: debug info (DWARF, as LLVM metadata)
str cwd
[]str md // the metadata, numbered by position: !0 = md[0]
map[str]int mdcache // metadata made already: types by key, locations by line:column
[]int mdfiles // the DIFile of each source file
int dbgScope // the DISubprogram of the function being built, or -1 (helpers have none)
int dbgLine
int dbgCol
int dbgArg // the parameter number of the next variable, or 0
[]str cdone // C functions and variables already declared []str cdone // C functions and variables already declared
str ftarget // the call an enclosing or / try is waiting on (see nodeKey) str ftarget // the call an enclosing or / try is waiting on (see nodeKey)
str ffailed // set by that call: an i1, true if it failed str ffailed // set by that call: an i1, true if it failed
@ -155,6 +171,11 @@ fn fail(ptr[Gen] g, ast.Node n, str msg) {
} }
fn emit(ptr[Gen] g, str line) { fn emit(ptr[Gen] g, str line) {
// with -g every call in a function that has debug info has to say where it is
if g.dbgScope >= 0 && line.contains("call ") {
g.body.push(" " + line + ", !dbg !" + str(dbgLocation(g)))
return
}
g.body.push(" " + line) g.body.push(" " + line)
} }
@ -577,10 +598,12 @@ struct Saved {
[]str allocas []str allocas
int tmp int tmp
bool term bool term
int dbgScope
} }
fn beginHelper(ptr[Gen] g) -> Saved { fn beginHelper(ptr[Gen] g) -> Saved {
s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term) s := Saved(body: g.body, allocas: g.allocas, tmp: g.tmp, term: g.term, dbgScope: g.dbgScope)
g.dbgScope = -1 // a helper made on the side has no source lines
[]str empty []str empty
g.body = empty g.body = empty
g.allocas = empty g.allocas = empty
@ -595,6 +618,7 @@ fn endHelper(ptr[Gen] g, Saved s, str header) {
g.allocas = s.allocas g.allocas = s.allocas
g.tmp = s.tmp g.tmp = s.tmp
g.term = s.term g.term = s.term
g.dbgScope = s.dbgScope
} }
// for i in 0..n, inside the current function // for i in 0..n, inside the current function
@ -1048,6 +1072,7 @@ fn addVar(ptr[Gen] g, ast.Node at, str name, ast.Type t, str init, bool readonly
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, order: g.nvars)) g.scopes[g.scopes.len - 1].vars.push(Var(name: name, ty: t, slot: slot, readonly: readonly, order: g.nvars))
g.nvars += 1 g.nvars += 1
dbgVariable(g, at, name, t, slot)
return slot return slot
} }
@ -1532,6 +1557,244 @@ fn resolveStructBody(ptr[Gen] g, int i) {
g.head.push("%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) + " = type { " + fields.join(", ") + " }") g.head.push("%S." + modName(g.structs[i].module) + "." + mangle(g.structs[i].name) + " = type { " + fields.join(", ") + " }")
} }
// ---------- debug info (-g) ----------
//
// DWARF, written as LLVM metadata: a compile unit, a DISubprogram per function, a DILocation on every call,
// and a DILocalVariable for every variable, with types a debugger can open up (structs, arrays, maps, enums).
fn md(ptr[Gen] g, str text) -> int {
g.md.push(text)
return g.md.len - 1
}
fn mdRef(int id) -> str {
if id < 0 {
return "null"
}
return "!" + str(id)
}
struct SizeAlign {
int size
int align
}
fn alignUp(int n, int a) -> int {
if a <= 1 {
return n
}
return (n + a - 1) / a * a
}
// the size and alignment LLVM gives a type (what a debugger needs to find the fields)
fn sizeAlign(ptr[Gen] g, ast.Type t) -> SizeAlign {
k := t.kind
if k == "bool" || k == "char" || k == "i8" || k == "u8" {
return SizeAlign(size: 1, align: 1)
}
if k == "i16" || k == "u16" {
return SizeAlign(size: 2, align: 2)
}
if k == "i32" || k == "u32" || k == "f32" || k == "enum" {
return SizeAlign(size: 4, align: 4)
}
if k == "str" || k == "array" {
return SizeAlign(size: 24, align: 8)
}
if k == "map" {
return SizeAlign(size: 56, align: 8)
}
if k == "fixed" {
e := sizeAlign(g, t.elem[0])
return SizeAlign(size: e.size * t.count, align: e.align)
}
if k == "struct" {
sd := g.structs[findStruct(g, t.module, t.name)]
if sd.isC {
return SizeAlign(size: sd.csize, align: sd.calign)
}
cur := 0
most := 1
for ft in sd.ftypes {
f := sizeAlign(g, ft)
cur = alignUp(cur, f.align) + f.size
if f.align > most {
most = f.align
}
}
return SizeAlign(size: alignUp(cur, most), align: most)
}
return SizeAlign(size: 8, align: 8)
}
// where each field of a struct is, in bytes
fn fieldOffsets(ptr[Gen] g, StructDef sd) -> []int {
if sd.isC {
return sd.offs
}
[]int out
cur := 0
for ft in sd.ftypes {
f := sizeAlign(g, ft)
cur = alignUp(cur, f.align)
out.push(cur)
cur += f.size
}
return out
}
fn dbgMember(ptr[Gen] g, str name, int scope, int file, int line, int base, int bits, int offset) -> int {
return md(g, "!DIDerivedType(tag: DW_TAG_member, name: \"" + name + "\", scope: !" + str(scope) + ", file: !" + str(file) + ", line: " + str(line) + ", baseType: " + mdRef(base) + ", size: " + str(bits) + ", offset: " + str(offset) + ")")
}
fn basicType(ptr[Gen] g, str name, int bits, str enc) -> int {
return md(g, "!DIBasicType(name: \"" + name + "\", size: " + str(bits) + ", encoding: " + enc + ")")
}
// the debug type of a Finch type; -1 where there is none (nothing, null)
fn diType(ptr[Gen] g, ast.Type t) -> int {
k := t.kind
if k == "void" || k == "null" {
return -1
}
key := "T:" + ast.show(t)
if g.mdcache.has(key) {
return g.mdcache[key]
}
id := -1
file := g.mdfiles[0]
if k == "bool" {
id = basicType(g, "bool", 8, "DW_ATE_boolean")
} else if k == "char" {
id = basicType(g, "char", 8, "DW_ATE_signed_char")
} else if k == "int" {
id = basicType(g, "int", 64, "DW_ATE_signed")
} else if k == "float" {
id = basicType(g, "float", 64, "DW_ATE_float")
} else if k == "f32" {
id = basicType(g, "f32", 32, "DW_ATE_float")
} else if isInt(t) {
enc := "DW_ATE_unsigned"
if isSigned(t) {
enc = "DW_ATE_signed"
}
id = basicType(g, k, bitsOf(t), enc)
} else if k == "enum" {
ed := g.enums[findEnum(g, t.module, t.name)]
[]str items
for i in 0..ed.variants.len {
items.push(mdRef(md(g, "!DIEnumerator(name: \"" + ed.variants[i] + "\", value: " + str(ed.values[i]) + ")")))
}
id = md(g, "!DICompositeType(tag: DW_TAG_enumeration_type, name: \"" + ed.name + "\", file: !" + str(file) + ", size: 32, elements: !{" + items.join(", ") + "})")
} else {
// the rest can point at themselves (a list node holds a pointer to the next), so the number comes first
id = md(g, "")
g.mdcache[key] = id
if k == "ptr" {
base := -1
if t.elem.len > 0 {
base = diType(g, t.elem[0])
}
g.md[id] = "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: " + mdRef(base) + ", size: 64)"
} else if k == "fixed" {
e := sizeAlign(g, t.elem[0])
sub := md(g, "!DISubrange(count: " + str(t.count) + ")")
g.md[id] = "!DICompositeType(tag: DW_TAG_array_type, baseType: " + mdRef(diType(g, t.elem[0])) + ", size: " + str(e.size * t.count * 8) + ", elements: !{!" + str(sub) + "})"
} else if k == "str" || k == "array" {
elem := ast.simple("char")
if k == "array" {
elem = t.elem[0]
}
ptrT := md(g, "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: " + mdRef(diType(g, elem)) + ", size: 64)")
intT := diType(g, ast.simple("int"))
a := dbgMember(g, "ptr", id, file, 0, ptrT, 64, 0)
b := dbgMember(g, "len", id, file, 0, intT, 64, 64)
c := dbgMember(g, "cap", id, file, 0, intT, 64, 128)
g.md[id] = "!DICompositeType(tag: DW_TAG_structure_type, name: \"" + ast.show(t) + "\", file: !" + str(file) + ", size: 192, align: 64, elements: !{!" + str(a) + ", !" + str(b) + ", !" + str(c) + "})"
} else if k == "map" {
intT := diType(g, ast.simple("int"))
raw := md(g, "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)")
names := ["entries", "len", "used", "cap", "index", "icap", "adds"]
[]str members
for i in 0..7 {
base := intT
if i == 0 || i == 4 {
base = raw
}
members.push("!" + str(dbgMember(g, names[i], id, file, 0, base, 64, 64 * i)))
}
g.md[id] = "!DICompositeType(tag: DW_TAG_structure_type, name: \"" + ast.show(t) + "\", file: !" + str(file) + ", size: 448, align: 64, elements: !{" + members.join(", ") + "})"
} else if k == "struct" {
sd := g.structs[findStruct(g, t.module, t.name)]
offs := fieldOffsets(g, sd)
sa := sizeAlign(g, t)
[]str members
for i in 0..sd.ftypes.len {
fs := sizeAlign(g, sd.ftypes[i])
members.push("!" + str(dbgMember(g, sd.fnames[i], id, file, 0, diType(g, sd.ftypes[i]), fs.size * 8, offs[i] * 8)))
}
g.md[id] = "!DICompositeType(tag: DW_TAG_structure_type, name: \"" + sd.name + "\", file: !" + str(file) + ", size: " + str(sa.size * 8) + ", align: 8, elements: !{" + members.join(", ") + "})"
} else {
g.md[id] = "!DIBasicType(name: \"" + k + "\", size: 64, encoding: DW_ATE_unsigned)"
}
return id
}
g.mdcache[key] = id
return id
}
fn dbgLocation(ptr[Gen] g) -> int {
key := "L:" + str(g.dbgLine) + ":" + str(g.dbgCol) + ":" + str(g.dbgScope)
if g.mdcache.has(key) {
return g.mdcache[key]
}
id := md(g, "!DILocation(line: " + str(g.dbgLine) + ", column: " + str(g.dbgCol) + ", scope: !" + str(g.dbgScope) + ")")
g.mdcache[key] = id
return id
}
fn dbgSubprogram(ptr[Gen] g, ast.Node f) -> int {
sig := g.cur
[]str types
types.push(mdRef(diType(g, sig.ret)))
if sig.recv.len > 0 {
types.push("null")
}
for p in sig.params {
types.push(mdRef(diType(g, p)))
}
file := g.mdfiles[f.file]
sub := md(g, "!DISubroutineType(types: !{" + types.join(", ") + "})")
return md(g, "distinct !DISubprogram(name: \"" + sig.name + "\", linkageName: \"" + sig.ll.sub(1, sig.ll.len) + "\", scope: !" + str(file) + ", file: !" + str(file) + ", line: " + str(f.line) + ", type: !" + str(sub) + ", scopeLine: " + str(f.line) + ", flags: DIFlagPrototyped, spFlags: DISPFlagDefinition | DISPFlagLocalToUnit, unit: !0)")
}
fn dbgVariable(ptr[Gen] g, ast.Node at, str name, ast.Type t, str slot) {
if !g.debug || g.dbgScope < 0 {
return
}
ty := diType(g, t)
if ty < 0 {
return
}
arg := ""
if g.dbgArg > 0 {
arg = ", arg: " + str(g.dbgArg)
}
v := md(g, "!DILocalVariable(name: \"" + name + "\", scope: !" + str(g.dbgScope) + ", file: !" + str(g.mdfiles[at.file]) + ", line: " + str(at.line) + ", type: !" + str(ty) + arg + ")")
g.dbgLine = at.line
g.dbgCol = at.col
emit(g, "call void @llvm.dbg.declare(metadata ptr " + slot + ", metadata !" + str(v) + ", metadata !DIExpression())")
}
// the metadata, at the end of the module
fn dbgModule(ptr[Gen] g) -> str {
[]str lines = ["!llvm.dbg.cu = !{!0}", "!llvm.module.flags = !{!1, !2}"]
for i in 0..g.md.len {
lines.push("!" + str(i) + " = " + g.md[i])
}
return lines.join("\n") + "\n"
}
fn define(ptr[Gen] g, int si) { fn define(ptr[Gen] g, int si) {
g.cur = g.sigs[si] g.cur = g.sigs[si]
g.module = g.cur.module g.module = g.cur.module
@ -1548,6 +1811,14 @@ fn define(ptr[Gen] g, int si) {
g.tmp = 0 g.tmp = 0
g.term = false g.term = false
g.nvars = 0 g.nvars = 0
g.dbgScope = -1
dbgsp := -1
if g.debug {
dbgsp = dbgSubprogram(g, f)
g.dbgScope = dbgsp
g.dbgLine = f.line
g.dbgCol = f.col
}
g.deferOn = false g.deferOn = false
g.inDefer = 0 g.inDefer = 0
[]Scope none []Scope none
@ -1561,7 +1832,12 @@ fn define(ptr[Gen] g, int si) {
if g.cur.recvEnum { if g.cur.recvEnum {
selfKind = "enum" selfKind = "enum"
} }
g.scopes[0].vars.push(Var(name: "self", ty: ast.Type(kind: selfKind, name: g.cur.recv, module: g.cur.recvmod), slot: "%self", readonly: false)) selfTy := ast.Type(kind: selfKind, name: g.cur.recv, module: g.cur.recvmod)
g.scopes[0].vars.push(Var(name: "self", ty: selfTy, slot: "%self", readonly: false))
if g.debug {
// the struct is at the address %self holds: shown as the struct, whose slot that address is
dbgVariable(g, f, "self", selfTy, "%self")
}
} }
for k in 0..g.cur.params.len { for k in 0..g.cur.params.len {
pt := g.cur.params[k] pt := g.cur.params[k]
@ -1570,7 +1846,12 @@ fn define(ptr[Gen] g, int si) {
if owning(g, pt) && !g.cur.borrow[k] { if owning(g, pt) && !g.cur.borrow[k] {
v = copyValue(g, v, pt) v = copyValue(g, v, pt)
} }
g.dbgArg = k + 1
if g.cur.recv.len > 0 {
g.dbgArg = k + 2
}
addVar(g, f.kids[k], g.cur.pnames[k], pt, v, false) addVar(g, f.kids[k], g.cur.pnames[k], pt, v, false)
g.dbgArg = 0
} }
block(g, f.kids[f.kids.len - 1], false) block(g, f.kids[f.kids.len - 1], false)
if !g.term { if !g.term {
@ -1584,7 +1865,12 @@ fn define(ptr[Gen] g, int si) {
emit(g, "ret void") emit(g, "ret void")
} }
} }
g.out.push("define internal " + retTy(g, g.cur) + " " + g.cur.ll + "(" + params.join(", ") + ") {\nentry:\n" + attach := ""
if dbgsp >= 0 {
attach = " !dbg !" + str(dbgsp)
}
g.dbgScope = -1
g.out.push("define internal " + retTy(g, g.cur) + " " + g.cur.ll + "(" + params.join(", ") + ")" + attach + " {\nentry:\n" +
g.allocas.join("\n") + "\n" + g.body.join("\n") + "\n}\n") g.allocas.join("\n") + "\n" + g.body.join("\n") + "\n}\n")
} }
@ -1606,6 +1892,8 @@ fn block(ptr[Gen] g, ast.Node b, bool scope) {
} }
fn stmt(ptr[Gen] g, ast.Node s) { fn stmt(ptr[Gen] g, ast.Node s) {
g.dbgLine = s.line
g.dbgCol = s.col
k := s.kind k := s.kind
if k == "block" { if k == "block" {
block(g, s, true) block(g, s, true)
@ -4515,9 +4803,32 @@ fn runtimeDecls() -> str {
return decls.join("\n") + "\n" return decls.join("\n") + "\n"
} }
fn generate([]ast.Program progs, []str files, cimport.CImports ci, bool shared) -> str { fn assemble(ptr[Gen] g) -> str {
gen := Gen(progs: progs, files: files, ci: ci, shared: shared) text := "; generated by the Finch compiler written in Finch\n\n" + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n"
if g.debug {
text = text + "declare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n"
}
text = text + g.out.join("\n")
if g.debug {
text = text + "\n" + dbgModule(g)
}
return text
}
fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) -> str {
shared := opt.shared
gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, cwd: opt.cwd, dbgScope: -1)
g := addr(gen) g := addr(gen)
if g.debug {
// !0 the compile unit, !1 and !2 the module flags, then the source files
g.md.push("")
g.md.push("!{i32 7, !\"Dwarf Version\", i32 4}")
g.md.push("!{i32 2, !\"Debug Info Version\", i32 3}")
for fname in files {
g.mdfiles.push(md(g, "!DIFile(filename: \"" + fname + "\", directory: \"" + g.cwd + "\")"))
}
g.md[0] = "distinct !DICompileUnit(language: DW_LANG_C, file: !" + str(g.mdfiles[0]) + ", producer: \"finch\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)"
}
declare(g) declare(g)
main := findFn(g, "", "main") main := findFn(g, "", "main")
if shared { if shared {
@ -4542,7 +4853,7 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, bool shared)
if shared { if shared {
// finch_module_on_fatal(handler): the host's say on runtime errors (see runtime/finch_rt.c) // finch_module_on_fatal(handler): the host's say on runtime errors (see runtime/finch_rt.c)
g.out.push("define void @finch_module_on_fatal(ptr %handler) {\nentry:\n call void @finch_on_fatal(ptr %handler)\n ret void\n}\n") g.out.push("define void @finch_module_on_fatal(ptr %handler) {\nentry:\n call void @finch_on_fatal(ptr %handler)\n ret void\n}\n")
return "; generated by the Finch compiler written in Finch\n\n" + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" + g.out.join("\n") return assemble(g)
} }
// the C entry point calls the Finch main // the C entry point calls the Finch main
m := g.sigs[main] m := g.sigs[main]
@ -4571,5 +4882,5 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, bool shared)
ret = " %code = trunc i64 %r to i32\n ret i32 %code\n" ret = " %code = trunc i64 %r to i32\n ret i32 %code\n"
} }
g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + pre + " " + callText + "\n" + ret + "}\n") g.out.push("define i32 @main(i32 %argc, ptr %argv) {\nentry:\n" + pre + " " + callText + "\n" + ret + "}\n")
return "; generated by the self-hosted Finch compiler\n\n" + g.head.join("\n") + "\n\n" + runtimeDecls() + "\n" + g.out.join("\n") return assemble(g)
} }

View file

@ -136,6 +136,7 @@ fn main([]str args) -> int {
out := stem(file) out := stem(file)
outGiven := false outGiven := false
shared := false shared := false
debug := false
opt := "-O2" opt := "-O2"
[]str extraLibs []str extraLibs
[]str extraDirs []str extraDirs
@ -161,8 +162,10 @@ fn main([]str args) -> int {
} else if a == "-I" && ai + 1 < args.len { } else if a == "-I" && ai + 1 < args.len {
extraDirs.push(args[ai + 1]) extraDirs.push(args[ai + 1])
ai += 1 ai += 1
} else if a == "-g" || a == "--target" { } else if a == "-g" {
print("error: '" + a + "' is not in the compiler written in Finch yet; the C++ one has it (branch before-bootstrap)") debug = true
} else if a == "--target" {
print("error: '--target' is not in the compiler written in Finch yet; the C++ one has it (branch before-bootstrap)")
return 1 return 1
} else if cmd == "run" { } else if cmd == "run" {
progArgs.push(a) progArgs.push(a)
@ -219,7 +222,7 @@ fn main([]str args) -> int {
print(err) print(err)
return 1 return 1
} }
ir := gen.generate(loader.progs, loader.files, ci, shared) ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, cwd: getenv("PWD")))
if cmd == "ir" { if cmd == "ir" {
print(ir) print(ir)
return 0 return 0
@ -246,6 +249,9 @@ fn main([]str args) -> int {
if shared { if shared {
sharedFlags = " -shared -fPIC" sharedFlags = " -shared -fPIC"
} }
if debug {
sharedFlags = sharedFlags + " -g" // or clang drops the debug info that is in the IR
}
// the compiler of the IR is always a clang (CC may be gcc, which can't read it); FINCH_CC picks another clang // the compiler of the IR is always a clang (CC may be gcc, which can't read it); FINCH_CC picks another clang
cc := getenv("FINCH_CC") cc := getenv("FINCH_CC")
if cc.len == 0 { if cc.len == 0 {

View file

@ -60,6 +60,20 @@ for f in tests/new/fail/*.fch; do
echo "$got" | head -3 echo "$got" | head -3
fi fi
done done
# the same programs with debug info (-g -O0): the metadata has to be valid for every kind of thing a program has
for f in tests/new/enums.fch tests/new/generics.fch tests/new/std_json.fch tests/run/basics.fch tests/run/methods_basic.fch \
tests/run/maps.fch tests/run/errors.fch tests/run/defer.fch tests/run/c_structs.fch tests/run/memory.fch; do
input="${f%.fch}.in"
[ -f "$input" ] || input=/dev/null
if "$boot" build "$f" -g -O0 -o "$tmp/progg" > "$tmp/err" 2>&1 && [ "$("$tmp/progg" < "$input" 2>&1)" == "$(cat "${f%.fch}.out")" ]; then
pass=$((pass + 1))
else
failed=$((failed + 1))
echo "FAIL (-g) $f"
head -3 "$tmp/err"
fi
done
# a library a C program loads (finch build --shared), with the compiler under test # a library a C program loads (finch build --shared), with the compiler under test
if FINCH_RT="$PWD/runtime/finch_rt.c" FINCH="$boot" tests/shared.sh > "$tmp/shared.out" 2>&1; then if FINCH_RT="$PWD/runtime/finch_rt.c" FINCH="$boot" tests/shared.sh > "$tmp/shared.out" 2>&1; then
pass=$((pass + 1)) pass=$((pass + 1))