No top-level value in the compiler goes unreferenced

This commit is contained in:
Joseph Ferano 2026-09-25 10:16:25 +07:00
parent 3636f31cba
commit e511174d3c
5 changed files with 13 additions and 69 deletions

View File

@ -5478,7 +5478,7 @@ error that could actually be clicked.
The source cache in `loc.ml` is process-lifetime, which is right for `flan build` — a fresh process per run. The
daemon is long-lived and never calls `report`; the interactive path draws no squiggle, it takes a location and a
message. `Loc.forget_sources` exists for the day that changes.
message. A daemon that did draw one would have to reset the cache when a file changes.
### Collecting, and where it stops
@ -5504,7 +5504,7 @@ file-shaped pile of nonsense. First error, stop. That is a decision, not an omis
Changing the error type without touching `dev.ml` and `session.ml` needed a compatible way to get one location and
one message out. The answer is that **the single-diagnostic exception is still the single-diagnostic exception**.
`Session.eval` and the daemon evaluate one form and have one failure to report; they keep catching `Loc.Error` and
take the pair out of it with `Loc.summary`. Only a driver that compiles a whole file raises `Loc.Errors`.
read `dloc` and `dmsg` out of it. Only a driver that compiles a whole file raises `Loc.Errors`.
That guarantee is **structural and not conventional**. `Parse.program` / `Check.program` stop at the first refusal;
`Parse.program_all` / `Check.program_all` collect. Two names rather than one function with a `~keep_going` label,

View File

