(** Typed IR → LLVM IR, as text. Text rather than libLLVM bindings, for the reasons in plan.org: the build dependency is a clang on PATH instead of a version-pinned libLLVM with C++ linkage, the output is readable when something is wrong, and an LLVM upgrade does not break the compiler. The only thing text loses is the in-process JIT, and that was measured at ~13ms — below perception. Layout — this is the whole of it, and it is deliberately C's: {v i8..i64 / u8..u64 i8..i64 signedness lives in the ops f32 f64 float double bool i1 [T] and string { ptr, i64 } ptr+len, non-owning [n T] [n x T] inline, a value (Ptr T) ptr opaque pointers (Option T) { i8, T } tag 0 None, 1 Some a struct a literal struct in declaration order Unit and Never {} zero-sized, one value v} No object headers anywhere, which is the consequence that drives everything (plan.org, Memory) — a Flan struct is exactly its C struct. Two things fall out of the layout and are load-bearing: - Every slot is an [alloca], so reading a local is a [load] and assigning is a [store]. Aggregates are SSA values in LLVM, so a [store] of a struct or a fixed array *is* the copy that spec-memory.md requires on assignment, and a slice copies its view for the same reason. [addr] of a local is then just the alloca. [mem2reg] removes the ones nobody took the address of. - A place lowers to a pointer and a value to a load from it, which is the split the interpreter would have had to make by hand: [(set (.pos c) ...)] through a [(Ptr Cursor)] becomes a [getelementptr] on the pointer, not on a copy of the struct. *) let fail = Loc.fail (* [List.map]'s evaluation order is unspecified, and so is [let ... and ...]. Emission is all side effect — instructions, calls, branches to a [ret] — so left-to-right is required, not a preference. Same rule as in Check. *) let rec map_lr f = function | [] -> [] | x :: rest -> let y = f x in y :: map_lr f rest (* ── Names ─────────────────────────────────────────────────────────── *) (* Flan names contain -, ?, > and /, so every emitted name is quoted. The [flan.] prefix keeps the Flan [main] from colliding with C's. *) let quoted s = "\"" ^ s ^ "\"" let fname n = "@" ^ quoted ("flan." ^ n) let gname n = "@" ^ quoted ("flan." ^ n) let sname n = "%" ^ quoted n (* A dev build's redefinable calls go through a cell: a mutable global holding the address of the function that is current. Redefinition is then one store, and every existing call site follows it — which is the whole point, since a call bound at link time cannot be made to notice a new body. Release builds have no cells and call the symbol directly. *) let cellname n = "@" ^ quoted ("flan.cell." ^ n) (* A name the host was never built with — a defn or a defvar typed in after the process started — has no symbol to bind to, so it is keyed by string through [flan_dev_cell] / [flan_dev_global] and the answer is cached in one of these module-local slots. One indirection more than a name the host has, which is why the compiler picks per name rather than routing everything this way. *) (* The transfer channel's parameter, the one name that is not a Flan name. It is not a slot: nothing in the language can address it, and it is read and written only by the guards this file emits. *) let xfer_param = "%xfer" (* The condition's own name, for the message an unhandled [error] prints. The checker has already refused anything that is not a struct. *) let struct_name_of (t : Types.t) = match t with Types.Named n -> n | _ -> "a condition" let cellptr n = "@" ^ quoted ("flan.cellp." ^ n) let globalptr n = "@" ^ quoted ("flan.gp." ^ n) (* ── Types ─────────────────────────────────────────────────────────── *) let rec ll (t : Types.t) = match t with | Types.Int k -> "i" ^ string_of_int (Types.bits k) | Types.Float Types.F32 -> "float" | Types.Float Types.F64 -> "double" | Types.Bool -> "i1" | Types.String | Types.Slice _ -> "%slice" | Types.Unit | Types.Never -> "{}" | Types.Named n -> sname n (* A C enum is an i32 — its own type in the checker, nothing at all here. *) | Types.Enum _ -> "i32" | Types.Array (n, e) -> Printf.sprintf "[%Ld x %s]" n (ll e) | Types.Ptr _ -> "ptr" | Types.Option e -> Printf.sprintf "{ i8, %s }" (ll e) | Types.Map _ | Types.Fn _ | Types.Var _ -> (* The checker rejects each of these by name — nothing reaches here. *) failwith ("no layout for " ^ Types.to_string t) let is_void (t : Types.t) = match t with Types.Unit | Types.Never -> true | _ -> false (* -- Debug info ----------------------------------------------------- *) (* DWARF, as LLVM metadata. This is only worth the lines it takes because of the layout above: a Flan struct *is* its C struct, every slot is an alloca and there are no tag words, so the debug info describes machine types directly and lldb has to learn nothing about Flan. The compile unit says DW_LANG_C99 for that reason -- it is less a claim about the source language than the truth about the data model, and it is what makes lldb's own struct-printing correct here. Metadata is a flat numbered pool with no ordering requirement, so a node can be allocated an id, referred to, and written out later -- which is what makes a recursive struct (a field of type [(Ptr Self)]) expressible. *) type dbg = { mutable dn : int; (* next metadata id *) dout : Buffer.t; (* the [!N = ...] lines *) dfiles : (string, int) Hashtbl.t; (* path -> !DIFile *) dtys : (string, int) Hashtbl.t; (* Types.to_string -> a type node *) dlocs : (string, int) Hashtbl.t; (* scope:line:col -> !DILocation *) mutable dcu : int; } let dalloc d = let n = d.dn in d.dn <- n + 1; n let dput d n body = Buffer.add_string d.dout (Printf.sprintf "!%d = %s\n" n body) let dnode d body = let n = dalloc d in dput d n body; n (* Metadata strings are C strings in the .ll grammar, so the two characters that could end one have to be escaped. Flan names contain - ? > and /, none of which do. *) let dstr s = let b = Buffer.create (String.length s + 2) in String.iter (fun c -> if c = '"' || c = '\\' then (Buffer.add_char b '\\'; Buffer.add_char b c) else Buffer.add_char b c) s; Buffer.contents b let dfile d path = match Hashtbl.find_opt d.dfiles path with | Some n -> n | None -> let abs = if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path else path in let n = dnode d (Printf.sprintf "!DIFile(filename: \"%s\", directory: \"%s\")" (dstr (Filename.basename abs)) (dstr (Filename.dirname abs))) in Hashtbl.replace d.dfiles path n; n (* -- Layout ---------------------------------------------------------- DWARF wants member offsets as integer literals: [!DIDerivedType(tag: DW_TAG_member, offset: N)] takes a constant and nothing else, so the [ptrtoint (ptr getelementptr ...)] form this file uses elsewhere for a size is not accepted there and these have to be computed. That makes this the one place in the backend where a layout number is worked out rather than handed to LLVM, and it is exactly where a wrong answer shows up as a plausible value printed for the wrong field. So the acceptance test checks every offset against LLVM's own [getelementptr] answer for the same struct type, not against a table written by the same hand as the code. The rules are C's, which is what LLVM gives a non-packed literal struct: natural alignment, each member at the next aligned offset, tail padding out to the struct's own alignment. The numbers are the host's -- [ptr] is 8 bytes -- which is why [Build] refuses a debug build for wasm32. *) let align_up x a = if a <= 1 then x else ((x + a - 1) / a) * a (* ── Module-level state ────────────────────────────────────────────── *) type m = { out : Buffer.t; strs : Buffer.t; (* string literal constants *) structs : (string, Tast.structure) Hashtbl.t; globals : (string, Types.t) Hashtbl.t; (* Flan name -> C symbol, for the foreign functions. A call to one names the symbol directly; there is no thunk. *) externs : (string, string) Hashtbl.t; checks : bool; (* emit bounds checks *) dev : bool; (* call through cells (below) *) (* Was this name in the build the running process came from? False only in a redefinition module, and only for a name introduced since. *) known : string -> bool; (* [Some] in a debug build. It lives on the module rather than being passed down because every emitter that can produce an instruction has to be able to hang a location on it. *) dbg : dbg option; mutable nstr : int; } let field_ty m sn i = let s = Hashtbl.find m.structs sn in (List.nth s.Tast.fields i).Tast.fty (* Size and alignment in bytes. *) let rec lay m (t : Types.t) : int * int = match t with | Types.Int k -> let n = Types.bits k / 8 in n, n | Types.Float Types.F32 -> 4, 4 | Types.Float Types.F64 -> 8, 8 (* [i1] occupies a byte in memory. *) | Types.Bool -> 1, 1 | Types.String | Types.Slice _ -> 16, 8 | Types.Unit | Types.Never -> 0, 1 | Types.Enum _ -> 4, 4 | Types.Ptr _ -> 8, 8 (* [n x T] adds no padding of its own: T's size already carries its tail. *) | Types.Array (n, e) -> let s, a = lay m e in Int64.to_int n * s, a | Types.Option e -> let s, a, _ = lay_fields m [ Types.Int Types.I8; e ] in s, a | Types.Named n -> (match Hashtbl.find_opt m.structs n with | Some st -> let s, a, _ = lay_fields m (List.map (fun (fl : Tast.field) -> fl.Tast.fty) st.Tast.fields) in s, a | None -> failwith ("no layout for struct " ^ n)) | Types.Map _ | Types.Fn _ | Types.Var _ -> failwith ("no layout for " ^ Types.to_string t) (* Size, alignment, and the offset of every member. *) and lay_fields m tys = let off = ref 0 and al = ref 1 and rev = ref [] in List.iter (fun t -> let s, a = lay m t in let a = if a < 1 then 1 else a in off := align_up !off a; rev := !off :: !rev; off := !off + s; if a > !al then al := a) tys; align_up !off !al, !al, List.rev !rev (* A DWARF type node for a Flan type, memoised by the type's printed form so the pool holds one node per distinct type. *) let rec dty m d (t : Types.t) : int = let key = Types.to_string t in match Hashtbl.find_opt d.dtys key with | Some n -> n | None -> let basic name bits enc = dnode d (Printf.sprintf "!DIBasicType(name: \"%s\", size: %d, encoding: %s)" (dstr name) bits enc) in (* A struct-shaped node, with its id claimed before the members are built: a field of type [(Ptr Self)] comes back through here. *) let composite name members = let id = dalloc d in Hashtbl.replace d.dtys key id; let size, al, offs = lay_fields m (List.map snd members) in let ms = List.map2 (fun (mname, mty) off -> let fs, fa = lay m mty in let base = dty m d mty in dnode d (Printf.sprintf "!DIDerivedType(tag: DW_TAG_member, name: \"%s\", baseType: !%d, size: %d, align: %d, offset: %d)" (dstr mname) base (fs * 8) (fa * 8) (off * 8))) members offs in dput d id (Printf.sprintf "!DICompositeType(tag: DW_TAG_structure_type, name: \"%s\", size: %d, align: %d, elements: !{%s})" (dstr name) (size * 8) (al * 8) (String.concat ", " (List.map (fun i -> Printf.sprintf "!%d" i) ms))); id in let n = match t with | Types.Int k -> (* DW_ATE_signed / DW_ATE_unsigned, not the _char variants: an i8 is a number in Flan, and lldb prints a character for a char. *) basic (Types.to_string t) (Types.bits k) (if Types.signed k then "DW_ATE_signed" else "DW_ATE_unsigned") | Types.Float k -> basic (Types.to_string t) (Types.bits_f k) "DW_ATE_float" | Types.Bool -> basic "bool" 8 "DW_ATE_boolean" | Types.Enum e -> basic e 32 "DW_ATE_signed" | Types.Unit | Types.Never -> composite (Types.to_string t) [] | Types.Ptr e -> let id = dalloc d in Hashtbl.replace d.dtys key id; (* [(Ptr Unit)] and [(Ptr Never)] are the opaque pointer, and a DWARF pointer with no base type is exactly C's void *. *) let base = match e with | Types.Unit | Types.Never -> "null" | e -> Printf.sprintf "!%d" (dty m d e) in dput d id (Printf.sprintf "!DIDerivedType(tag: DW_TAG_pointer_type, baseType: %s, size: 64)" base); id | Types.Array (n, e) -> let base = dty m d e in let size, al = lay m t in let sub = dnode d (Printf.sprintf "!DISubrange(count: %Ld)" n) in dnode d (Printf.sprintf "!DICompositeType(tag: DW_TAG_array_type, baseType: !%d, size: %d, align: %d, elements: !{!%d})" base (size * 8) (al * 8) sub) (* ptr+len, and shown as ptr+len. There is no hidden owner and no capacity, so two members are the whole truth about a slice. *) | Types.String -> composite "string" [ ("ptr", Types.Ptr (Types.Int Types.U8)); ("len", Types.Int Types.I64) ] | Types.Slice e -> composite (Types.to_string t) [ ("ptr", Types.Ptr e); ("len", Types.Int Types.I64) ] | Types.Option e -> composite (Types.to_string t) [ ("tag", Types.Int Types.U8); ("value", e) ] | Types.Named sn -> (match Hashtbl.find_opt m.structs sn with | Some st -> composite sn (List.map (fun (fl : Tast.field) -> (fl.Tast.fname, fl.Tast.fty)) st.Tast.fields) | None -> failwith ("no debug type for struct " ^ sn)) | Types.Map _ | Types.Fn _ | Types.Var _ -> failwith ("no debug type for " ^ Types.to_string t) in Hashtbl.replace d.dtys key n; n (* ── Per-function state ────────────────────────────────────────────── *) type f = { md : m; allocas : Buffer.t; (* the entry block: mem2reg only promotes these *) b : Buffer.t; mutable n : int; mutable live : bool; (* is the current block still open? *) ret : Types.t; slots : string array; slot_tys : Types.t array; (* The transfer channel's landing blocks, spec-conditions.md §6. A guard after a call branches to the innermost one; each pops whatever frames it established and either catches the transfer or forwards it outward. The innermost is first, and with none open a transfer leaves the function through [unwind], which runs its defers (§5) and returns early. The flag says the block was branched to, so an unused one is not emitted. *) mutable pads : (string * bool ref) list; unwind : string; mutable unwound : bool; defers : Tast.expr list; (* The function's !DISubprogram, in a debug build, and the line it was declared on -- the fallback for a node the checker made up. *) dsub : int option; dline : int; (* The [, !dbg !N] suffix every instruction in this function carries, or "". Uniform rather than only on the instructions that want a line: LLVM's verifier rejects a call without a location inside a function that has debug info, and this file emits calls from a dozen places -- the bounds failure, the handler push and pop, the transfer guards -- none of which would remember to ask. *) mutable dloc : string; } let fresh f = f.n <- f.n + 1; Printf.sprintf "%%t%d" f.n let fresh_label f name = f.n <- f.n + 1; Printf.sprintf "%s%d" name f.n (* Nothing may follow a terminator, so emission after one is dropped: the code is unreachable and LLVM would reject it. *) let ins f fmt = Printf.ksprintf (fun s -> if f.live then Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n")) fmt let term f fmt = Printf.ksprintf (fun s -> if f.live then Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n"); f.live <- false) fmt let label f name = Buffer.add_string f.b (Printf.sprintf "\n%s:\n" name); f.live <- true let alloca f ty = let name = fresh f in Buffer.add_string f.allocas (Printf.sprintf " %s = alloca %s\n" name (ll ty)); name (* For the few slots whose LLVM type is not a Flan type: a handler frame is the runtime's shape, not something [Types] can name. *) let alloca_raw f lltype = let name = fresh f in Buffer.add_string f.allocas (Printf.sprintf " %s = alloca %s\n" name lltype); name (* ── Constants ─────────────────────────────────────────────────────── *) (* LLVM's hex form is exact, which decimal is not: a literal must mean the same thing after a round trip through the .ll file. *) let float_const (k : Types.fkind) x = let x = match k with Types.F32 -> Int32.float_of_bits (Int32.bits_of_float x) | Types.F64 -> x in Printf.sprintf "0x%Lx" (Int64.bits_of_float x) let escape s = let b = Buffer.create (String.length s + 8) in String.iter (fun c -> if c = '"' || c = '\\' || Char.code c < 0x20 || Char.code c > 0x7e then Buffer.add_string b (Printf.sprintf "\\%02X" (Char.code c)) else Buffer.add_char b c) s; Buffer.contents b (* The constant itself, as the pointer and length a caller needs separately — a bounds message crosses to C as ptr+len like any other slice. *) let string_bytes m s = let id = Printf.sprintf "@\".str.%d\"" m.nstr in m.nstr <- m.nstr + 1; Buffer.add_string m.strs (Printf.sprintf "%s = private unnamed_addr constant [%d x i8] c\"%s\"\n" id (String.length s) (escape s)); id, String.length s let string_const m s = let id, n = string_bytes m s in (* The value alone: LLVM takes the type from the operand's context. *) Printf.sprintf "{ ptr %s, i64 %d }" id n (* A NUL-terminated copy, for the two dev lookups that take a C string. Flan strings are ptr+len and never NUL-terminated, so this is its own constant. *) let cstring m s = let id = Printf.sprintf "@\".name.%d\"" m.nstr in m.nstr <- m.nstr + 1; Buffer.add_string m.strs (Printf.sprintf "%s = private unnamed_addr constant [%d x i8] c\"%s\\00\"\n" id (String.length s + 1) (escape s)); id (* ── Bounds checks ───────────────────────────────────────────────────── *) (* A failure is a branch to a [noreturn] call and then [unreachable] — the same explicit shape as [return] and [some], so wasm32 needs no unwinding for it either. Whether to check is its own flag, not the optimisation level: dev builds trap, release builds do not (NEXT.md), and the acceptance table runs at both -O0 and -O2 with the checks on either way. Indices are i32 in Flan and sign-extended to i64 for the gep, so a negative one arrives here as a huge unsigned value: an unsigned comparison catches the negative and the too-large case in a single test. *) let fail_block f (loc : Loc.t) ok emit_call = let good = fresh_label f "inb" and bad = fresh_label f "oob" in term f "br i1 %s, label %%%s, label %%%s" ok good bad; label f bad; let id, n = string_bytes f.md (Loc.to_string loc) in emit_call id n; term f "unreachable"; label f good (* [at] is strict: the last valid index is len - 1. *) let check_at f loc idx len = if f.md.checks then begin let ok = fresh f in ins f "%s = icmp ult i64 %s, %s" ok idx len; fail_block f loc ok (fun id n -> ins f "call void @flan_bounds_fail(ptr %s, i64 %d, i64 %s, i64 %s)" id n idx len) end (* [slice] is not: a slice ending at len — or an empty one at lo = len — is legal, and its one-past-the-end gep is defined. [lo <= hi] is not redundant with it, because a reversed range would otherwise yield hi - lo as a huge unsigned length, which is a worse hole than the missing check. *) let check_slice f loc lo hi len = if f.md.checks then begin let a = fresh f in ins f "%s = icmp ule i64 %s, %s" a lo hi; let b = fresh f in ins f "%s = icmp ule i64 %s, %s" b hi len; let ok = fresh f in ins f "%s = and i1 %s, %s" ok a b; fail_block f loc ok (fun id n -> ins f "call void @flan_slice_fail(ptr %s, i64 %d, i64 %s, i64 %s, i64 %s)" id n lo hi len) end (* ── Expressions ───────────────────────────────────────────────────── *) let icmp_op signed = function | Tast.Eq -> "eq" | Tast.Ne -> "ne" | Tast.Lt -> if signed then "slt" else "ult" | Tast.Le -> if signed then "sle" else "ule" | Tast.Gt -> if signed then "sgt" else "ugt" | Tast.Ge -> if signed then "sge" else "uge" | _ -> assert false let fcmp_op = function | Tast.Eq -> "oeq" | Tast.Ne -> "one" | Tast.Lt -> "olt" | Tast.Le -> "ole" | Tast.Gt -> "ogt" | Tast.Ge -> "oge" | _ -> assert false (* Every [Tast] node already carries the position it was read from, and until now nothing wrote them out. The location is set for the duration of a node's own emission and restored afterwards, so instructions a parent emits *after* a child -- the branch at the end of an [if], the store of a [set] -- are attributed to the parent and not to whatever ran last inside it. *) let rec value f (e : Tast.expr) : string = match f.dsub with | None -> value_at f e | Some _ -> let saved = f.dloc in at_loc f e.Tast.loc; let v = value_at f e in f.dloc <- saved; v (* The [!DILocation] for a position, memoised: a loop body emits the same few lines over and over and each would otherwise make its own node. *) and at_loc f (loc : Loc.t) = match f.md.dbg, f.dsub with | Some d, Some sub -> (* Line 0 is [Loc.unknown] -- a node the checker made up rather than one anyone wrote. It is attributed to the function's own line instead, since a zero line in DWARF means "no line" and would make lldb step over the whole construct. *) let line = if loc.Loc.line = 0 then f.dline else loc.Loc.line in let key = Printf.sprintf "%d:%d:%d" sub line loc.Loc.col in let id = match Hashtbl.find_opt d.dlocs key with | Some id -> id | None -> let id = dnode d (Printf.sprintf "!DILocation(line: %d, column: %d, scope: !%d)" line loc.Loc.col sub) in Hashtbl.replace d.dlocs key id; id in f.dloc <- Printf.sprintf ", !dbg !%d" id | _ -> () and value_at f (e : Tast.expr) : string = match e.Tast.e with | Tast.Int (n, _) -> Int64.to_string n | Tast.Float (x, k) -> float_const k x | Tast.Bool b -> if b then "true" else "false" | Tast.Str s -> string_const f.md s | Tast.Unit | Tast.Zero _ | Tast.None_ -> "zeroinitializer" | Tast.Uninit _ -> "poison" | Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ -> (* Everything that denotes a location is a load from its address. *) load f (addr f e) e.Tast.ty | Tast.Addr p -> fst (place f p) | Tast.Prim (p, args) -> prim f e p args | Tast.Call (name, args) -> (match Hashtbl.find_opt f.md.externs name with | Some sym -> extern_call f e.Tast.ty ("@" ^ sym) args | None -> call f e.Tast.ty name args) | Tast.Do body -> block f body | Tast.Let (bs, body) -> List.iter (fun (slot, v) -> let v' = value f v in ins f "store %s %s, ptr %s" (ll v.Tast.ty) v' f.slots.(slot)) bs; block f body | Tast.If (c, t, e') -> emit_if f e.Tast.ty c t e' | Tast.While (c, body) -> emit_while f c body; "zeroinitializer" | Tast.Return v -> (match v with | None -> term f "ret %s zeroinitializer" (ll f.ret) | Some v -> let v' = value f v in term f "ret %s %s" (ll f.ret) v'); "zeroinitializer" | Tast.Set (p, v) -> let ptr, ty = place f p in let v' = value f v in ins f "store %s %s, ptr %s" (ll ty) v' ptr; "zeroinitializer" | Tast.Make (_, fields) -> aggregate f e.Tast.ty fields | Tast.Arr items -> aggregate f e.Tast.ty items | Tast.Some_ v -> let v' = value f v in let t = ll e.Tast.ty in let a = fresh f in ins f "%s = insertvalue %s zeroinitializer, i8 1, 0" a t; let b = fresh f in ins f "%s = insertvalue %s %s, %s %s, 1" b t a (ll v.Tast.ty) v'; b | Tast.Match (s, arms) -> emit_match f e.Tast.ty s arms | Tast.UnwrapSome v -> emit_unwrap f e.Tast.ty v (* The condition crosses as a pointer: a handler runs while the signalling frame is still alive, so there is nothing to copy and nothing to own. *) | Tast.Signal (Tast.Ssignal, id, c) -> let p = addr f c in ins f "call void @flan_signal(i32 %d, ptr %s, ptr %s)" id p xfer_param; guard f; "zeroinitializer" (* §2's diverging variant. [flan_error] does not return unless a handler transferred, so the guard is the only way out and the fall-through is unreachable. It cannot be marked noreturn for that reason — it does return, on exactly one path. *) | Tast.Signal (Tast.Serror, id, c) -> let p = addr f c in let name = struct_name_of c.Tast.ty in let nid, nn = string_bytes f.md name in ins f "call void @flan_error(i32 %d, ptr %s, ptr %s, ptr %s, i64 %d)" id p xfer_param nid nn; guard f; term f "unreachable"; "zeroinitializer" | Tast.Handled (frames, body) -> emit_handled f frames body | Tast.RestartCase (clauses, body) -> emit_restart_case f e.Tast.ty clauses body (* §4's lookup, then the transfer itself: the frame that was found goes into the channel and this function leaves through its landing block. Type Never, so nothing follows. *) | Tast.InvokeRestart (id, name, rloc) -> let t = fresh f in ins f "%s = call ptr @flan_find_restart(i32 %d)" t id; let ok = fresh f in ins f "%s = icmp ne ptr %s, null" ok t; (* No frame offers the name. That is a runtime error at the invoke site — not an unwind past everything — because there is nowhere to resume. *) fail_block f rloc ok (fun id n -> let nid, nn = string_bytes f.md name in ins f "call void @flan_restart_fail(ptr %s, i64 %d, ptr %s, i64 %d)" id n nid nn); ins f "store ptr %s, ptr %s" t xfer_param; term f "br label %%%s" (current_pad f); "zeroinitializer" (* Where a global's storage is. A global the host was built with is a symbol; one introduced since lives wherever [flan_dev_global] put it. *) and global_addr f n = if (not f.md.dev) || f.md.known n then gname n else begin let p = fresh f in ins f "%s = load ptr, ptr %s" p (globalptr n); p end and load f ptr ty = let t = fresh f in ins f "%s = load %s, ptr %s" t (ll ty) ptr; t (* The address of an expression that denotes a location. Anything else is spilled to a temporary first, so [(at (f) 0)] on a returned array works. *) and addr f (e : Tast.expr) : string = match e.Tast.e with | Tast.Local i -> f.slots.(i) | Tast.Global n -> global_addr f n | Tast.Deref p -> value f p | Tast.Field (target, i) -> field_addr f target i | Tast.Prim (Tast.At, target :: idx) -> fst (element_addr f target idx) | _ -> let tmp = alloca f e.Tast.ty in let v = value f e in ins f "store %s %s, ptr %s" (ll e.Tast.ty) v tmp; tmp and field_addr f (target : Tast.expr) i = let base = addr f target in (* An Option is { i8, T } and has no declared name to gep through, so its layout is spelled out instead. Nothing in the surface language reaches a field of one -- [match] and [some] are how an Option is opened -- but the structural printer does, to read the tag without unwrapping a None. *) let sty = match target.Tast.ty with | Types.Named n -> sname n | Types.Option _ as t -> ll t | t -> failwith ("field of " ^ Types.to_string t) in let p = fresh f in ins f "%s = getelementptr inbounds %s, ptr %s, i32 0, i32 %d" p sty base i; p (* One index per dimension, so [(at grid row col)] is two geps. Indices are i32 in Flan and i64 in a gep. *) and element_addr f (target : Tast.expr) idx = let rec go ptr ty = function | [] -> ptr, ty | (i : Tast.expr) :: rest -> let iv = value f i in let i64 = fresh f in ins f "%s = sext %s %s to i64" i64 (ll i.Tast.ty) iv; (match ty with | Types.Array (n, elem) -> (* The bound is static; LLVM folds the check away for a literal index. *) check_at f i.Tast.loc i64 (Int64.to_string n); let p = fresh f in ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s" p (ll ty) ptr i64; go p elem rest | Types.Slice elem -> (* A slice is ptr+len, so step through the pointer it holds. *) let s = load f ptr ty in let base = fresh f in ins f "%s = extractvalue %%slice %s, 0" base s; let len = fresh f in ins f "%s = extractvalue %%slice %s, 1" len s; check_at f i.Tast.loc i64 len; let p = fresh f in ins f "%s = getelementptr inbounds %s, ptr %s, i64 %s" p (ll elem) base i64; go p elem rest | t -> failwith ("index into " ^ Types.to_string t)) in go (addr f target) target.Tast.ty idx and place f (p : Tast.place) : string * Types.t = match p with | Tast.Plocal i -> f.slots.(i), f.slot_tys.(i) | Tast.Pglobal n -> global_addr f n, Hashtbl.find f.md.globals n | Tast.Pfield (target, i) -> let sn = match target.Tast.ty with | Types.Named n -> n | t -> failwith ("field of " ^ Types.to_string t) in field_addr f target i, field_ty f.md sn i | Tast.Pindex (target, idx) -> element_addr f target idx | Tast.Pderef target -> let t = match target.Tast.ty with | Types.Ptr t -> t | t -> failwith ("deref of " ^ Types.to_string t) in value f target, t (* A struct or fixed-array value, built field by field from zeroinitializer. The checker already filled the omitted fields in with Zero, so this is simply every field in declaration order. *) and aggregate f ty parts = let t = ll ty in let acc = ref "zeroinitializer" in List.iteri (fun i (p : Tast.expr) -> let v = value f p in let next = fresh f in ins f "%s = insertvalue %s %s, %s %s, %d" next t !acc (ll p.Tast.ty) v i; acc := next) parts; !acc and block f body = match body with | [] -> "zeroinitializer" | _ -> let last = ref "zeroinitializer" in List.iter (fun e -> last := value f e) body; !last and call f ret flan args = let vs = map_lr (fun (a : Tast.expr) -> let v = value f a in Printf.sprintf "%s %s" (ll a.Tast.ty) v) args in (* The cell is loaded *after* the arguments, so a redefinition that lands between two calls still cannot land in the middle of one. *) let callee = if not f.md.dev then fname flan else if f.md.known flan then begin let p = fresh f in ins f "%s = load ptr, ptr %s" p (cellname flan); p end else begin (* The cell itself is not a symbol here; its address was looked up by name at install time and cached. *) let c = fresh f in ins f "%s = load ptr, ptr %s" c (cellptr flan); let p = fresh f in ins f "%s = load ptr, ptr %s" p c; p end in let t = fresh f in ins f "%s = call %s %s(%s)" t (ll ret) callee (String.concat ", " (vs @ [ "ptr " ^ xfer_param ])); guard f; t (* The check after a call, which is the whole of §6's lowering at a call site: a load, a compare and a branch that reads like ordinary code. A foreign call gets none — a transfer cannot cross a C frame, so there is nothing a guard there could find. *) and guard f = if f.live then begin let t = fresh f in ins f "%s = load ptr, ptr %s" t xfer_param; let c = fresh f in ins f "%s = icmp ne ptr %s, null" c t; let cont = fresh_label f "on" in let pad = current_pad f in term f "br i1 %s, label %%%s, label %%%s" c pad cont; label f cont end and current_pad f = match f.pads with | (p, used) :: _ -> used := true; p | [] -> f.unwound <- true; f.unwind (* A foreign call, where the same rule applies as to the runtime shims: a slice or a string crosses as ptr+len and never as a struct by value. Every other argument type is a scalar, because [check.ml] rejects an extern signature that would need an aggregate — that is the shim's job, in C, where clang knows the target's calling convention. *) and extern_call f ret name args = let vs = List.concat_map (fun (a : Tast.expr) -> match a.Tast.ty with | Types.String | Types.Slice _ -> let p, n = explode f a in [ Printf.sprintf "ptr %s" p; Printf.sprintf "i64 %s" n ] | ty -> [ Printf.sprintf "%s %s" (ll ty) (value f a) ]) args in if is_void ret then begin ins f "call void %s(%s)" name (String.concat ", " vs); "zeroinitializer" end else begin let t = fresh f in ins f "%s = call %s %s(%s)" t (ll ret) name (String.concat ", " vs); t end (* Establishing a handler is two stores and a push, per spec-conditions.md §2, and the frame lives on this function's own stack. Popping is by frame rather than by count: restoring what this one displaced is right even if something below it left the stack out of step. The body may not [return] — the checker rejects that — so the pops here are on the only path out. *) and emit_handled f frames body = let allocated = List.map (fun (h : Tast.hframe) -> let slot = alloca_raw f "%handler" in let ty = fresh f in ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 1" ty slot; ins f "store i32 %d, ptr %s" h.Tast.htype ty; let fp = fresh f in ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 2" fp slot; (* The clause's body address, deliberately, and not a cell load: plan.org makes a top-level function value a stable trampoline over its cell, but a handler frame is not one — nothing can name it, and it lives only for this body. A reload landing while it is on the stack finds what it pushed still valid, which is what "old code is never unloaded" means. See NEXT.md, conditions step 1. *) ins f "store ptr %s, ptr %s" (fname h.Tast.hfn) fp; ins f "call void @flan_handler_push(ptr %s)" slot; slot) frames in let pop () = (* Innermost first, which is the order they were pushed in reverse. *) List.iter (fun slot -> ins f "call void @flan_handler_pop(ptr %s)" slot) (List.rev allocated) in let ld = fresh_label f "endhandled" in let pad = fresh_label f "hxfer" and used = ref false in f.pads <- (pad, used) :: f.pads; let last = block f body in f.pads <- List.tl f.pads; ignore last; let reached = f.live in if f.live then begin pop (); term f "br label %%%s" ld end; (* A transfer passing through: these frames are on the establishing function's stack and must come off before it goes any further, and this is the only path out that the checker's refusal of [return] leaves. Nothing here calls Flan, so the channel can stay as it is. *) if !used then begin label f pad; pop (); term f "br label %%%s" (current_pad f) end; if not reached then begin f.live <- false; "zeroinitializer" end else begin label f ld; "zeroinitializer" end (* (restart-case BODY (name [] BODY-1) ...) — §3, §4 and §6 together. One frame per clause, so that the frame a transfer names says which clause to run: the address is the identity, which is exact where a number would have to be unique against every module the running program might later load. §4's "innermost offering the name" falls out of the stack walk, and re-entering a restart-case works because each activation allocates its own frames. §5's defers between here and the invoke have already run — each function on the way out ran its own before returning. What is left here is to take these frames off and start the clause. *) and emit_restart_case f ty clauses body = let result = if is_void ty then None else Some (alloca f ty) in let frames = map_lr (fun (c : Tast.rclause) -> let slot = alloca_raw f "%restart" in let nid = fresh f in ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 1" nid slot; ins f "store i32 %d, ptr %s" c.Tast.rname_id nid; (* The name itself, beside the hash. A hash is all that matching needs, but a break loop has to *show* someone their choices, and nothing at run time can turn a hash back into a name. *) let sid, slen = string_bytes f.md c.Tast.rname in let np = fresh f in ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 2" np slot; ins f "store ptr %s, ptr %s" sid np; let nl = fresh f in ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 3" nl slot; ins f "store i64 %d, ptr %s" slen nl; ins f "call void @flan_restart_push(ptr %s)" slot; slot) clauses in let pop () = List.iter (fun slot -> ins f "call void @flan_restart_pop(ptr %s)" slot) (List.rev frames) in let ld = fresh_label f "endrestart" in let pad = fresh_label f "rxfer" and used = ref false in let reached = ref false in let yield v = if f.live then begin (match result with | Some r -> ins f "store %s %s, ptr %s" (ll ty) v r | None -> ()); reached := true; term f "br label %%%s" ld end in f.pads <- (pad, used) :: f.pads; let v = value f body in f.pads <- List.tl f.pads; if f.live then pop (); yield v; label f pad; let tgt = fresh f in ins f "%s = load ptr, ptr %s" tgt xfer_param; (* Cleared before the clause runs, and put back if this transfer turns out to be aimed further out. A clause body is ordinary code and its calls are guarded like any other; it must not start with the channel still set. *) ins f "store ptr null, ptr %s" xfer_param; pop (); let rec dispatch = function | [] -> ins f "store ptr %s, ptr %s" tgt xfer_param; term f "br label %%%s" (current_pad f) | (slot, (c : Tast.rclause)) :: rest -> let hit = fresh_label f "restart" and next = fresh_label f "outer" in let t = fresh f in ins f "%s = icmp eq ptr %s, %s" t tgt slot; term f "br i1 %s, label %%%s, label %%%s" t hit next; label f hit; yield (block f c.Tast.rbody); label f next; dispatch rest in dispatch (List.combine frames clauses); if not !reached then begin f.live <- false; "zeroinitializer" end else begin label f ld; match result with Some r -> load f r ty | None -> "zeroinitializer" end and emit_if f ty c t e = let cv = value f c in let lt = fresh_label f "then" and le = fresh_label f "else" and ld = fresh_label f "endif" in let result = if is_void ty then None else Some (alloca f ty) in term f "br i1 %s, label %%%s, label %%%s" cv lt le; let arm lbl (branch : Tast.expr) = label f lbl; let v = value f branch in (match result with | Some r when f.live -> ins f "store %s %s, ptr %s" (ll ty) v r | _ -> ()); let reached = f.live in term f "br label %%%s" ld; reached in let a = arm lt t in let b = arm le e in if not (a || b) then begin (* Both branches diverge, so there is no join: nothing follows. *) f.live <- false; "zeroinitializer" end else begin label f ld; match result with Some r -> load f r ty | None -> "zeroinitializer" end and emit_while f c body = let lc = fresh_label f "loop" and lb = fresh_label f "body" and le = fresh_label f "endloop" in term f "br label %%%s" lc; label f lc; let cv = value f c in term f "br i1 %s, label %%%s, label %%%s" cv lb le; label f lb; List.iter (fun e -> ignore (value f e)) body; term f "br label %%%s" lc; label f le and emit_match f ty scrut arms = let sv = value f scrut in let sty = ll scrut.Tast.ty in let tag = fresh f in ins f "%s = extractvalue %s %s, 0" tag sty sv; let payload_ty = match scrut.Tast.ty with | Types.Option t -> t | t -> failwith ("match on " ^ Types.to_string t) in let ld = fresh_label f "endmatch" in let result = if is_void ty then None else Some (alloca f ty) in let reached = ref false in let rec go = function | [] -> term f "unreachable" (* the checker proved exhaustiveness *) | (a : Tast.arm) :: rest -> let lb = fresh_label f "arm" and ln = fresh_label f "next" in (match a.Tast.acase with | None -> term f "br label %%%s" lb | Some c -> let want = if c = "Some" then 1 else 0 in let t = fresh f in ins f "%s = icmp eq i8 %s, %d" t tag want; term f "br i1 %s, label %%%s, label %%%s" t lb ln); label f lb; List.iter (fun slot -> let v = fresh f in ins f "%s = extractvalue %s %s, 1" v sty sv; ins f "store %s %s, ptr %s" (ll payload_ty) v f.slots.(slot)) a.Tast.binds; let v = block f a.Tast.abody in (match result with | Some r when f.live -> ins f "store %s %s, ptr %s" (ll ty) v r | _ -> ()); if f.live then reached := true; term f "br label %%%s" ld; if a.Tast.acase <> None then begin label f ln; go rest end in go arms; if not !reached then begin f.live <- false; "zeroinitializer" end else begin label f ld; match result with Some r -> load f r ty | None -> "zeroinitializer" end (* (some x): unwrap Some, else return None from the enclosing function. The early return is explicit — a branch to a ret, not platform unwinding, so native and wasm32 do the same thing (plan.org, Compilation). *) and emit_unwrap f ty v = let ov = value f v in let oty = ll v.Tast.ty in let tag = fresh f in ins f "%s = extractvalue %s %s, 0" tag oty ov; let isnone = fresh f in ins f "%s = icmp eq i8 %s, 0" isnone tag; let ln = fresh_label f "none" and lc = fresh_label f "some" in term f "br i1 %s, label %%%s, label %%%s" isnone ln lc; label f ln; term f "ret %s zeroinitializer" (ll f.ret); label f lc; let out = fresh f in ins f "%s = extractvalue %s %s, 1" out oty ov; ignore ty; out (* ── Primitives ────────────────────────────────────────────────────── *) and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = match p, args with | (Tast.Add | Tast.Sub | Tast.Mul | Tast.Div | Tast.Rem), [ x; y ] -> let a = value f x in let b = value f y in let op = match x.Tast.ty, p with | Types.Float _, Tast.Add -> "fadd" | Types.Float _, Tast.Sub -> "fsub" | Types.Float _, Tast.Mul -> "fmul" | Types.Float _, Tast.Div -> "fdiv" | Types.Float _, _ -> "frem" | Types.Int _, Tast.Add -> "add" | Types.Int _, Tast.Sub -> "sub" | Types.Int _, Tast.Mul -> "mul" | Types.Int k, Tast.Div -> if Types.signed k then "sdiv" else "udiv" | Types.Int k, _ -> if Types.signed k then "srem" else "urem" | t, _ -> failwith ("arithmetic on " ^ Types.to_string t) in let t = fresh f in (* No nsw/nuw: arithmetic wraps (plan.org, Types). *) ins f "%s = %s %s %s, %s" t op (ll x.Tast.ty) a b; t | (Tast.Eq | Tast.Ne | Tast.Lt | Tast.Le | Tast.Gt | Tast.Ge), [ x; y ] -> let a = value f x in let b = value f y in let t = fresh f in (match x.Tast.ty with | Types.Float _ -> ins f "%s = fcmp %s %s %s, %s" t (fcmp_op p) (ll x.Tast.ty) a b | Types.Int k -> ins f "%s = icmp %s %s %s, %s" t (icmp_op (Types.signed k) p) (ll x.Tast.ty) a b (* An enum is an i32 at run time, and [Types.is_comparable] says so by admitting one — the checker was stating an intent the backend never honoured, so [(= k :a)] type checked and then died here with no source location. Signed, because a member may be declared negative. *) | Types.Enum _ -> ins f "%s = icmp %s %s %s, %s" t (icmp_op true p) (ll x.Tast.ty) a b | t' -> failwith ("comparison on " ^ Types.to_string t')); t | (Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr), [ x; y ] -> let a = value f x in let b = value f y in let op = match x.Tast.ty, p with | _, Tast.BitAnd -> "and" | _, Tast.BitOr -> "or" | _, Tast.BitXor -> "xor" | _, Tast.Shl -> "shl" | Types.Int k, _ -> if Types.signed k then "ashr" else "lshr" | t, _ -> failwith ("bitwise on " ^ Types.to_string t) in (* The count is masked to the operand's width. LLVM makes an over-wide shift poison, and a poison return at -O2 is a function that returns nothing at all; masking is what the hardware does anyway, and LLVM folds the [and] away whenever the count is a constant. [check] has already rejected a literal that is out of range, so this only ever fires on a computed count. *) let b = match x.Tast.ty, p with | Types.Int k, (Tast.Shl | Tast.Shr) -> let m = fresh f in ins f "%s = and %s %s, %d" m (ll x.Tast.ty) b (Types.bits k - 1); m | _ -> b in let t = fresh f in ins f "%s = %s %s %s, %s" t op (ll x.Tast.ty) a b; t | Tast.Not, [ x ] -> let a = value f x in let t = fresh f in ins f "%s = xor i1 %s, true" t a; t | Tast.Len, [ x ] -> (match x.Tast.ty with | Types.Array (n, _) -> Int64.to_string n | _ -> let v = value f x in let n = fresh f in ins f "%s = extractvalue %%slice %s, 1" n v; let t = fresh f in ins f "%s = trunc i64 %s to i32" t n; t) | Tast.At, target :: idx -> let p, elem = element_addr f target idx in load f p elem | Tast.Slice, [ target; lo; hi ] -> (* lo is evaluated once and used twice — as the offset and as part of the length — so it must not be emitted twice. *) let lov = value f lo in let hiv = value f hi in let lo64 = fresh f in ins f "%s = sext i32 %s to i64" lo64 lov; let hi64 = fresh f in ins f "%s = sext i32 %s to i64" hi64 hiv; (* The source is read once, and the check goes between reading it and the gep: the length it is checked against must be the one the gep uses. *) let base = match target.Tast.ty with | Types.Array (n, _) -> let a = addr f target in check_slice f e.Tast.loc lo64 hi64 (Int64.to_string n); let p = fresh f in ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s" p (ll target.Tast.ty) a lo64; p | Types.Slice elem -> let v = value f target in let q = fresh f in ins f "%s = extractvalue %%slice %s, 0" q v; let n = fresh f in ins f "%s = extractvalue %%slice %s, 1" n v; check_slice f e.Tast.loc lo64 hi64 n; let p = fresh f in ins f "%s = getelementptr inbounds %s, ptr %s, i64 %s" p (ll elem) q lo64; p | Types.String -> let v = value f target in let q = fresh f in ins f "%s = extractvalue %%slice %s, 0" q v; let n = fresh f in ins f "%s = extractvalue %%slice %s, 1" n v; check_slice f e.Tast.loc lo64 hi64 n; let p = fresh f in ins f "%s = getelementptr inbounds i8, ptr %s, i64 %s" p q lo64; p | t -> failwith ("slice of " ^ Types.to_string t) in let d = fresh f in ins f "%s = sub i64 %s, %s" d hi64 lo64; let a = fresh f in ins f "%s = insertvalue %%slice zeroinitializer, ptr %s, 0" a base; let b = fresh f in ins f "%s = insertvalue %%slice %s, i64 %s, 1" b a d; b (* string and [u8] have the same layout, so bytes is the identity — a view, no copy (plan.org, Milestone-2 primitives). *) | Tast.Bytes, [ x ] -> value f x | Tast.BytesToF64, [ x ] -> shim_in f "@flan_bytes_to_f64" "double" x | Tast.BytesToI64, [ x ] -> shim_in f "@flan_bytes_to_i64" "i64" x | Tast.F64ToBytes, [ x ] -> shim_out f "@flan_f64_to_bytes" x | Tast.I64ToBytes, [ x ] -> shim_out f "@flan_i64_to_bytes" x | Tast.U64ToBytes, [ x ] -> shim_out f "@flan_u64_to_bytes" x | Tast.EscapeBytes, [ x ] -> shim_in_out f "@flan_escape_bytes" x | Tast.WriteStdout, [ x ] -> let p, n = explode f x in ins f "call void @flan_write_stdout(ptr %s, i64 %s)" p n; "zeroinitializer" | Tast.Exit, [ x ] -> let v = value f x in ins f "call void @flan_exit(i32 %s)" v; term f "unreachable"; "zeroinitializer" | Tast.Argv, [] -> let tmp = alloca f (Types.Slice Types.String) in ins f "call void @flan_argv(ptr %s)" tmp; load f tmp (Types.Slice Types.String) | Tast.Cast target, [ x ] -> cast f x target | _ -> failwith "malformed primitive" (* A slice argument crosses to C as ptr+len, never as a struct by value. *) and explode f (x : Tast.expr) = let v = value f x in let p = fresh f in ins f "%s = extractvalue %%slice %s, 0" p v; let n = fresh f in ins f "%s = extractvalue %%slice %s, 1" n v; p, n and shim_in f name ret x = let p, n = explode f x in let t = fresh f in ins f "%s = call %s %s(ptr %s, i64 %s)" t ret name p n; t and shim_out f name (x : Tast.expr) = let v = value f x in let tmp = alloca f (Types.Slice (Types.Int Types.U8)) in ins f "call void %s(%s %s, ptr %s)" name (ll x.Tast.ty) v tmp; load f tmp (Types.Slice (Types.Int Types.U8)) (* Slice in, slice out: [shim_in] returns a scalar and [shim_out] takes one, so a shim that transforms bytes into bytes is neither. *) and shim_in_out f name (x : Tast.expr) = let p, n = explode f x in let tmp = alloca f (Types.Slice (Types.Int Types.U8)) in ins f "call void %s(ptr %s, i64 %s, ptr %s)" name p n tmp; load f tmp (Types.Slice (Types.Int Types.U8)) and cast f (x : Tast.expr) target = let v = value f x in (* An enum is an i32 at run time and its own type only in the checker, so a cast involving one is a cast on that i32. Nothing in the surface language produces this — a keyword resolves against the enum and never widens — but the REPL's renderer needs an enum's number when it falls outside the declared members. *) let concrete (t : Types.t) = match t with Types.Enum _ -> Types.Int Types.I32 | t -> t in let src = concrete x.Tast.ty and target = concrete target in if Types.equal src target then v else let op = match src, target with | Types.Int a, Types.Int b -> if Types.bits b < Types.bits a then "trunc" else if Types.bits b = Types.bits a then "bitcast" else if Types.signed a then "sext" else "zext" | Types.Int a, Types.Float _ -> if Types.signed a then "sitofp" else "uitofp" | Types.Float _, Types.Int b -> if Types.signed b then "fptosi" else "fptoui" | Types.Float a, Types.Float b -> if Types.bits_f b > Types.bits_f a then "fpext" else "fptrunc" | _ -> failwith "unsupported cast" in if op = "bitcast" then v else begin let t = fresh f in ins f "%s = %s %s %s to %s" t op (ll src) v (ll target); t end (* ── Functions ─────────────────────────────────────────────────────── *) (* The one place a Flan function's LLVM signature is spelled. A [define] and the [declare] a redefinition module needs for the same function have to agree exactly, and the way they stop agreeing is one of them growing a case for Unit or for a slice parameter that the other never gets. *) let signature ~named (fn : Tast.fn) = let params = List.mapi (fun i ty -> if named then Printf.sprintf "%s %%p%d" (ll ty) i else ll ty) fn.Tast.params in (* The transfer channel, spec-conditions.md §6: one [ptr] appended to every signature, written by an [invoke-restart] and checked after every call. Uniform rather than only on the functions that need it — the spec's escape analysis is an optimisation, and in a dev build a cell can hold anything, so the honest answer to "what can this call?" is "anything". *) let params = params @ [ (if named then "ptr " ^ xfer_param else "ptr") ] in Printf.sprintf "%s %s(%s)" (ll fn.Tast.ret) (fname fn.Tast.name) (String.concat ", " params) (* [hidden] on a redefinition's own body, and this is load-bearing. Default visibility in a shared object is interposable: [@"flan.bump"] inside the module would resolve to the *host's* copy, so the installer would publish the function it was replacing and the reload would appear to do nothing. *) (* The name a slot goes into the debug info under. The typed IR refers to locals by index and nothing records what they were called -- [Check] knows, in its scope list, and drops it. So a parameter gets the name the source gave it, recovered by the driver and handed down in [pnames], and everything else gets [s], which is the slot it actually is. A [let]-bound local printing as [s4] is a real gap and it is named here rather than papered over: fixing it means the typed IR carrying the name, which is a change to [Tast]. *) let slot_name ~pnames ~nparams i = if i < nparams then match List.nth_opt pnames i with | Some n when n <> "" -> n | _ -> Printf.sprintf "p%d" i else Printf.sprintf "s%d" i let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) = let n = Array.length fn.Tast.slots in (* The subprogram's id is claimed before the body is emitted, because every instruction in the body refers to it, and the node itself is written at the end once the retained variables are known. *) let dsub = match m.dbg with None -> None | Some d -> Some (dalloc d) in let f = { md = m; allocas = Buffer.create 256; b = Buffer.create 1024; n = 0; live = true; ret = fn.Tast.ret; slots = Array.init n (fun i -> Printf.sprintf "%%s%d" i); slot_tys = fn.Tast.slots; pads = []; unwind = "unwind"; unwound = false; defers = fn.Tast.fdefers; dsub; dline = (if fn.Tast.floc.Loc.line = 0 then 1 else fn.Tast.floc.Loc.line); dloc = ""; } in (* Every slot is an alloca in the entry block, because [addr] may take the address of any of them and mem2reg only promotes entry-block allocas. *) Array.iteri (fun i ty -> Buffer.add_string f.allocas (Printf.sprintf " %s = alloca %s\n" f.slots.(i) (ll ty))) fn.Tast.slots; (* Parameters arrive as SSA values and are stored into their slots at once, which is also the copy a value struct gets on assignment. *) List.iteri (fun i ty -> Buffer.add_string f.allocas (Printf.sprintf " store %s %%p%d, ptr %s\n" (ll ty) i f.slots.(i))) fn.Tast.params; (* One [llvm.dbg.declare] per slot, in the entry block beside the alloca it describes. This is the whole of what lldb needs to print a local: the slot is ordinary stack storage of an ordinary machine type, so there is no accessor to describe and no header to skip. *) (match m.dbg, dsub with | Some d, Some sub -> let file = dfile d fn.Tast.floc.Loc.file in let nparams = List.length fn.Tast.params in let vars = Array.to_list (Array.mapi (fun i ty -> let arg = (* [arg:] is 1-based over the LLVM formals, and the transfer channel is appended after all of them, so a parameter's index is its Flan index either way. The channel itself gets no variable: nothing in the language can name it. *) if i < nparams then Printf.sprintf ", arg: %d" (i + 1) else "" in dnode d (Printf.sprintf "!DILocalVariable(name: \"%s\"%s, scope: !%d, file: !%d, line: %d, type: !%d)" (dstr (slot_name ~pnames ~nparams i)) arg sub file f.dline (dty m d ty))) fn.Tast.slots) in let dl = dnode d (Printf.sprintf "!DILocation(line: %d, column: 1, scope: !%d)" f.dline sub) in List.iteri (fun i v -> Buffer.add_string f.allocas (Printf.sprintf " call void @llvm.dbg.declare(metadata ptr %s, metadata !%d, metadata !DIExpression()), !dbg !%d\n" f.slots.(i) v dl)) vars; let sty = dnode d (Printf.sprintf "!DISubroutineType(types: !{%s})" (String.concat ", " ((if is_void fn.Tast.ret then "null" else Printf.sprintf "!%d" (dty m d fn.Tast.ret)) :: List.map (fun t -> Printf.sprintf "!%d" (dty m d t)) fn.Tast.params))) in dput d sub (Printf.sprintf "distinct !DISubprogram(name: \"%s\", linkageName: \"flan.%s\", scope: !%d, file: !%d, line: %d, type: !%d, scopeLine: %d, spFlags: DISPFlagDefinition, flags: DIFlagPrototyped, unit: !%d, retainedNodes: !{%s})" (dstr fn.Tast.name) (dstr fn.Tast.name) file file f.dline sty f.dline d.dcu (String.concat ", " (List.map (fun v -> Printf.sprintf "!%d" v) vars))); at_loc f fn.Tast.floc | _ -> ()); let last = ref "zeroinitializer" in List.iter (fun e -> last := value f e) fn.Tast.body; (* A Unit function's body may end on a form of any type — the value is discarded, so the return is the Unit constant rather than that value. *) if Types.equal fn.Tast.ret Types.Unit then last := "zeroinitializer"; term f "ret %s %s" (ll fn.Tast.ret) !last; (* The transfer exit, spec-conditions.md §5 and §6. A transfer that reached the top of this function without a restart-case to catch it leaves the same way a [return] does — which is what reuses the existing return path, and with it the defers, for free. The value returned is meaningless: the caller's guard sees the channel set and never looks at it. *) if f.unwound then begin label f f.unwind; let cleanup = "unwind.cleanup" and used = ref false in if f.defers <> [] then begin (* The channel is cleared while the defers run and put back after. A defer makes ordinary calls and each one is guarded; with the channel still set the first of them would branch straight back here. *) let tgt = fresh f in ins f "%s = load ptr, ptr %s" tgt xfer_param; ins f "store ptr null, ptr %s" xfer_param; f.pads <- [ (cleanup, used) ]; List.iter (fun e -> ignore (value f e)) f.defers; f.pads <- []; ins f "store ptr %s, ptr %s" tgt xfer_param end; term f "ret %s zeroinitializer" (ll fn.Tast.ret); (* A defer that starts a *second* transfer while the first is unwinding. §6's per-frame slot nests, but nothing here does: the first transfer's target is in hand and the defers are half run. Refused loudly rather than resolved to one of them. *) if !used then begin label f cleanup; let id, n = string_bytes f.md (Loc.to_string fn.Tast.floc) in ins f "call void @flan_transfer_fail(ptr %s, i64 %d)" id n; term f "unreachable" end end; Buffer.add_string m.out (Printf.sprintf "\ndefine %s%s%s {\nentry:\n%s%s}\n" (if hidden then "hidden " else "") (signature ~named:true fn) (match dsub with None -> "" | Some n -> Printf.sprintf " !dbg !%d" n) (Buffer.contents f.allocas) (Buffer.contents f.b)) (* ── Globals ───────────────────────────────────────────────────────── *) (* A global's initialiser is a compile-time constant: literals live in read-only memory and zeroed globals live in BSS and cost nothing to start (plan.org, Data model). There is no init-at-startup path, by design. *) let rec const m (e : Tast.expr) = match e.Tast.e with | Tast.Int (n, _) -> Int64.to_string n | Tast.Float (x, k) -> float_const k x | Tast.Bool b -> if b then "true" else "false" | Tast.Str s -> string_const m s | Tast.Unit | Tast.Zero _ | Tast.None_ -> "zeroinitializer" | Tast.Uninit _ -> "poison" | Tast.Make (_, parts) | Tast.Arr parts -> let inner = map_lr (fun (p : Tast.expr) -> Printf.sprintf "%s %s" (ll p.Tast.ty) (const m p)) parts in (match e.Tast.ty with | Types.Array _ -> "[" ^ String.concat ", " inner ^ "]" | _ -> "{ " ^ String.concat ", " inner ^ " }") | Tast.Some_ v -> Printf.sprintf "{ i8 1, %s %s }" (ll v.Tast.ty) (const m v) | _ -> fail e.Tast.loc "a global's value must be a compile-time constant — this one is computed" (* A dev build emits a [defconst] as a mutable [global]. Two things follow, and both are wanted: LLVM can no longer fold a read of it, and a redefinition module can store a new value into it — so tuning a constant live works, which it cannot when its only copy is immutable in .rodata. A release build emits [constant] and gets all the folding back. *) let emit_global m (g : Tast.global) = Buffer.add_string m.out (Printf.sprintf "%s = %s %s %s\n" (gname g.Tast.gname) (if g.Tast.gconst && not m.dev then "constant" else "global") (ll g.Tast.gty) (const m g.Tast.ginit)) (* ── Program ───────────────────────────────────────────────────────── *) let header = {|; Generated by flan. The layout is C's: no object headers anywhere, ; so a Flan struct is exactly its C struct and nothing marshals. %slice = type { ptr, i64 } ; A handler frame: the one it displaced, the condition type it matches, and ; the lifted function that runs. Allocated on the establishing frame's stack. %handler = type { ptr, i32, ptr } ; A restart frame: the one it displaced and the name it offers. There is no ; target field, because the frame's own address *is* the target — which makes ; a transfer's aim exact, and makes re-entering a restart-case work with ; nothing extra, since each activation allocates its own. %restart = type { ptr, i32, ptr, i64 } declare void @flan_rt_init(i32, ptr) declare void @flan_argv(ptr) declare void @flan_write_stdout(ptr, i64) declare void @flan_exit(i32) declare double @flan_bytes_to_f64(ptr, i64) declare i64 @flan_bytes_to_i64(ptr, i64) declare void @flan_f64_to_bytes(double, ptr) declare void @flan_i64_to_bytes(i64, ptr) declare void @flan_u64_to_bytes(i64, ptr) declare void @flan_escape_bytes(ptr, i64, ptr) declare void @flan_handler_push(ptr) declare void @flan_handler_pop(ptr) declare void @flan_signal(i32, ptr, ptr) declare void @flan_error(i32, ptr, ptr, ptr, i64) declare void @flan_restart_push(ptr) declare void @flan_restart_pop(ptr) declare ptr @flan_find_restart(i32) declare void @flan_restart_fail(ptr, i64, ptr, i64) noreturn cold declare void @flan_transfer_fail(ptr, i64) noreturn cold declare void @flan_bounds_fail(ptr, i64, i64, i64) noreturn cold declare void @flan_slice_fail(ptr, i64, i64, i64, i64) noreturn cold |} (* 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 (fn : Tast.fn) = let b = Buffer.create 256 in Buffer.add_string b "\ndefine i32 @main(i32 %argc, ptr %argv) {\nentry:\n"; Buffer.add_string b " call void @flan_rt_init(i32 %argc, ptr %argv)\n"; (* 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 unwinding past everything. *) Buffer.add_string b (Printf.sprintf " %s = alloca ptr\n" xfer_param); Buffer.add_string b (Printf.sprintf " store ptr null, ptr %s\n" xfer_param); let args = if fn.Tast.params = [] then "" else begin Buffer.add_string b " %a = alloca %slice\n"; Buffer.add_string b " call void @flan_argv(ptr %a)\n"; Buffer.add_string b " %args = load %slice, ptr %a\n"; "%slice %args" end in Buffer.add_string b (Printf.sprintf " %%r = call %s %s(%s)\n" (ll fn.Tast.ret) (fname "main") (if args = "" then "ptr " ^ xfer_param else args ^ ", ptr " ^ xfer_param)); (* Flushing matters: stdout is a FILE* and the acceptance test reads it. *) Buffer.add_string b " call void @flan_exit(i32 "; Buffer.add_string b (if Types.equal fn.Tast.ret (Types.Int Types.I32) then "%r" else "0"); Buffer.add_string b ")\n unreachable\n}\n"; Buffer.add_string m.out (Buffer.contents b) (* Everything a module needs before its own definitions: the tables the emitters look names up in, the struct types, and the foreign [declare]s. Both entry points below start here, so a redefinition module cannot drift from the whole-program one in how it names or lays out a type. *) (* Which file the compile unit is about. Every subprogram carries its own [!DIFile], so this only decides what a debugger calls the unit as a whole; the first function anyone actually wrote is the honest answer. *) let cu_file (p : Tast.program) = match List.find_opt (fun (f : Tast.fn) -> f.Tast.floc.Loc.line > 0) p.Tast.fns with | Some f -> f.Tast.floc.Loc.file | None -> "" let new_dbg (p : Tast.program) = let d = { dn = 0; dout = Buffer.create 4096; dfiles = Hashtbl.create 8; dtys = Hashtbl.create 32; dlocs = Hashtbl.create 256; dcu = 0 } in let file = dfile d (cu_file p) in d.dcu <- dalloc d; (* [isOptimized: false] is not decoration: it is what a debug build is, and [Build] sets -O0 to make it true. DW_LANG_C99 because the layout is C's and lldb's C support is then exactly right for it. *) dput d d.dcu (Printf.sprintf "distinct !DICompileUnit(language: DW_LANG_C99, file: !%d, producer: \"flan\", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false)" file); d let new_module ~checks ~dev ~known ?(debug = false) (p : Tast.program) = let m = { out = Buffer.create 8192; strs = Buffer.create 512; structs = Hashtbl.create 16; globals = Hashtbl.create 16; externs = Hashtbl.create 32; checks; dev; known; nstr = 0; dbg = (if debug then Some (new_dbg p) else None); } in List.iter (fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s) p.Tast.structs; List.iter (fun (g : Tast.global) -> Hashtbl.replace m.globals g.Tast.gname g.Tast.gty) p.Tast.globals; List.iter (fun (e : Tast.extern) -> Hashtbl.replace m.externs e.Tast.ename e.Tast.esym) p.Tast.externs; List.iter (fun (s : Tast.structure) -> Buffer.add_string m.out (Printf.sprintf "%s = type { %s }\n" (sname s.Tast.sname) (String.concat ", " (List.map (fun (f : Tast.field) -> ll f.Tast.fty) s.Tast.fields)))) p.Tast.structs; Buffer.add_char m.out '\n'; (* The foreign declarations. Every struct that crosses this boundary was flattened by a C shim, so each of these is scalars only and no calling convention has to be reproduced here. *) List.iter (fun (e : Tast.extern) -> Buffer.add_string m.out (Printf.sprintf "declare %s @%s(%s)\n" (ll e.Tast.eret) e.Tast.esym (String.concat ", " (List.concat_map (fun (t : Types.t) -> match t with | Types.String | Types.Slice _ -> [ "ptr"; "i64" ] | t -> [ ll t ]) e.Tast.eparams)))) p.Tast.externs; if p.Tast.externs <> [] then Buffer.add_char m.out '\n'; m (* The two named metadata nodes without which none of the above survives: LLVM drops every scrap of debug metadata, silently and with no diagnostic, if "Debug Info Version" is absent. A build that "works" and shows nothing in the debugger is that flag. *) let dmodule d = let b = Buffer.create 512 in Buffer.add_string b "\ndeclare void @llvm.dbg.declare(metadata, metadata, metadata)\n\n"; let dv = dalloc d and div = dalloc d in dput d dv "!{i32 7, !\"Dwarf Version\", i32 5}"; dput d div "!{i32 2, !\"Debug Info Version\", i32 3}"; Buffer.add_string b (Printf.sprintf "!llvm.dbg.cu = !{!%d}\n" d.dcu); Buffer.add_string b (Printf.sprintf "!llvm.module.flags = !{!%d, !%d}\n\n" dv div); Buffer.add_buffer b d.dout; Buffer.contents b let finish m = header ^ Buffer.contents m.strs ^ "\n" ^ Buffer.contents m.out ^ (match m.dbg with None -> "" | Some d -> dmodule d) (* [checks] is on by default: a dev build traps on an out-of-bounds [at] or [slice], a release build is told to drop them. *) let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = []) (p : Tast.program) : string = let m = new_module ~checks ~dev ~known:(fun _ -> true) ~debug p in (* One cell per function, initialised to the function this build compiled. Nothing has been redefined yet, so a dev build starts out behaving exactly like a release one — the indirection is the only difference. *) if dev then begin List.iter (fun (fn : Tast.fn) -> Buffer.add_string m.out (Printf.sprintf "%s = global ptr %s\n" (cellname fn.Tast.name) (fname fn.Tast.name))) p.Tast.fns; Buffer.add_char m.out '\n' end; List.iter (emit_global m) p.Tast.globals; List.iter (fun (fn : Tast.fn) -> emit_fn m ~pnames:(match List.assoc_opt fn.Tast.name pnames with | Some ns -> ns | None -> []) fn) p.Tast.fns; (match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns with | Some fn -> emit_main m fn | None -> ()); finish m (* A list of top-level forms, compiled into their own module against a host that is already running — the redefinition unit (NEXT.md, the dev loop). [C-c C-c] passes one name, [C-c C-k] passes a file's worth; there is one code path either way. The difference from [program] is almost entirely in what this module *does not* define: - a global the host has is [external]. Defining it would give the loaded object a second copy, and the whole point of reloading into a live process is that the state survives: sand's grid is a global, and "edit the code, keep the sand" is the demo. So a redefinition can change a function's body and can never re-initialise the program's data. - a function the host has is reached through its cell, which is the host's symbol, so a redefined [settle] calls whatever [move-grain] is current rather than carrying a private copy of it. - there is no [main]; this module is loaded, not started. A name the host does *not* have is the case ELF cannot express, since there is no symbol to bind to and no way to grow one. Those go through [flan_dev_cell] / [flan_dev_global], keyed by string, resolved once at install time into a module-local slot. See runtime/flan_dev.c. String literals still have to come along: they are this module's own constants, and omitting them is an undefined [@.str.N] at link time. *) let redefinition ?(checks = true) ?(dev = false) ?(debug = false) ?(known = fun _ -> true) ?call ?(consts = []) (p : Tast.program) ~fns : string = let target name = match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = name) p.Tast.fns with | Some f -> f | None -> failwith (Printf.sprintf "no such function: %s" name) in let targets = List.map target fns in (* A clause lifted out of one of these comes with it: its body may have changed too, and it is reached by address from inside the module rather than through a cell. Every other lifted clause is invisible here — it needs no declaration, since nothing in this module names it. *) let lifted = List.filter (fun (f : Tast.fn) -> match f.Tast.fparent with | Some p -> List.mem p fns | None -> false) p.Tast.fns in (* The rest of the program, as the cell and registry machinery below sees it. A lifted clause has neither, so it must not appear in either. *) let siblings = List.filter (fun (f : Tast.fn) -> f.Tast.fparent = None) p.Tast.fns in let m = new_module ~checks ~dev ~known ~debug p in (* A thunk the module runs itself is excluded from all of this: it is called directly by [flan_reload_call], so it needs no cell, must not be published into one, and must not take a registry slot — there are 4096 of those and an expression evaluated in a loop would exhaust them. Nothing pointing into the module is also what lets the agent unload it afterwards. *) let transient f = call = Some f in let new_fns = List.filter (fun (f : Tast.fn) -> (not (known f.Tast.name)) && not (transient f.Tast.name)) siblings and new_globals = List.filter (fun (g : Tast.global) -> not (known g.Tast.gname)) p.Tast.globals in List.iter (fun (g : Tast.global) -> Buffer.add_string m.out (if known g.Tast.gname then Printf.sprintf "%s = external %s %s\n" (gname g.Tast.gname) (if g.Tast.gconst && not dev then "constant" else "global") (ll g.Tast.gty) else Printf.sprintf "%s = internal global ptr null\n" (globalptr g.Tast.gname))) p.Tast.globals; if dev then begin (* The cells are the host's, like the globals. Referencing one is how a redefined function reaches its siblings, and storing into one is how it replaces itself. A name the host lacks gets a slot instead, filled by the installer below. *) List.iter (fun (f : Tast.fn) -> if not (transient f.Tast.name) then Buffer.add_string m.out (if known f.Tast.name then Printf.sprintf "%s = external global ptr\n" (cellname f.Tast.name) else Printf.sprintf "%s = internal global ptr null\n" (cellptr f.Tast.name))) siblings; if new_fns <> [] || new_globals <> [] then Buffer.add_string m.out "\ndeclare ptr @flan_dev_cell(ptr)\n\ declare ptr @flan_dev_global(ptr, i64, ptr)\n"; Buffer.add_char m.out '\n' end else (* Without cells there is nothing to route a call through, so the siblings are named directly and every one of them needs a declaration. *) List.iter (fun (f : Tast.fn) -> if not (List.exists (String.equal f.Tast.name) fns) then Buffer.add_string m.out (Printf.sprintf "declare %s\n" (signature ~named:false f))) siblings; (* Hidden for the same reason a redefined body is: default visibility in a shared object is interposable, and that applies to taking the address too, so a plain reference would resolve to the host's copy of the clause and this module would install the very handler it is replacing. *) List.iter (fun f -> emit_fn m ~hidden:true f) lifted; List.iter (fun f -> emit_fn m ~hidden:dev f) targets; if dev then begin (* Publishing is a separate, named function rather than a constructor: the agent has to choose *when* the swap happens — at a frame boundary, on the game thread — and a loader-run ctor would do it during dlopen, on whatever thread called it, in the middle of a frame. Order inside it is load-bearing. Every lookup is resolved before any body is published, because publishing first exposes a function whose slots are still null to anything that calls it. *) let b = Buffer.create 512 in let n = ref 0 in let fresh () = incr n; Printf.sprintf "%%d%d" !n in List.iter (fun (f : Tast.fn) -> let t = fresh () in Buffer.add_string b (Printf.sprintf " %s = call ptr @flan_dev_cell(ptr %s)\n store ptr %s, ptr %s\n" t (cstring m ("flan." ^ f.Tast.name)) t (cellptr f.Tast.name))) new_fns; List.iter (fun (g : Tast.global) -> let t = fresh () in (* sizeof, spelled the way LLVM spells it: the offset of element one of a null pointer. Cheaper than a layout calculator in OCaml that would have to agree with LLVM's on every target. *) (* Its declared initial value travels with it, as a constant the runtime copies on the allocation and ignores afterwards. Without this a new (defvar n i64 42) or a new defconst would silently be zero — calloc is only the right answer for ZII. *) let init = Printf.sprintf "@\".init.%d\"" m.nstr in m.nstr <- m.nstr + 1; Buffer.add_string m.strs (Printf.sprintf "%s = private constant %s %s\n" init (ll g.Tast.gty) (const m g.Tast.ginit)); Buffer.add_string b (Printf.sprintf " %s = call ptr @flan_dev_global(ptr %s, i64 ptrtoint (ptr getelementptr (%s, ptr null, i32 1) to i64), ptr %s)\n \ store ptr %s, ptr %s\n" t (cstring m ("flan." ^ g.Tast.gname)) (ll g.Tast.gty) init t (globalptr g.Tast.gname))) new_globals; (* A constant whose value the checker never consumed is just bytes in the program's memory, so a new value is published the same way a new body is: one store, at the frame boundary. One the checker *did* consume is in the shape of the program and never gets here — the session refuses it. *) List.iter (fun (g : Tast.global) -> if List.exists (String.equal g.Tast.gname) consts then Buffer.add_string b (Printf.sprintf " store %s %s, ptr %s\n" (ll g.Tast.gty) (const m g.Tast.ginit) (gname g.Tast.gname))) p.Tast.globals; List.iter (fun (f : Tast.fn) -> if transient f.Tast.name then () else if known f.Tast.name then Buffer.add_string b (Printf.sprintf " store ptr %s, ptr %s\n" (fname f.Tast.name) (cellname f.Tast.name)) else begin let t = fresh () in Buffer.add_string b (Printf.sprintf " %s = load ptr, ptr %s\n store ptr %s, ptr %s\n" t (cellptr f.Tast.name) (fname f.Tast.name) t) end) targets; Buffer.add_string m.out (Printf.sprintf "\ndefine void @flan_reload_install() {\nentry:\n%s ret void\n}\n" (Buffer.contents b)); (* An expression evaluation compiles to a function with nowhere to be called from, so the module says so and the agent runs it once — after the install, on the game thread, so it sees both the bodies this module just published and a program state the program agrees is consistent. *) match call with | Some fn -> Buffer.add_string m.out (Printf.sprintf "\ndefine void @flan_reload_call() {\nentry:\n \ %s = alloca ptr\n store ptr null, ptr %s\n \ call %s %s(ptr %s)\n ret void\n}\n" xfer_param xfer_param (ll Types.Unit) (fname fn) xfer_param); (* Nothing outside this module refers to anything in it once the call has returned — no cell holds an address in its text, the registry has no slot for it, and the value it produced was copied out. So it says so, and the agent unloads it. A module that publishes a body can never say this: its whole purpose is to leave a pointer behind. [m.nstr = 0] is the third condition and it is about *data*, not text. A string literal is emitted into this module's own image, and an expression may store one anywhere it likes — [(set msg "tuned")] on a string global leaves that global pointing into the mapping the agent is about to drop. The next thunk can be mapped at the same address, so the result is silent garbage rather than a fault. A module with no string constants has nothing in its image anyone could still be pointing at; one with any keeps its mapping, which costs a page and is the same bargain every redefinition already makes. *) if fns = [ fn ] && consts = [] && m.nstr = 0 then Buffer.add_string m.out "\n@flan_reload_transient = global i8 1\n" | None -> () end; finish m