diff --git a/lib/emit.ml b/lib/emit.ml index 6d4264f..55b98b5 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -600,6 +600,30 @@ type f = { what makes the pop happen on the transfer path as well as the normal one. [None] in a release build, where there is no frame at all. *) mutable frame : string option; + (* How many dyn roots this function pushed at entry, and so how many one + [flan_dyn_root_pop] at each exit takes off. Zero for every function with + no dyn in it, which is every function in every program written so far — + and zero means *nothing is emitted at all*, neither push nor pop nor a + pop of zero. That is what keeps a fully annotated program's IR byte for + byte what it was before dyn existed, which is the thing [--no-gc] + promises and is tested for. + + It is a count and not a saved depth because the ABI offers + [flan_dyn_root_pop(n)] and no way to read the stack's height; it can be a + count, rather than needing one, because the number is a static property of + the function that [dyn_roots] works out before a line of the body is + emitted. That matters: [ret] runs *during* emission, and a count + accumulated as roots were discovered would be short at every early + return. *) + mutable droots : int; + (* The root slots' addresses, in push order: the dyn slots first and then one + per dyn-producing runtime call, minted by [dyn_tmp] as the body is + emitted. Both kinds are entry-block allocas, so the addresses are good for + the function's whole extent — which is why rooting is per function here + and not per scope. A slot that is not live any more holds a value the + collector keeps one cycle longer than it must, and that is the safe + direction to be wrong in. *) + mutable droot_ns : string list; (* Where a dev build records each slot's address, so that a stopped frame's locals can be read. [None] in a release build and in a function with no named slot at all. Only *named* slots are recorded: a slot the compiler @@ -666,10 +690,67 @@ let label f name = this frame, so a pop written only on the normal path leaves a dead frame on the stack after every handled error, and the next backtrace is a lie. Same lesson [emit_with_alloc] learned about the context allocator. *) +(* How many dyn roots a function will push, worked out before any of it is + emitted. One per dyn slot — a parameter or a local of that type — and one per + runtime call that answers a dyn, because the word a call hands back is live + from the moment it exists and the next allocation may be the one that + collects it. + + Rooting every dyn-producing call, rather than only the ones whose value + outlives a call, is conservative and is the only thing available: this file + has no liveness and no lexical scope, both of which the checker resolved + away into flat slot indices long before anything got here. The cost is real + and is the cost of a precise collector with an address-registration ABI + rather than stack maps — a rooted alloca has its address escape through + [flan_dyn_root_push], so mem2reg cannot promote it, and every dyn value + becomes a stack slot with a store at every optimisation level. + + A count rather than a running tally for the reason [droots] gives: [ret] is + reached while the body is still being emitted. *) +let dyn_roots (fn : Tast.fn) = + let slots = + Array.fold_left + (fun acc t -> if t = Types.Dyn then acc + 1 else acc) 0 fn.Tast.slots + in + let temps = ref 0 in + let count (e : Tast.expr) = + match e.Tast.e with + | Tast.Prim (Tast.Rt _, _) when e.Tast.ty = Types.Dyn -> incr temps + | _ -> () + in + List.iter (Tast.walk count) fn.Tast.body; + slots + !temps + +(* The next pre-made root slot for a dyn temporary. They are all minted, zeroed + and pushed in the entry block before a line of the body is emitted, and this + only hands them out — which is what makes the pushes and the pops balance by + construction rather than by the body being walked the same way twice. + + [dyn_roots] counts the same nodes the emission visits, so the supply runs + out only if those two disagree. If it ever does, the fallback is an ordinary + unrooted slot: one temporary the collector cannot see is a bug to find, + where a root stack that pops more than it pushed is memory corruption. *) +let dyn_tmp f = + match f.droot_ns with + | n :: rest -> f.droot_ns <- rest; n + | [] -> + let name = Printf.sprintf "%%dx%d" f.n in + f.n <- f.n + 1; + Buffer.add_string f.allocas (Printf.sprintf " %s = alloca i64\n" name); + name + let ret f v = (match f.frame with | Some prev -> ins f "store ptr %s, ptr @flan_frame_head" prev | None -> ()); + (* The pop, on every path out, for exactly the reason the shadow stack's is + here: a condition handled further out unwinds through the landing block, + and a pop written only on the normal path would leave this function's + roots on the stack after every handled error. The [unreachable] + terminators emit none, and are right not to — each of them dies inside C + and the process does not come back. *) + if f.droots > 0 then + ins f "call void @flan_dyn_root_pop(i64 %d)" f.droots; term f "ret %s %s" (ll f.ret) v (* The store that says "this slot is bound now". Emitted at each binding of a @@ -2262,6 +2343,18 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = let t = fresh f in ins f "%s = call %s @%s(%s)" t (ll e.Tast.ty) sym args'; if signals then guard f; + (* A dyn word is spilled into a rooted slot the instant it exists. It is + an SSA value otherwise, and an SSA value is invisible to a collector + that finds its roots by address — the next allocation could be the one + that frees what this is holding. [dyn_roots] counted this call, so the + slot below is one the entry block has already pushed. + + The value carries on being used as a register: the store is what the + collector reads, and reading it back would only make the IR longer. *) + if e.Tast.ty = Types.Dyn then begin + let slot = dyn_tmp f in + ins f "store i64 %s, ptr %s" t slot + end; t end | Tast.SizeOf t, [] -> Printf.sprintf "%d" (fst (lay f.md t)) @@ -2431,6 +2524,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) = pads = []; loops = []; unwind = "unwind"; unwound = false; defers = fn.Tast.fdefers; frame = None; slotv = None; snames = fn.Tast.snames; + droots = 0; droot_ns = []; dsub; dline = (if fn.Tast.floc.Loc.line = 0 then 1 else fn.Tast.floc.Loc.line); dloc = ""; @@ -2449,6 +2543,54 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) = Buffer.add_string f.allocas (Printf.sprintf " store %s %%p%d, ptr %s\n" (ll ty) i f.slots.(i))) fn.Tast.params; + (* The dyn roots, and this is not gated on [m.dev]: the shadow stack below is + a debugging convenience and a release build does without it, while a + collector that cannot find its roots is a collector that frees live + values. Every build pays this, and only a function that has a dyn in it + pays anything — [dyn_roots] is zero otherwise and not a line is emitted, + which is what makes an annotated program's IR identical with and without + --no-gc. + + The dyn *slots* are already allocas from the loop above, so they are + pushed where they are; the temporaries need slots of their own, and they + are minted here, in order, so that [dyn_tmp] only has to hand them out. + Zeroed because the push happens at entry and the call that fills one may + be inside a branch that never runs — runtime/flan_dyn.h says a rooted slot + holding 0 is not a value. *) + let nroots = dyn_roots fn in + if nroots > 0 then begin + let nparams = List.length fn.Tast.params in + let pushed = ref [] in + Array.iteri + (fun i t -> + if t = Types.Dyn then begin + (* A parameter's slot was filled from [%pN] a few lines above and + must not be zeroed over the top of it. Every other slot holds + whatever the stack held until its binding runs, and the binding + may be inside a branch that does not. *) + if i >= nparams then + Buffer.add_string f.allocas + (Printf.sprintf " store i64 0, ptr %s\n" f.slots.(i)); + pushed := f.slots.(i) :: !pushed + end) + fn.Tast.slots; + let ntemps = nroots - List.length !pushed in + let temps = + List.init ntemps (fun i -> + let name = Printf.sprintf "%%dr%d" i in + Buffer.add_string f.allocas (Printf.sprintf " %s = alloca i64\n" name); + Buffer.add_string f.allocas + (Printf.sprintf " store i64 0, ptr %s\n" name); + name) + in + List.iter + (fun n -> + Buffer.add_string f.allocas + (Printf.sprintf " call void @flan_dyn_root_push(ptr %s)\n" n)) + (List.rev !pushed @ temps); + f.droots <- nroots; + f.droot_ns <- temps + end; (* The shadow stack's push, in the entry block, and the pop is at every [ret] (see [ret]). plan.org has had *Frames: shadow stack* in the dev column since the beginning; this is it, and it is dev-only, so a shipped @@ -2935,14 +3077,53 @@ declare i64 @flan_file_fail_reason() declare i8 @flan_slurp_into(ptr, ptr, i64, i64, ptr, i64) |} +(* Whether the program has a dyn in it anywhere, which is the one question + [main] asks before calling [flan_gc_init]. Asked of the whole program rather + than assumed, so that a program with no dyn emits no call and its [main] is + byte for byte the [main] it was before any of this existed. + + Every shape a dyn can take is one of these: a global of that type, a + signature that mentions it, a slot that holds one, or an expression that + produces one. *) +let uses_dyn (p : Tast.program) = + let found = ref false in + let note t = if t = Types.Dyn then found := true in + List.iter (fun (g : Tast.global) -> note g.Tast.gty) p.Tast.globals; + List.iter + (fun (fn : Tast.fn) -> + List.iter note fn.Tast.params; + note fn.Tast.ret; + Array.iter note fn.Tast.slots; + List.iter (Tast.walk (fun (e : Tast.expr) -> note e.Tast.ty)) fn.Tast.body) + p.Tast.fns; + !found + (* C's main, adapting to whichever of the four shapes Flan's main has: argv and the i32 status are each optional (plan.org, Milestone-2 primitives). *) -let emit_main m ?(startup = false) (fn : Tast.fn) = +let emit_main m ?(startup = false) ?(gc = false) ?(dyn_globals = []) (fn : Tast.fn) = let b = Buffer.create 256 in Buffer.add_string b (Printf.sprintf "\ndefine i32 @main(i32 %%argc, ptr %%argv)%s {\nentry:\n" (attrs m)); Buffer.add_string b " call void @flan_rt_init(i32 %argc, ptr %argv)\n"; + (* Immediately after the host runtime and before anything that could box: a + dyn global's initialiser runs in the startup function below, and the very + first thing it does is allocate. *) + if gc then Buffer.add_string b " call void @flan_gc_init()\n"; + (* The dyn globals, rooted here and never popped, which is the whole of what + a global's extent means. They go on the stack *before* the startup + function runs, because that function is what fills them and its first + allocation may be the one that collects — and before any of it pushes a + root of its own, because every pop in the program takes the top of the + stack and these are the ones that must never be at the top. + + Zero is what a global holds until its initialiser has run: BSS gives that + for free, and runtime/flan_dyn.h says a rooted slot holding 0 is not a + value. *) + List.iter + (fun g -> Buffer.add_string b + (Printf.sprintf " call void @flan_dyn_root_push(ptr %s)\n" (gname g))) + dyn_globals; (* The program's own end of the transfer channel. Nothing can be transferring when [main] returns: a restart is found by name on the restart stack, and an [invoke-restart] that finds none fails at the invoke site rather than @@ -3247,7 +3428,14 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = []) p.Tast.fns; let startup = emit_startup m ~hidden p.Tast.globals in (match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns with - | Some fn -> emit_main m ~startup fn + | Some fn -> + emit_main m ~startup ~gc:(uses_dyn p) + ~dyn_globals: + (List.filter_map + (fun (g : Tast.global) -> + if g.Tast.gty = Types.Dyn then Some g.Tast.gname else None) + p.Tast.globals) + fn (* A program with no [main] is linked into a C host that brings its own entry point, and then nothing calls the startup function — which is why the constant image is a constant image on both backends and not a diff --git a/test/programs/dyn-global.flan b/test/programs/dyn-global.flan new file mode 100644 index 0000000..e16f16d --- /dev/null +++ b/test/programs/dyn-global.flan @@ -0,0 +1,21 @@ +;;;; A dyn global, which is the case that needs the startup function. +;;;; +;;;; A dyn value is made by a call into the runtime, and a call is not a +;;;; constant, so the initialiser cannot be a constant image the way a typed +;;;; global's is. It runs in flan..init-globals, which main calls after +;;;; flan_gc_init and before anything the programmer wrote — the same machinery +;;;; the computed globals already use, which is the point: a dyn global is a +;;;; computed global and needed no new mechanism. + +(defvar counter dyn 0) +(defvar label dyn "start") + +(defn bump [] () + (set counter (+ counter 1))) + +(defn main [] () + (print counter) (print " ") (print label) (print "\n") + (bump) + (bump) + (set label "done") + (print counter) (print " ") (print label) (print "\n"))