@ -747,10 +747,6 @@ and captured_set ctx loc name =
"Return the new value, or keep it in a local of this fn")
| None -> ()
(* A binding this body captured, as opposed to one it declared. Used where the
difference matters and nowhere else. *)
let is_captured ctx name = List.mem_assoc name ctx.caught
let scoped ctx f =
let saved = ctx.scope in
let r = f () in
@ -2338,11 +2334,6 @@ let widen loc (want : Types.t) (e : Tast.expr) =
if Types.equal want e.Tast.ty then e
else mk loc want (Tast.Prim (Tast.Cast want, [ e ]))
let unboxable t =
match t with
| Types.Int Types.I64 | Types.Float Types.F64 | Types.Bool -> true
| _ -> false
(* The sentence a refusal at this boundary gives. It names the type and says
which direction failed, because "expected dyn, found (Vec i64)" would read
as a type error the programmer could fix by writing something else, and
@ -2353,7 +2344,7 @@ let no_dyn_yet loc ~into t extra =
(Types.to_string t) (if into then "dyn" else "a written type") extra
(* M2 item 3: a typed container crossing into dyn as a view. The element set
is exactly [unboxable] above — i64, f64, bool — and that is not a smaller
is exactly the unboxable scalars — i64, f64, bool — and that is not a smaller
version of the same cut for the same reason: every other element type
would need [box] to run on IT too, and a string element's dyn form is a
pointer into the collector's heap, while a typed container's storage is
@ -3416,7 +3407,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
runtime owns the storage the way (vec-new dyn) does, keys and values are
both dyn words, and a typed want other than dyn refuses through [expect]
like any other dyn value would. The literal lowers to a fresh slot — a
rooted one, because a slot of type dyn is what [dyn_roots] counts — so
rooted one, because a slot of type dyn is what [Emit.root_plan] counts — so
the map stays reachable across the allocations its own entries make. *)
| Ast.MapLit (tag, kvs) ->
let m = fresh_slot ctx Types.Dyn in

View File

@ -35,8 +35,6 @@
through a [(Ptr Cursor)] becomes a [getelementptr] on the pointer, not on
a copy of the struct. *)
let fail = Loc.fail
(* The assertions below this line are not diagnostics. Every one of them says
the checker admitted something it refuses — a type with no layout, a case
that is not a case of its data type, arithmetic on a struct — so no program
@ -112,11 +110,6 @@ let xfer_param = "%xfer"
to have had all along. *)
let env_param = "%env"
(* What a call through a [(Fn ...)] value passes when it has no environment —
a value made out of a name, or one widened from a [CFn]. Spelled once so
the sites cannot drift. *)
let no_env = "ptr null"
(* 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) =
@ -936,7 +929,7 @@ type f = {
It is a count and not a saved depth because the ABI offers
[flan_dyn_root_pop(n)] and no way to read the stack's height; it can be a
count, rather than needing one, because the number is a static property of
the function that [dyn_roots] works out before a line of the body is
the function that [root_plan] works out before a line of the body is
emitted. That matters: [ret] runs *during* emission, and a count
accumulated as roots were discovered would be short at every early
return. *)
@ -1370,16 +1363,12 @@ let root_plan m (fn : Tast.fn) : rootplan =
!agg;
rpins = !pins }
let dyn_roots m (fn : Tast.fn) =
let p = root_plan m fn in
List.length p.rslots + p.rdyn + List.length p.ragg
(* The next pre-made root slot for a dyn temporary. They are all minted, zeroed
and pushed in the entry block before a line of the body is emitted, and this
only hands them out — which is what makes the pushes and the pops balance by
construction rather than by the body being walked the same way twice.
[dyn_roots] counts the same nodes the emission visits, so the supply runs
[root_plan] counts the same nodes the emission visits, so the supply runs
out only if those two disagree. If it ever does, the fallback is an ordinary
unrooted slot: one temporary the collector cannot see is a bug to find,
where a root stack that pops more than it pushed is memory corruption. *)
@ -3357,7 +3346,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
(* A dyn word is spilled into a rooted slot the instant it exists. It is
an SSA value otherwise, and an SSA value is invisible to a collector
that finds its roots by address — the next allocation could be the one
that frees what this is holding. [dyn_roots] counted this call, so the
that frees what this is holding. [root_plan] counted this call, so the
slot below is one the entry block has already pushed.
The value carries on being used as a register: the store is what the
@ -3574,7 +3563,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
a debugging convenience and a release build does without it, while a
collector that cannot find its roots is a collector that frees live
values. Every build pays this, and only a function that has a dyn in it
pays anything — [dyn_roots] is zero otherwise and not a line is emitted,
pays anything — [root_plan] is empty otherwise and not a line is emitted,
which is what makes an annotated program's IR identical with and without
--no-gc.

View File

@ -143,10 +143,6 @@ exception Error of diag
and never raised by a path that checks a single form. *)
exception Errors of diag list
(** The one location and one message a caller with a single line to print gets
out of a diagnostic. Notes are dropped here on purpose. *)
let summary (d : diag) = (d.dloc, d.dmsg)
let before (a : t) (b : t) =
if a.line <> b.line then compare a.line b.line else compare a.col b.col
@ -210,8 +206,6 @@ let caught s f =
| x -> Some x
| exception Error d -> s.found <- d :: s.found; None
let any s = s.found <> []
(** Raise everything found, in the order it was found, or return if the pass
was clean. *)
let finish s =
@ -255,8 +249,6 @@ let lines_of file =
Hashtbl.replace source_cache file v;
v
let forget_sources () = Hashtbl.reset source_cache
let source_line (t : t) =
if t.line <= 0 then None
else

View File

