One field list per runtime struct, one spelling per symbol prefix

The %handler, %restart, %fninfo and %flanframe shapes were written twice:
as LLVM type strings in emit.ml and as hand-computed byte offsets in x86.ml,
with the two %fninfo initialisers spelled a third and fourth time. Emit.Rt
now holds one field list per struct and derives all four — the type string
and the getelementptr index for LLVM, the offset and the size for x86, and
the initialiser for both. The derived numbers were checked against every old
constant before the call sites moved.

The flan. prefixes were spelled in four files, including both backends
hand-writing "flan." ^ name for a DWARF linkage name instead of calling
their own helper. Mangle now holds them unquoted; each backend adds its own
sigil. The ABI markers stay apart on purpose: flan.abi.llvm and flan.abi.x86
differing is what makes the loader refuse a crossed pair.

The float-to-integer cast bounds and the division-check elision policy are
Emit.cast_range and Emit.div_checks. The second is a language decision and
had been byte-identical in both files; the first had drifted cosmetically.

emit and emit --x86 output for all 166 test/programs, at -O0 release, --dev,
--debug and --dev --debug, stdout and stderr, is byte-identical to the
pre-change compiler.
This commit is contained in:
Joseph Ferano 2026-09-20 13:00:52 +07:00
parent 26439dc22e
commit 1829cd43b6
5 changed files with 320 additions and 115 deletions

View File

