diff --git a/docs/BUILT.md b/docs/BUILT.md index 7703ace3..9230c1bc 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -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, diff --git a/lib/check.ml b/lib/check.ml index 64e43869..90cfd4bc 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 diff --git a/lib/emit.ml b/lib/emit.ml index a83eb80f..08aff1c6 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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. diff --git a/lib/loc.ml b/lib/loc.ml index b99294d8..f8943c96 100644 --- a/lib/loc.ml +++ b/lib/loc.ml @@ -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 diff --git a/lib/x86.ml b/lib/x86.ml index 8202c26d..96e16580 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -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.