@ -355,7 +355,6 @@ let and_imm b ~dst n = grp1_imm b ~ext:4 ~dst n
let sub_imm b ~dst n = grp1_imm b ~ext:5 ~dst n
let cmp_imm b ~dst n = grp1_imm b ~ext:7 ~dst n
let neg_r b ~dst = rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xf7; modrm_r b ~r:3 ~m:dst
let not_r b ~dst = rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xf7; modrm_r b ~r:2 ~m:dst
let test_rr b ~a ~c = rex b ~w:true ~r:c ~x:0 ~m:a; u8 b 0x85; modrm_r b ~r:c ~m:a
@ -696,7 +695,7 @@ type fnctx = {
A count rather than a running tally for [emit.ml]'s reason: the epilogue
is emitted after the body, but the pushes are decided before it, by
[Emit.dyn_roots], which is deliberately the *same* function both backends
[Emit.root_plan], which is deliberately the *same* function both backends
call. The pushes and the pops balance because one counter decides both
ends, and the two backends root the same nodes because there is one
counter and not two. *)
@ -943,7 +942,7 @@ let scoped f g =
(* ── Moving values ───────────────────────────────────────────────────── *)
(* Scalar in [reg] <- [rbp+off], and back. A bool is a byte; everything else
(* Scalar in [reg] <- [rbp+off]. A bool is a byte; everything else
is its own width, widened on load. *)
let load_scalar f ~reg ~off (t : Types.t) =
if is_float t then fload f.b ~dst:reg ~mm:(Frame off) ~f64:(f64_of t)
@ -951,26 +950,6 @@ let load_scalar f ~reg ~off (t : Types.t) =
let size = match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md t) in
load_int f.b ~dst:reg ~mm:(Frame off) ~size ~signed:(signed_of t)
let store_scalar f ~reg ~off (t : Types.t) =
if is_float t then fstore f.b ~src:reg ~mm:(Frame off) ~f64:(f64_of t)
else
let size = match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md t) in
store_int f.b ~src:reg ~mm:(Frame off) ~size
(* Through a pointer rather than a frame offset: the same two, with the
address already in a register. *)
let load_scalar_at f ~reg ~base ~disp (t : Types.t) =
if is_float t then fload f.b ~dst:reg ~mm:(Reg (base, disp)) ~f64:(f64_of t)
else
let size = match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md t) in
load_int f.b ~dst:reg ~mm:(Reg (base, disp)) ~size ~signed:(signed_of t)
let store_scalar_at f ~reg ~base ~disp (t : Types.t) =
if is_float t then fstore f.b ~src:reg ~mm:(Reg (base, disp)) ~f64:(f64_of t)
else
let size = match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md t) in
store_int f.b ~src:reg ~mm:(Reg (base, disp)) ~size
(* n bytes from the address in rsi to the address in rdi. *)
let blockcopy f n =
if n > 0 then begin
@ -981,13 +960,6 @@ let blockcopy f n =
rep_movsb f.b
end
let copy_frames f ~dst ~src n =
if n > 0 then begin
lea f.b ~dst:rdi ~mm:(Frame dst);
lea f.b ~dst:rsi ~mm:(Frame src);
blockcopy f n
end
let zero_frame f ~dst n =
if n > 0 then begin
note f (Printf.sprintf "rep stosb: %d bytes of zero, which is what this backend \
@ -1448,7 +1420,7 @@ let with_pad f tag g =
hands them out, which is what makes the pushes and the pops balance by
construction rather than by the body being walked the same way twice.
[Emit.dyn_roots] counts the same nodes this emission visits, so the supply
[Emit.root_plan] counts the same nodes this emission visits, so the supply
runs out only if those two disagree — and since both backends call that one
function, disagreeing would be one of them visiting a node the other does
not. The fallback is an ordinary unrooted temporary, for [emit.ml]'s
@ -3006,7 +2978,7 @@ and call_rt f ~sym ~args ~rty dst =
the collector finds its roots by address. [dst] is not enough: it is
often a temporary inside a [scoped] that the bump allocator is about to
hand out again, and it is never a slot anything was pushed for.
[Emit.dyn_roots] counted this call, so the slot below is one the entry
[Emit.root_plan] counted this call, so the slot below is one the entry
block has already zeroed and pushed.
Here rather than in [call_native], which is [call_c]'s as well: a dyn
@ -3744,7 +3716,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
everything below it: the shadow stack is a debugging convenience a release
build does without, while a collector that cannot find its roots is a
collector that frees live values. Only a function with a dyn in it pays
anything, because [Emit.dyn_roots] is zero otherwise and not an
anything, because [Emit.root_plan] is empty otherwise and not an
instruction is emitted — which is what keeps every dyn-free program in the
survey byte for byte what it was before this lane.