@ -2773,7 +2773,7 @@ let render_listing ~sym insns =
Buffer.contents b Buffer.contents b
let asm_of ~obj name = let asm_of ~obj name =
let sym = "flan." ^ name in let sym = Mangle.sym name in
let code, text = let code, text =
run_capture run_capture
(String.concat " " (String.concat " "

View File

@ -47,10 +47,12 @@ let rec map_lr f = function
(* ── Names ─────────────────────────────────────────────────────────── *) (* ── Names ─────────────────────────────────────────────────────────── *)
(* Flan names contain -, ?, > and /, so every emitted name is quoted. The (* Flan names contain -, ?, > and /, so every emitted name is quoted. The
[flan.] prefix keeps the Flan [main] from colliding with C's. *) prefix each of these applies is [Mangle]'s, spelled there once for both
backends and for the macro loader; the [@] and the quotes are LLVM's and
are applied here. *)
let quoted s = "\"" ^ s ^ "\"" let quoted s = "\"" ^ s ^ "\""
let fname n = "@" ^ quoted ("flan." ^ n) let fname n = "@" ^ quoted (Mangle.sym n)
let gname n = "@" ^ quoted ("flan." ^ n) let gname n = "@" ^ quoted (Mangle.sym n)
let sname n = "%" ^ quoted n let sname n = "%" ^ quoted n
(* A dev build's redefinable calls go through a cell: a mutable global holding (* A dev build's redefinable calls go through a cell: a mutable global holding
@ -58,7 +60,7 @@ let sname n = "%" ^ quoted n
and every existing call site follows it which is the whole point, since a 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 call bound at link time cannot be made to notice a new body. Release builds
have no cells and call the symbol directly. *) have no cells and call the symbol directly. *)
let cellname n = "@" ^ quoted ("flan.cell." ^ n) let cellname n = "@" ^ quoted (Mangle.cell n)
(* A name the host was never built with — a defn or a defvar typed in after the (* 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 process started has no symbol to bind to, so it is keyed by string through
@ -75,8 +77,8 @@ let xfer_param = "%xfer"
let struct_name_of (t : Types.t) = let struct_name_of (t : Types.t) =
match t with Types.Named n -> n | _ -> "a condition" match t with Types.Named n -> n | _ -> "a condition"
let cellptr n = "@" ^ quoted ("flan.cellp." ^ n) let cellptr n = "@" ^ quoted (Mangle.cellptr n)
let globalptr n = "@" ^ quoted ("flan.gp." ^ n) let globalptr n = "@" ^ quoted (Mangle.globalptr n)
(* Which backend built this image. A dev build defines its own marker and a (* Which backend built this image. A dev build defines its own marker and a
redefinition module emits a data relocation against the one it was built redefinition module emits a data relocation against the one it was built
@ -89,6 +91,130 @@ let globalptr n = "@" ^ quoted ("flan.gp." ^ n)
let abi_marker = "flan.abi.llvm" let abi_marker = "flan.abi.llvm"
let abi_marker_sym = "@" ^ quoted abi_marker let abi_marker_sym = "@" ^ quoted abi_marker
(* ── The runtime's own structs ───────────────────────────────────────── *)
(* Four structs that are not Flan types: they are declared in C, in
runtime/flan_rt.c and runtime/flan_dev.c, and both backends have to agree
with that C and with each other about every field. This backend needs the
LLVM type string and the field *index* a [getelementptr] takes; [x86.ml]
needs the byte *offset* and the total size. All four are derived here from
one list per struct, so that adding a field to [flan_restart] in the C is
one edit on this side rather than three.
The rules are C's, which is what makes the derivation legal at all: fields
in declaration order, each at the next offset its own alignment allows, the
whole rounded up to the strictest alignment in it. The general case of that
is [lay_fields] below, over Flan types; these four hold only pointers and
fixed-width integers, so they are measured here without a module context
which is what lets [x86.ml] ask for an offset before it has one. *)
module Rt = struct
(* Every field any of them has. A pointer is 8 bytes on the one target both
backends emit for; [i32] and [i64] are what the C spells. *)
type kind = Ptr | I32 | I64
type t = { sname : string; fields : (string * kind) list }
let ll_of = function Ptr -> "ptr" | I32 -> "i32" | I64 -> "i64"
let size_of = function Ptr | I64 -> 8 | I32 -> 4
(* A handler frame: the one it displaced, the condition type it matches, and
the lifted function that runs. *)
let handler =
{ sname = "handler"; fields = [ "prev", Ptr; "type", I32; "fn", Ptr ] }
(* A restart frame. The first four fields are what the runtime's own
[flan_restart] declares and their offsets do not move; the rest are §3's
parameter passing, described where the type is written into the header. *)
let restart =
{ sname = "restart";
fields =
[ "prev", Ptr; "name_id", I32; "name", Ptr; "namelen", I64;
"args", Ptr; "arity", I32; "sig_id", I32; "armed", I32;
"sig", Ptr; "siglen", I64 ] }
(* The static description of a function, and the shadow-stack frame that
points at one. Dev builds only (runtime/flan_dev.c). *)
let fninfo =
{ sname = "fninfo";
fields =
[ "name", Ptr; "namelen", I64; "loc", Ptr; "loclen", I64;
"nslots", I32; "slots_fp", I32; "refs_fp", I32 ] }
let flanframe =
{ sname = "flanframe"; fields = [ "prev", Ptr; "info", Ptr; "slots", Ptr ] }
let align_up n a = (n + a - 1) / a * a
(* Size, and the offset of every field, by C's rules. *)
let layout s =
let off = ref 0 and al = ref 1 and rev = ref [] in
List.iter
(fun (n, k) ->
let sz = size_of k in
off := align_up !off sz;
rev := (n, !off) :: !rev;
off := !off + sz;
if sz > !al then al := sz)
s.fields;
align_up !off !al, List.rev !rev
let size s = fst (layout s)
let field s n =
match List.assoc_opt n (snd (layout s)) with
| Some o -> o
| None -> failwith (Printf.sprintf "no field %s in %%%s" n s.sname)
(* The [getelementptr] index of a field, which is this backend's handle on
it LLVM counts fields where the assembler counts bytes. *)
let index s n =
let rec go i = function
| [] -> failwith (Printf.sprintf "no field %s in %%%s" n s.sname)
| (f, _) :: rest -> if String.equal f n then i else go (i + 1) rest
in
go 0 s.fields
(* The type declaration this file's header carries. *)
let ll_type s =
Printf.sprintf "%%%s = type { %s }" s.sname
(String.concat ", " (List.map (fun (_, k) -> ll_of k) s.fields))
(* An initialised constant of one of them, given one operand per field in
declaration order. Both backends build the same [%fninfo] this way, which
is the whole point: the field list decides the order and the widths, and
neither spelling can be updated without the other. *)
let ll_init s vals =
Printf.sprintf "%%%s { %s }" s.sname
(String.concat ", "
(List.map2 (fun (_, k) v -> ll_of k ^ " " ^ v) s.fields vals))
(* The same constant as assembler directives. All padding is explicit,
inside and at the end, because the assembler adds none: a [.align] before
the label says where the object starts, not how the fields sit in it nor
how long it is, and the next object would otherwise begin inside this
one's tail. Every field is 4 or 8 bytes wide, so a trailing gap is always
a whole number of [.long]s; an interior one is whatever C's rule leaves
and is written as bytes. *)
let asm_init s vals =
let b = Buffer.create 128 in
let gap n = if n > 0 then Buffer.add_string b (Printf.sprintf "\t.zero\t%d\n" n) in
let raw =
List.fold_left2
(fun off (_, k) v ->
let sz = size_of k in
let at = align_up off sz in
gap (at - off);
Buffer.add_string b
(Printf.sprintf "\t%s\t%s\n" (if sz = 8 then ".quad" else ".long") v);
at + sz)
0 s.fields vals
in
for _ = 1 to (size s - raw) / 4 do
Buffer.add_string b "\t.long\t0\n"
done;
Buffer.contents b
end
(* ── Types ─────────────────────────────────────────────────────────── *) (* ── Types ─────────────────────────────────────────────────────────── *)
let rec ll (t : Types.t) = let rec ll (t : Types.t) =
@ -1124,10 +1250,12 @@ let fninfo m (fn : Tast.fn) ~nslots =
let id = Printf.sprintf "@\".fi.%d\"" m.nfi in let id = Printf.sprintf "@\".fi.%d\"" m.nfi in
m.nfi <- m.nfi + 1; m.nfi <- m.nfi + 1;
Buffer.add_string m.strs Buffer.add_string m.strs
(Printf.sprintf (Printf.sprintf "%s = private unnamed_addr constant %s\n" id
"%s = private unnamed_addr constant %%fninfo { ptr %s, i64 %d, ptr %s, i64 %d, i32 %d, i32 %d, i32 %d }\n" (Rt.ll_init Rt.fninfo
id nid nlen lid llen nslots (slot_fingerprint fn) [ nid; string_of_int nlen; lid; string_of_int llen;
(Reach.ref_fingerprint ~is_global:(Hashtbl.mem m.globals) fn)); string_of_int nslots; string_of_int (slot_fingerprint fn);
string_of_int
(Reach.ref_fingerprint ~is_global:(Hashtbl.mem m.globals) fn) ]));
id id
(* ── Bounds checks ───────────────────────────────────────────────────── *) (* ── Bounds checks ───────────────────────────────────────────────────── *)
@ -1224,6 +1352,47 @@ let widen f (k : Types.ikind) v =
(* The most negative value of a signed kind, as the decimal LLVM wants. *) (* The most negative value of a signed kind, as the decimal LLVM wants. *)
let int_min k = Int64.neg (Int64.shift_left 1L (Types.bits k - 1)) let int_min k = Int64.neg (Int64.shift_left 1L (Types.bits k - 1))
(* Which of the two arithmetic guards a division actually needs. This is a
decision about the language and not about either instruction set, so both
backends ask it here: a divisor that is a literal the test cannot fire on
carries no test at all, and (/ x 2) the common case is then exactly the
divide it reads as. The overflow test exists only for signed kinds, where
[min / -1] is the one pair whose quotient does not fit.
A literal the checker folded is what [lit] carries; [None] is anything else,
including a constant the folder could not see, and pays both tests. *)
let div_checks ~lit (k : Types.ikind) =
let need_zero = match lit with Some n -> Int64.equal n 0L | None -> true in
let need_ovf =
Types.signed k
&& (match lit with Some n -> Int64.equal n (-1L) | None -> true)
in
need_zero, need_ovf
(* The bounds a float-to-integer cast is checked against, for both backends.
The pair of floats is the open interval the source value has to be in, and
both ends are exact in a double: a power of two is, and [ldexp] of it is
the only spelling that cannot round. The high end is the first value
*above* the range rather than the last one in it, because 2^63 - 1 is not
representable and 2^63 is so the test is [< hi] and never [<= hi].
The pair of integers is the range the failure message reports, which is the
integer range itself: what a programmer wants told is "u8 holds 0 to 255",
not the two floats the guard compared. *)
let cast_range (k : Types.ikind) =
let n = Types.bits k in
let signed = Types.signed k in
let lo_f = if signed then ldexp (-1.0) (n - 1) else 0.0 in
let hi_f = if signed then ldexp 1.0 (n - 1) else ldexp 1.0 n in
let lo_i = if signed then int_min k else 0L in
let hi_i =
if signed then Int64.sub (Int64.shift_left 1L (n - 1)) 1L
else if n = 64 then -1L
else Int64.sub (Int64.shift_left 1L n) 1L
in
lo_f, hi_f, lo_i, hi_i
(* A divide or a remainder. [is_rem] only picks which pair of codes is used; (* A divide or a remainder. [is_rem] only picks which pair of codes is used;
the tests are identical, because `srem` overflows on exactly the operands the tests are identical, because `srem` overflows on exactly the operands
`sdiv` does the intermediate quotient is the thing that does not fit. `sdiv` does the intermediate quotient is the thing that does not fit.
@ -1234,11 +1403,7 @@ let int_min k = Int64.neg (Int64.shift_left 1L (Types.bits k - 1))
let check_div f ~guard loc ~is_rem (k : Types.ikind) ~lit a b = let check_div f ~guard loc ~is_rem (k : Types.ikind) ~lit a b =
if f.md.checks then begin if f.md.checks then begin
let ty = ll (Types.Int k) in let ty = ll (Types.Int k) in
let need_zero = match lit with Some n -> Int64.equal n 0L | None -> true in let need_zero, need_ovf = div_checks ~lit k in
let need_ovf =
Types.signed k
&& (match lit with Some n -> Int64.equal n (-1L) | None -> true)
in
if need_zero || need_ovf then begin if need_zero || need_ovf then begin
(* [false] rather than an emitted instruction when a test is elided: an (* [false] rather than an emitted instruction when a test is elided: an
LLVM operand may be a constant, and the [or] and the [select] below LLVM operand may be a constant, and the [or] and the [select] below
@ -1310,18 +1475,7 @@ let check_cast f ~guard loc (src : Types.fkind) (k : Types.ikind) v =
ins f "%s = fpext float %s to double" t v; ins f "%s = fpext float %s to double" t v;
t t
in in
let n = Types.bits k in let lo_f, hi_f, lo_i, hi_i = cast_range k in
let signed = Types.signed k in
(* The first value below the range and the first value above it, and then
the range the condition reports, which is the last value *in* it. *)
let lo_f = if signed then ldexp (-1.0) (n - 1) else 0.0 in
let hi_f = if signed then ldexp 1.0 (n - 1) else ldexp 1.0 n in
let lo_i = if signed then int_min k else 0L in
let hi_i =
if signed then Int64.sub (Int64.shift_left 1L (n - 1)) 1L
else if n = 64 then -1L
else Int64.sub (Int64.shift_left 1L n) 1L
in
(* LLVM takes a double constant as the hex of its bits, which is the only (* LLVM takes a double constant as the hex of its bits, which is the only
spelling that cannot lose anything on the way through. *) spelling that cannot lose anything on the way through. *)
let dbl x = Printf.sprintf "0x%016Lx" (Int64.bits_of_float x) in let dbl x = Printf.sprintf "0x%016Lx" (Int64.bits_of_float x) in
@ -1579,11 +1733,11 @@ and value_at f (e : Tast.expr) : string =
collision between two different signatures harmless in practice and collision between two different signatures harmless in practice and
it is also the cheaper half. *) it is also the cheaper half. *)
let arity = fresh f in let arity = fresh f in
ins f "%s = load i32, ptr %s" arity (restart_field f t 5); ins f "%s = load i32, ptr %s" arity (restart_field f t "arity");
let a_ok = fresh f in let a_ok = fresh f in
ins f "%s = icmp eq i32 %s, %d" a_ok arity (List.length args); ins f "%s = icmp eq i32 %s, %d" a_ok arity (List.length args);
let want = fresh f in let want = fresh f in
ins f "%s = load i32, ptr %s" want (restart_field f t 6); ins f "%s = load i32, ptr %s" want (restart_field f t "sig_id");
let s_ok = fresh f in let s_ok = fresh f in
ins f "%s = icmp eq i32 %s, %d" s_ok want sg_id; ins f "%s = icmp eq i32 %s, %d" s_ok want sg_id;
let both = fresh f in let both = fresh f in
@ -1593,9 +1747,9 @@ and value_at f (e : Tast.expr) : string =
(* What the frame says it takes is read off the frame, because only the (* What the frame says it takes is read off the frame, because only the
frame knows; what was given is this call site's own spelling. *) frame knows; what was given is this call site's own spelling. *)
let wp = fresh f in let wp = fresh f in
ins f "%s = load ptr, ptr %s" wp (restart_field f t 8); ins f "%s = load ptr, ptr %s" wp (restart_field f t "sig");
let wl = fresh f in let wl = fresh f in
ins f "%s = load i64, ptr %s" wl (restart_field f t 9); ins f "%s = load i64, ptr %s" wl (restart_field f t "siglen");
let gid, gn = string_bytes f.md sg in let gid, gn = string_bytes f.md sg in
ins f ins f
"call void @flan_restart_args_fail(ptr %s, i64 %d, ptr %s, i64 %d, \ "call void @flan_restart_args_fail(ptr %s, i64 %d, ptr %s, i64 %d, \
@ -1605,7 +1759,7 @@ and value_at f (e : Tast.expr) : string =
signature just agreed on. *) signature just agreed on. *)
if vals <> [] then begin if vals <> [] then begin
let buf = fresh f in let buf = fresh f in
ins f "%s = load ptr, ptr %s" buf (restart_field f t 4); ins f "%s = load ptr, ptr %s" buf (restart_field f t "args");
let sty = let sty =
"{ " ^ String.concat ", " (List.map (fun (_, ty) -> ll ty) vals) ^ " }" "{ " ^ String.concat ", " (List.map (fun (_, ty) -> ll ty) vals) ^ " }"
in in
@ -1616,7 +1770,7 @@ and value_at f (e : Tast.expr) : string =
p sty buf i; p sty buf i;
ins f "store %s %s, ptr %s" (ll ty) v p) ins f "store %s %s, ptr %s" (ll ty) v p)
vals; vals;
ins f "store i32 1, ptr %s" (restart_field f t 7) ins f "store i32 1, ptr %s" (restart_field f t "armed")
end; end;
ins f "store ptr %s, ptr %s" t xfer_param; ins f "store ptr %s, ptr %s" t xfer_param;
term f "br label %%%s" (current_pad f); term f "br label %%%s" (current_pad f);
@ -1947,12 +2101,12 @@ and emit_handled f frames body =
(fun (h : Tast.hframe) -> (fun (h : Tast.hframe) ->
let slot = alloca_raw f "%handler" in let slot = alloca_raw f "%handler" in
let ty = fresh f in let ty = fresh f in
ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 1" ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 %d"
ty slot; ty slot (Rt.index Rt.handler "type");
ins f "store i32 %d, ptr %s" h.Tast.htype ty; ins f "store i32 %d, ptr %s" h.Tast.htype ty;
let fp = fresh f in let fp = fresh f in
ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 2" ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 %d"
fp slot; fp slot (Rt.index Rt.handler "fn");
(* The clause's body address, deliberately, and not a cell load: (* The clause's body address, deliberately, and not a cell load:
plan.org makes a top-level function value a stable trampoline over 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 its cell, but a handler frame is not one nothing can name it, and
@ -2008,9 +2162,10 @@ and emit_handled f frames body =
and args_type (c : Tast.rclause) = and args_type (c : Tast.rclause) =
"{ " ^ String.concat ", " (List.map (fun (_, t) -> ll t) c.Tast.rparams) ^ " }" "{ " ^ String.concat ", " (List.map (fun (_, t) -> ll t) c.Tast.rparams) ^ " }"
and restart_field f slot i = and restart_field f slot name =
let p = fresh f in let p = fresh f in
ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 %d" p slot i; ins f "%s = getelementptr inbounds %%restart, ptr %s, i32 0, i32 %d" p slot
(Rt.index Rt.restart name);
p p
(* (with-allocator A BODY...) — spec-memory.md's "Allocators". (* (with-allocator A BODY...) — spec-memory.md's "Allocators".
@ -2076,33 +2231,33 @@ and emit_restart_case f ty clauses body =
map_lr map_lr
(fun (c : Tast.rclause) -> (fun (c : Tast.rclause) ->
let slot = alloca_raw f "%restart" in let slot = alloca_raw f "%restart" in
ins f "store i32 %d, ptr %s" c.Tast.rname_id (restart_field f slot 1); ins f "store i32 %d, ptr %s" c.Tast.rname_id (restart_field f slot "name_id");
(* The name itself, beside the hash. A hash is all that matching (* The name itself, beside the hash. A hash is all that matching
needs, but a break loop has to *show* someone their choices, and needs, but a break loop has to *show* someone their choices, and
nothing at run time can turn a hash back into a name. *) nothing at run time can turn a hash back into a name. *)
let sid, slen = string_bytes f.md c.Tast.rname in let sid, slen = string_bytes f.md c.Tast.rname in
ins f "store ptr %s, ptr %s" sid (restart_field f slot 2); ins f "store ptr %s, ptr %s" sid (restart_field f slot "name");
ins f "store i64 %d, ptr %s" slen (restart_field f slot 3); ins f "store i64 %d, ptr %s" slen (restart_field f slot "namelen");
(* §3's signature, which every frame carries whether it takes (* §3's signature, which every frame carries whether it takes
parameters or not: an [invoke-restart] compares against whatever parameters or not: an [invoke-restart] compares against whatever
frame the name found, and a clause taking none has to be able to frame the name found, and a clause taking none has to be able to
refuse arguments as loudly as one taking two of the wrong type. *) refuse arguments as loudly as one taking two of the wrong type. *)
ins f "store i32 %d, ptr %s" ins f "store i32 %d, ptr %s"
(List.length c.Tast.rparams) (restart_field f slot 5); (List.length c.Tast.rparams) (restart_field f slot "arity");
ins f "store i32 %d, ptr %s" c.Tast.rsig_id (restart_field f slot 6); ins f "store i32 %d, ptr %s" c.Tast.rsig_id (restart_field f slot "sig_id");
let gid, glen = string_bytes f.md c.Tast.rsig in let gid, glen = string_bytes f.md c.Tast.rsig in
ins f "store ptr %s, ptr %s" gid (restart_field f slot 8); ins f "store ptr %s, ptr %s" gid (restart_field f slot "sig");
ins f "store i64 %d, ptr %s" glen (restart_field f slot 9); ins f "store i64 %d, ptr %s" glen (restart_field f slot "siglen");
let args = let args =
if c.Tast.rparams = [] then None if c.Tast.rparams = [] then None
else begin else begin
let buf = alloca_raw f (args_type c) in let buf = alloca_raw f (args_type c) in
ins f "store ptr %s, ptr %s" buf (restart_field f slot 4); ins f "store ptr %s, ptr %s" buf (restart_field f slot "args");
(* Nothing has filled it in yet. Whoever aims a transfer at this (* Nothing has filled it in yet. Whoever aims a transfer at this
frame without going through an [invoke-restart] the break frame without going through an [invoke-restart] the break
loop, today leaves this zero, and the clause traps rather loop, today leaves this zero, and the clause traps rather
than running on values no one supplied. *) than running on values no one supplied. *)
ins f "store i32 0, ptr %s" (restart_field f slot 7); ins f "store i32 0, ptr %s" (restart_field f slot "armed");
Some buf Some buf
end end
in in
@ -2151,7 +2306,7 @@ and emit_restart_case f ty clauses body =
| None -> () | None -> ()
| Some buf -> | Some buf ->
let armed = fresh f in let armed = fresh f in
ins f "%s = load i32, ptr %s" armed (restart_field f slot 7); ins f "%s = load i32, ptr %s" armed (restart_field f slot "armed");
let ok = fresh f in let ok = fresh f in
ins f "%s = icmp ne i32 %s, 0" ok armed; ins f "%s = icmp ne i32 %s, 0" ok armed;
(* Aimed here by something that supplied no arguments — there is no such (* Aimed here by something that supplied no arguments — there is no such
@ -3021,9 +3176,13 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
" prev); " prev);
List.iter List.iter
(fun line -> Buffer.add_string f.allocas (" " ^ line ^ "\n")) (fun line -> Buffer.add_string f.allocas (" " ^ line ^ "\n"))
[ "%frame.i = getelementptr inbounds %flanframe, ptr %frame, i32 0, i32 1"; [ Printf.sprintf
"%%frame.i = getelementptr inbounds %%flanframe, ptr %%frame, i32 0, i32 %d"
(Rt.index Rt.flanframe "info");
Printf.sprintf "store ptr %s, ptr %%frame.i" info; Printf.sprintf "store ptr %s, ptr %%frame.i" info;
"%frame.s = getelementptr inbounds %flanframe, ptr %frame, i32 0, i32 2"; Printf.sprintf
"%%frame.s = getelementptr inbounds %%flanframe, ptr %%frame, i32 0, i32 %d"
(Rt.index Rt.flanframe "slots");
Printf.sprintf "store ptr %s, ptr %%frame.s" Printf.sprintf "store ptr %s, ptr %%frame.s"
(match f.slotv with Some v -> v | None -> "null"); (match f.slotv with Some v -> v | None -> "null");
"store ptr %frame, ptr @flan_frame_head" ]; "store ptr %frame, ptr @flan_frame_head" ];
@ -3093,8 +3252,8 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
in in
dput d sub dput d sub
(Printf.sprintf (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})" "distinct !DISubprogram(name: \"%s\", linkageName: \"%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 (dstr fn.Tast.name) (dstr (Mangle.sym fn.Tast.name)) file file f.dline sty f.dline
d.dcu d.dcu
(String.concat ", " (List.map (fun v -> Printf.sprintf "!%d" v) vars))); (String.concat ", " (List.map (fun v -> Printf.sprintf "!%d" v) vars)));
at_loc f fn.Tast.floc at_loc f fn.Tast.floc
@ -3367,7 +3526,7 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
%map = type { ptr, i64, i64, ptr, i64 } %map = type { ptr, i64, i64, ptr, i64 }
; A handler frame: the one it displaced, the condition type it matches, and ; 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. ; the lifted function that runs. Allocated on the establishing frame's stack.
%handler = type { ptr, i32, ptr } |} ^ Rt.ll_type Rt.handler ^ {|
; A restart frame: the one it displaced and the name it offers. There is no ; 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 ; 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 ; a transfer's aim exact, and makes re-entering a restart-case work with
@ -3379,13 +3538,12 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
; filled the buffer in, and that spelling itself for the message when the two ; filled the buffer in, and that spelling itself for the message when the two
; ends disagree. The first four fields are what the runtime's own ; ends disagree. The first four fields are what the runtime's own
; [flan_restart] declares and their offsets do not move. ; [flan_restart] declares and their offsets do not move.
%restart = type { ptr, i32, ptr, i64, ptr, i32, i32, i32, ptr, i64 } |} ^ Rt.ll_type Rt.restart ^ {|
; A shadow-stack frame and the static description of the function that pushed ; A shadow-stack frame and the static description of the function that pushed
; it (runtime/flan_dev.c). Dev builds only: [emit_fn] pushes one on entry and ; it (runtime/flan_dev.c). Dev builds only: [emit_fn] pushes one on entry and
; every [ret] restores the head, the transfer path included. A release build ; every [ret] restores the head, the transfer path included. A release build
; emits neither, and the head below is then a symbol nothing in the .ll names. ; emits neither, and the head below is then a symbol nothing in the .ll names.
%fninfo = type { ptr, i64, ptr, i64, i32, i32, i32 } |} ^ Rt.ll_type Rt.fninfo ^ "\n" ^ Rt.ll_type Rt.flanframe ^ {|
%flanframe = type { ptr, ptr, ptr }
@flan_frame_head = external global ptr @flan_frame_head = external global ptr
declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg) declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
@ -3908,7 +4066,7 @@ let macro_thunk m (fn : Tast.fn) =
\ store %s %%r, ptr %%out\n\ \ store %s %%r, ptr %%out\n\
\ ret void\n\ \ ret void\n\
}\n\n" }\n\n"
(quoted ("flan.macro." ^ name)) (quoted (Mangle.macro name))
ret (fname name) ret) ret (fname name) ret)
(* [checks] is on by default: a dev build traps on an out-of-bounds [at] or (* [checks] is on by default: a dev build traps on an out-of-bounds [at] or
@ -4167,7 +4325,7 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
let t = fresh () in let t = fresh () in
Buffer.add_string b Buffer.add_string b
(Printf.sprintf " %s = call ptr @flan_dev_cell(ptr %s)\n store ptr %s, ptr %s\n" (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))) t (cstring m (Mangle.sym f.Tast.name)) t (cellptr f.Tast.name)))
new_fns; new_fns;
List.iter List.iter
(fun (g : Tast.global) -> (fun (g : Tast.global) ->
@ -4204,7 +4362,7 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
(Printf.sprintf (Printf.sprintf
" %s = call ptr @flan_dev_global(ptr %s, i64 ptrtoint (ptr getelementptr (%s, ptr null, i32 1) to i64), ptr %s)\n \ " %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" store ptr %s, ptr %s\n"
t (cstring m ("flan." ^ g.Tast.gname)) (ll g.Tast.gty) init t t (cstring m (Mangle.sym g.Tast.gname)) (ll g.Tast.gty) init t
(globalptr g.Tast.gname))) (globalptr g.Tast.gname)))
new_globals; new_globals;
(* A constant whose value the checker never consumed is just bytes in the (* A constant whose value the checker never consumed is just bytes in the

View File

@ -294,7 +294,7 @@ let compile (names : string list) (extra : Form.t list) : loaded =
end; end;
let handle = Dynload.dl_open out in let handle = Dynload.dl_open out in
{ handle; { handle;
fns = List.map (fun n -> (n, Dynload.dl_sym handle ("flan.macro." ^ n))) names } fns = List.map (fun n -> (n, Dynload.dl_sym handle (Mangle.macro n))) names }
(* ── Where the call site is ──────────────────────────────────────── (* ── Where the call site is ────────────────────────────────────────
The one thing a macro cannot find out for itself and the one it needs to The one thing a macro cannot find out for itself and the one it needs to

42
lib/mangle.ml Normal file
View File

@ -0,0 +1,42 @@
(* The symbol names a Flan build puts into an object, spelled once.
Both backends emit the same names that is not a nicety, it is the link:
an [--x86] host and a redefinition module built by LLVM bind against each
other, so [@"flan.cell.<n>"] has to be byte-for-byte the same string on
both sides or the dlopen fails and the piece served nothing. The macro
loader and the daemon's disassembler look up the same names from outside
the backends entirely.
So the prefixes live here, unquoted and without a sigil. Quoting is each
backend's own LLVM writes [@"..."], the assembler writes ["..."] and a
Flan name holds -, ?, > and /, which is why every emitted name is quoted
at all. This module only decides *which string* is quoted.
Not here: the ABI marker. [Emit.abi_marker] is ["flan.abi.llvm"] and
[X86.abi_marker] is ["flan.abi.x86"], and the two must stay distinct
a crossed pair is refused at [dlopen] precisely because the marker one
image defines is not the one the other references. Sharing that string
would delete the mechanism. *)
(* The prefix itself. It keeps the Flan [main] from colliding with C's, and
it is what makes every Flan symbol recognisable in a disassembly. *)
let prefix = "flan."
(* A function or a global. One namespace, because the language has one: a
[defn] and a [defvar] cannot share a name, so nothing here has to keep
them apart. The compiler's own names go through this too [.init-globals]
and [.init-data] start with a dot no reader token can produce. *)
let sym n = prefix ^ n
(* A dev build's indirection cell: a mutable global holding the address of the
function that is currently this name's body. *)
let cell n = prefix ^ "cell." ^ n
(* The two module-local caches a name the host was never built with is reached
through one for a function, one for a global. *)
let cellptr n = prefix ^ "cellp." ^ n
let globalptr n = prefix ^ "gp." ^ n
(* A compiled macro's entry point, which the expander dlsyms by this name out
of the module [Build.macro_module] wrote. *)
let macro n = prefix ^ "macro." ^ n

View File

@ -510,11 +510,13 @@ let signed_of (t : Types.t) =
(* ── Mangling ────────────────────────────────────────────────────────── *) (* ── Mangling ────────────────────────────────────────────────────────── *)
(* The same names [emit.ml] gives, so a build made here links against the same (* The same names [emit.ml] gives, so a build made here links against the same
runtime and a disassembly reads with the same symbols. A Flan name can hold runtime and a disassembly reads with the same symbols. That agreement is
characters an assembler will not take bare, so every symbol is quoted. *) [Mangle]'s, which holds the prefixes for both backends; the quotes are this
one's, because a Flan name can hold characters an assembler will not take
bare. *)
let asm_sym s = "\"" ^ s ^ "\"" let asm_sym s = "\"" ^ s ^ "\""
let fsym n = asm_sym ("flan." ^ n) let fsym n = asm_sym (Mangle.sym n)
let gsym n = asm_sym ("flan." ^ n) let gsym n = asm_sym (Mangle.sym n)
(* The indirection cell: a mutable global holding the address of the function (* The indirection cell: a mutable global holding the address of the function
that is currently this name's body. Spelled exactly as [Emit.cellname] that is currently this name's body. Spelled exactly as [Emit.cellname]
@ -522,7 +524,7 @@ let gsym n = asm_sym ("flan." ^ n)
redefinition module is still built by LLVM, and it binds redefinition module is still built by LLVM, and it binds
[@"flan.cell.<n>" = external global ptr] against whatever built the host. [@"flan.cell.<n>" = external global ptr] against whatever built the host.
Byte-for-byte or the link fails and the piece served nothing. *) Byte-for-byte or the link fails and the piece served nothing. *)
let csym n = asm_sym ("flan.cell." ^ n) let csym n = asm_sym (Mangle.cell n)
(* The marker that says which backend built an image, and it is the whole of (* The marker that says which backend built an image, and it is the whole of
the answer to the one way these two backends can be mixed and be wrong. the answer to the one way these two backends can be mixed and be wrong.
@ -1043,12 +1045,15 @@ let fninfo f (fn : Tast.fn) ~nslots =
let l = rodata_label f in let l = rodata_label f in
Buffer.add_string f.rodata Buffer.add_string f.rodata
(Printf.sprintf (Printf.sprintf
"\t.section\t.data.rel.ro,\"aw\"\n\t.align 8\n%s:\n\ "\t.section\t.data.rel.ro,\"aw\"\n\t.align 8\n%s:\n%s\t.section\t.rodata\n"
\t.quad\t%s\n\t.quad\t%d\n\t.quad\t%s\n\t.quad\t%d\n\ l
\t.long\t%d\n\t.long\t%d\n\t.long\t%d\n\t.long\t0\n\ (Emit.Rt.asm_init Emit.Rt.fninfo
\t.section\t.rodata\n" [ nlbl; string_of_int nlen; llbl; string_of_int llen;
l nlbl nlen llbl llen nslots (Emit.slot_fingerprint fn) string_of_int nslots;
(Reach.ref_fingerprint ~is_global:(Hashtbl.mem f.md.Emit.globals) fn)); string_of_int (Emit.slot_fingerprint fn);
string_of_int
(Reach.ref_fingerprint ~is_global:(Hashtbl.mem f.md.Emit.globals)
fn) ]));
l l
(* The store that says "this slot is bound now", and it is the address rather (* The store that says "this slot is bound now", and it is the address rather
@ -1415,30 +1420,32 @@ let agg_tmp f (ty : Types.t) =
(* ── The runtime's two dynamic stacks ────────────────────────────────── *) (* ── The runtime's two dynamic stacks ────────────────────────────────── *)
(* [emit.ml]'s [%handler] and [%restart] types, laid out by the C rules — the (* [emit.ml]'s [%handler] and [%restart] types, measured in bytes rather than
same rules the runtime's own structs get, and the same [Emit.lay] applies to in fields, which is the only difference between what that backend needs of
everything else. Both live as frame temporaries of the function that them and what this one does. The field lists are [Emit.Rt]'s, shared so
establishes them, which is the point: the *address* of a frame is the that a field added to [flan_restart] in the C moves the offsets here
identity a transfer carries, so re-entering the same restart-case gets a without anyone's remembering to retype them; the rules are C's, the same
different one and a module loaded later cannot collide with it. *) ones [Emit.lay] applies to everything else.
(* { ptr prev, i32 type_id, ptr fn } *) Both live as frame temporaries of the function that establishes them, which
let h_size = 24 is the point: the *address* of a frame is the identity a transfer carries,
let h_type = 8 so re-entering the same restart-case gets a different one and a module
let h_fn = 16 loaded later cannot collide with it. *)
let h_size = Emit.Rt.size Emit.Rt.handler
let h_type = Emit.Rt.field Emit.Rt.handler "type"
let h_fn = Emit.Rt.field Emit.Rt.handler "fn"
(* { ptr prev, i32 name_id, ptr name, i64 namelen, ptr args, let r_size = Emit.Rt.size Emit.Rt.restart
i32 arity, i32 sig_id, i32 armed, ptr sig, i64 siglen } *) let r_field = Emit.Rt.field Emit.Rt.restart
let r_size = 72 let r_name_id = r_field "name_id"
let r_name_id = 8 let r_name = r_field "name"
let r_name = 16 let r_namelen = r_field "namelen"
let r_namelen = 24 let r_args = r_field "args"
let r_args = 32 let r_arity = r_field "arity"
let r_arity = 40 let r_sig_id = r_field "sig_id"
let r_sig_id = 44 let r_armed = r_field "armed"
let r_armed = 48 let r_sig = r_field "sig"
let r_sig = 56 let r_siglen = r_field "siglen"
let r_siglen = 64
(* ── The calling convention, as the header states it ─────────────────── *) (* ── The calling convention, as the header states it ─────────────────── *)
@ -2522,16 +2529,10 @@ and check_cast f (loc : Loc.t) (src : Types.fkind) (k : Types.ikind) =
"The range check on a float-to-integer cast. Two compares, written in the \ "The range check on a float-to-integer cast. Two compares, written in the \
directions that make a NaN fail both of them."; directions that make a NaN fail both of them.";
let f64 = (src = Types.F64) in let f64 = (src = Types.F64) in
let n = Types.bits k in (* The same four bounds [emit.ml] compares against, from the same place:
let signed = Types.signed k in the interval is the type's and both backends have to refuse the same
let lo_f = if signed then ldexp (-1.0) (n - 1) else 0.0 in values of it. Only the instructions below are this file's. *)
let hi_f = if signed then ldexp 1.0 (n - 1) else ldexp 1.0 n in let lo_f, hi_f, lo_i, hi_i = Emit.cast_range k in
let lo_i = if signed then Int64.neg (Int64.shift_left 1L (n - 1)) else 0L in
let hi_i =
if signed then Int64.sub (Int64.shift_left 1L (n - 1)) 1L
else if n = 64 then -1L
else Int64.sub (Int64.shift_left 1L n) 1L
in
let klo = float_const f lo_f ~f64 and khi = float_const f hi_f ~f64 in let klo = float_const f lo_f ~f64 and khi = float_const f hi_f ~f64 in
scoped f (fun () -> scoped f (fun () ->
let so = ptmp f and sa = ptmp f and sb = ptmp f in let so = ptmp f and sa = ptmp f and sb = ptmp f in
@ -3493,7 +3494,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
if md.Emit.dev then begin if md.Emit.dev then begin
if nslots > 0 && Array.exists (fun n -> n <> None) fn.Tast.snames then if nslots > 0 && Array.exists (fun n -> n <> None) fn.Tast.snames then
f.dslotv <- Some (alloc f (8 * nslots) 8); f.dslotv <- Some (alloc f (8 * nslots) 8);
f.dframe <- Some (alloc f 24 8) f.dframe <- Some (alloc f (Emit.Rt.size Emit.Rt.flanframe) 8)
end; end;
f.retlbl <- new_label f "ret"; f.retlbl <- new_label f "ret";
f.xfer_lbl <- new_label f "xfer"; f.xfer_lbl <- new_label f "xfer";
@ -3599,11 +3600,15 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
load_int f.b ~dst:rax ~mm:(lmem f head ~scratch:r11) ~size:8 ~signed:false; load_int f.b ~dst:rax ~mm:(lmem f head ~scratch:r11) ~size:8 ~signed:false;
store_int f.b ~src:rax ~mm:(Frame fr) ~size:8; store_int f.b ~src:rax ~mm:(Frame fr) ~size:8;
addr_into f ~reg:rax (Lg (fninfo f fn ~nslots:(if f.dslotv = None then 0 else nslots), 0)); addr_into f ~reg:rax (Lg (fninfo f fn ~nslots:(if f.dslotv = None then 0 else nslots), 0));
store_int f.b ~src:rax ~mm:(Frame (fr + 8)) ~size:8; store_int f.b
~src:rax ~mm:(Frame (fr + Emit.Rt.field Emit.Rt.flanframe "info"))
~size:8;
(match f.dslotv with (match f.dslotv with
| Some sv -> lea f.b ~dst:rax ~mm:(Frame sv) | Some sv -> lea f.b ~dst:rax ~mm:(Frame sv)
| None -> xor_rr f.b ~dst:rax ~src:rax); | None -> xor_rr f.b ~dst:rax ~src:rax);
store_int f.b ~src:rax ~mm:(Frame (fr + 16)) ~size:8; store_int f.b
~src:rax ~mm:(Frame (fr + Emit.Rt.field Emit.Rt.flanframe "slots"))
~size:8;
lea f.b ~dst:rax ~mm:(Frame fr); lea f.b ~dst:rax ~mm:(Frame fr);
store_int f.b ~src:rax ~mm:(lmem f head ~scratch:r11) ~size:8; store_int f.b ~src:rax ~mm:(lmem f head ~scratch:r11) ~size:8;
(* The parameters are bound before the body starts, so they are recorded (* The parameters are bound before the body starts, so they are recorded
@ -3869,8 +3874,8 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
this backend's rule only while this was the only backend that ran this backend's rule only while this was the only backend that ran
initialisers, and a refusal that is about the language belongs where both initialisers, and a refusal that is about the language belongs where both
backends meet it. *) backends meet it. *)
let data_sym = "\"flan..init-data\"" let data_sym = asm_sym (Mangle.sym ".init-data")
let init_sym = "\"flan..init-globals\"" let init_sym = asm_sym (Mangle.sym ".init-globals")
(* ── C's main ────────────────────────────────────────────────────────── *) (* ── C's main ────────────────────────────────────────────────────────── *)
@ -4275,7 +4280,7 @@ let emit_dwarf (dw : dwarf) ~cufile ~tbeg ~tend =
"\t.uleb128 2\n\t.asciz\t\"%s\"\n\t.asciz\t\"%s\"\n\ "\t.uleb128 2\n\t.asciz\t\"%s\"\n\t.asciz\t\"%s\"\n\
\t.uleb128 %d\n\t.uleb128 %d\n\t.quad\t%s\n\t.quad\t%s - %s\n" \t.uleb128 %d\n\t.uleb128 %d\n\t.quad\t%s\n\t.quad\t%s - %s\n"
(asm_str s.sname) (asm_str s.sname)
(asm_str ("flan." ^ s.sname)) (asm_str (Mangle.sym s.sname))
s.sfile s.sline s.ssym s.send s.ssym)) s.sfile s.sline s.ssym s.send s.ssym))
(List.rev dw.dsubs); (List.rev dw.dsubs);
Buffer.add_string out "\t.byte\t0\n.Ldwinfo_end:\n"; Buffer.add_string out "\t.byte\t0\n.Ldwinfo_end:\n";
@ -4658,8 +4663,8 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
the key is [csym]; a global is reached by its own name, so it is [gsym]. the key is [csym]; a global is reached by its own name, so it is [gsym].
The two can never collide, because a function and a global cannot share a The two can never collide, because a function and a global cannot share a
name and [gsym] and [fsym] are the same string. *) name and [gsym] and [fsym] are the same string. *)
let cellp n = asm_sym ("flan.cellp." ^ n) let cellp n = asm_sym (Mangle.cellptr n)
and gp n = asm_sym ("flan.gp." ^ n) in and gp n = asm_sym (Mangle.globalptr n) in
let slots = Hashtbl.create 8 in let slots = Hashtbl.create 8 in
List.iter (fun (f : Tast.fn) -> List.iter (fun (f : Tast.fn) ->
Hashtbl.replace slots (csym f.Tast.name) (cellp f.Tast.name)) new_fns; Hashtbl.replace slots (csym f.Tast.name) (cellp f.Tast.name)) new_fns;
@ -4736,14 +4741,14 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
let cstr sym = let l = string_const f sym in lea f.b ~dst:rdi ~mm:(Sym (l, 0)) in let cstr sym = let l = string_const f sym in lea f.b ~dst:rdi ~mm:(Sym (l, 0)) in
List.iter List.iter
(fun (fn : Tast.fn) -> (fun (fn : Tast.fn) ->
cstr ("flan." ^ fn.Tast.name); cstr (Mangle.sym fn.Tast.name);
xor_rr f.b ~dst:rax ~src:rax; xor_rr f.b ~dst:rax ~src:rax;
call_sym f.b "flan_dev_cell"; call_sym f.b "flan_dev_cell";
store_int f.b ~src:rax ~mm:(Sym (cellp fn.Tast.name, 0)) ~size:8) store_int f.b ~src:rax ~mm:(Sym (cellp fn.Tast.name, 0)) ~size:8)
new_fns; new_fns;
List.iter List.iter
(fun ((g : Tast.global), l, size, _) -> (fun ((g : Tast.global), l, size, _) ->
cstr ("flan." ^ g.Tast.gname); cstr (Mangle.sym g.Tast.gname);
movabs f.b ~dst:rsi (Int64.of_int size); movabs f.b ~dst:rsi (Int64.of_int size);
(match l with (match l with
| Some l -> lea f.b ~dst:rdx ~mm:(Sym (l, 0)) | Some l -> lea f.b ~dst:rdx ~mm:(Sym (l, 0))