A short-circuit temp stops calling itself a destructuring one

fresh_temp takes the purpose, so and/or mint and~N and or~N where they used to
mint destructure~N. The names are shown -- the inspector lists a frame locals
by name -- and nothing pinned the old spelling; destructure~nth is an
unrelated compiler builtin. Confirmed in a --debug build DWARF.

The caret wart on and last operand in a want-free position is documented at
the site and in FIX.org rather than fixed: every candidate fix reads worse
than what is there, and the real fix is check_if choosing which arm to blame.
This commit is contained in:
Joseph Ferano 2026-09-20 13:00:55 +07:00
parent 9eed4cbd8c
commit 2daf7e9164

View File

@ -14,15 +14,21 @@ let sym (f : Form.t) =
| Sym s -> s | Sym s -> s
| _ -> fail f "expected a name, found %s" (Form.to_string f) | _ -> fail f "expected a name, found %s" (Form.to_string f)
(* Names for the temporaries a destructuring binding needs — the value is bound (* Names for the temporaries this file mints — the value is bound once and
once and every name in the pattern reads *that*, so a pattern over a call everything that needs it reads *that*, so a destructuring pattern over a
calls it once. [~] is a delimiter in the reader, so no symbol anyone can call calls it once and a short-circuit operand is evaluated once. [~] is a
write contains one: these cannot collide with a source name and a source delimiter in the reader, so no symbol anyone can write contains one: these
name cannot shadow one. Reset per program so the names, and therefore the cannot collide with a source name and a source name cannot shadow one.
slot numbering downstream, are the same every run. *) Reset per program so the names, and therefore the slot numbering
downstream, are the same every run.
The purpose is part of the name because these names are shown: the
inspector lists a frame's locals by name, and a short-circuit temp called
[destructure~3] is a plain lie about where it came from. One counter across
all purposes, so a name is still unique whatever minted it. *)
let temps = ref 0 let temps = ref 0
let fresh_temp () = incr temps; Printf.sprintf "destructure~%d" !temps let fresh_temp what = incr temps; Printf.sprintf "%s~%d" what !temps
(* Destructuring binds in [let] and nowhere else. Every other binding position — (* Destructuring binds in [let] and nowhere else. Every other binding position —
a [defn] parameter, a [defstruct] field, an [fn] parameter, a [dotimes] a [defn] parameter, a [defstruct] field, an [fn] parameter, a [dotimes]
@ -673,7 +679,7 @@ and bindings f (items : Form.t list) : Ast.binding list =
the reference as well as the binding is what makes the two impossible to the reference as well as the binding is what makes the two impossible to
separate by accident. *) separate by accident. *)
and temp (p : Form.t) (v : Ast.expr) : Ast.expr * Ast.binding = and temp (p : Form.t) (v : Ast.expr) : Ast.expr * Ast.binding =
let t = fresh_temp () in let t = fresh_temp "destructure" in
({ Ast.e = Ast.Var t; loc = p.loc }, ({ Ast.e = Ast.Var t; loc = p.loc },
{ Ast.bname = t; bty = None; bval = v; bloc = p.loc }) { Ast.bname = t; bty = None; bval = v; bloc = p.loc })
@ -915,7 +921,24 @@ and cond f (args : Form.t list) : Ast.expr =
because the sentinel it answered was a bool literal that boxed to fit because the sentinel it answered was a bool literal that boxed to fit
whatever the real branch was; neither is now, and they are at least whatever the real branch was; neither is now, and they are at least
symmetric about it. Making bool and dyn arms join as dyn is a check_if symmetric about it. Making bool and dyn arms join as dyn is a check_if
question, noted in FIX.org under item 7 and not decided here. *) question, noted in FIX.org under item 7 and not decided here.
One known wart, measured rather than guessed, and left alone deliberately.
In a want-free position [(println (and true true (vec-new i32)))] the
caret lands on the *second* [true] and not on the vec: the last operand is
the then arm, check_if types the then arm first, and the mismatch is
therefore reported against the else arm, which is the previous operand's
temp. An operand anywhere but last is a condition instead, so check_truthy
blames it at its own loc and the caret is right; [or] is right everywhere,
because there the chain and not the sentinel sits in the else arm. Giving
the else arm's [Var] node the *last* operand's loc moves the caret onto the
vec and makes the sentence read backwards "expected (Vec i32), found
bool" under a caret on the thing that is the (Vec i32) — so it is not an
improvement; answering a bool literal again would revert the paragraph
above; and inverting the condition to move the last operand into the else
arm buys a [not] per operand and worse locs than it fixes. What would
actually fix it is check_if preferring the arm that is not a compiler temp
when it reports, which is check.ml's call. Written up in FIX.org. *)
and shortcircuit f (args : Form.t list) ~is_and : Ast.expr = and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
let mk e = { Ast.e; loc = f.loc } in let mk e = { Ast.e; loc = f.loc } in
let rec go = function let rec go = function
@ -923,7 +946,7 @@ and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
| [ last ] -> expr last | [ last ] -> expr last
| x :: rest -> | x :: rest ->
let ex = expr x in let ex = expr x in
let t = fresh_temp () in let t = fresh_temp (if is_and then "and" else "or") in
let tvar = { Ast.e = Ast.Var t; loc = ex.Ast.loc } in let tvar = { Ast.e = Ast.Var t; loc = ex.Ast.loc } in
let bind = { Ast.bname = t; bty = None; bval = ex; bloc = ex.Ast.loc } in let bind = { Ast.bname = t; bty = None; bval = ex; bloc = ex.Ast.loc } in
let rest = go rest in let rest = go rest in