Three review findings on M2 item 4, none blocking
(Some nil) at an already-(Option T) want reported expect's bare-T sentence instead of its own: checking the argument against inner's element type routed a literal nil through expect's "wrap the type in Option" refusal before Some's own is_nil_lit guard ever ran, and the advice was nonsense there — the type already is one. The argument is now checked with no want when it is syntactically nil, which is what a bare nil resolves against on its own, so it arrives at Some's own check still dyn and still nil. no_fallback_slots cannot see the slot unbox_option mints: %dx/%ax are the pool-ran-dry fallback for a temporary root_plan counted, and a named local root_plan never counted emits neither mark. The comment where nil-option.flan and some-nil.flan were added to that list said otherwise; corrected to say what the check does and does not cover, and to record that the slot was verified by reading the IR directly instead — flan.unbox-opt's dyn slot is pushed, flan.as-dyn's is a plain alloca correctly, since its element is always scalar there. dyn_offsets falls through Option/Vec/Map with no arm of its own, correct today only because Check.hidden_dyn refuses a dyn inside any of them at every storage site first. Commented at the fallthrough, naming hidden_dyn as the gate and the typed-container view (M2 item 3, in review now) as the kind of change that could relax it for Vec/Map without anything here pointing back.
This commit is contained in:
parent
c71ae8020c
commit
221df5af1c
10
lib/check.ml
10
lib/check.ml
@ -5547,8 +5547,16 @@ and named_call ctx ~want loc name args =
|
|||||||
does. *)
|
does. *)
|
||||||
| "Some" ->
|
| "Some" ->
|
||||||
arity loc name 1 args;
|
arity loc name 1 args;
|
||||||
|
let arg = List.hd args in
|
||||||
let inner = match want with Some (Types.Option t) -> Some t | _ -> None in
|
let inner = match want with Some (Types.Option t) -> Some t | _ -> None in
|
||||||
let a = check ctx ?want:inner (List.hd args) in
|
(* A literal [nil] is refused by this form's own message below, not by
|
||||||
|
[expect]'s bare-T refusal — checking it against [inner] here would
|
||||||
|
let a bare T's "wrap the type in Option" reach the reader even though
|
||||||
|
the type here is already wrapped in one. Checked with no want instead,
|
||||||
|
which is exactly what a bare [nil] resolves against on its own (see
|
||||||
|
[var]'s "nil" arm), so it arrives below still dyn and still nil. *)
|
||||||
|
let ast_nil = match arg.Ast.e with Ast.Var "nil" -> true | _ -> false in
|
||||||
|
let a = check ctx ?want:(if ast_nil then None else inner) arg in
|
||||||
let a =
|
let a =
|
||||||
if not (Types.equal a.Tast.ty Types.Dyn) then a
|
if not (Types.equal a.Tast.ty Types.Dyn) then a
|
||||||
else if is_nil_lit a then
|
else if is_nil_lit a then
|
||||||
|
|||||||
10
lib/emit.ml
10
lib/emit.ml
@ -451,6 +451,16 @@ and dyn_offsets m (t : Types.t) : int list =
|
|||||||
— which is a run-time question a static descriptor cannot answer.
|
— which is a run-time question a static descriptor cannot answer.
|
||||||
Refused in [Check] rather than described wrongly here. *)
|
Refused in [Check] rather than described wrongly here. *)
|
||||||
| None -> acc)
|
| None -> acc)
|
||||||
|
(* [Types.Option], [Types.Vec] and [Types.Map] fall through here with no
|
||||||
|
arm of their own and answer no offsets, which is correct only because
|
||||||
|
nothing reaches this function holding one with a dyn inside it:
|
||||||
|
[Check.hidden_dyn] refuses that at every global, parameter, return and
|
||||||
|
frame slot first. If that gate is ever relaxed — the typed-container
|
||||||
|
view the M2 queue's item 3 is building is exactly the kind of change
|
||||||
|
that would relax it for [Vec]/[Map] — this arm has to grow alongside
|
||||||
|
it, the way the array and struct arms above already walk their own
|
||||||
|
storage; until then a silent [] here would be an unrooted dyn, not a
|
||||||
|
refusal. *)
|
||||||
| _ -> acc
|
| _ -> acc
|
||||||
in
|
in
|
||||||
List.sort_uniq compare (go [] 0 t [])
|
List.sort_uniq compare (go [] 0 t [])
|
||||||
|
|||||||
@ -3773,11 +3773,19 @@ level "1"
|
|||||||
"programs/dyn-struct.flan";
|
"programs/dyn-struct.flan";
|
||||||
"programs/dyn-global.flan"; "programs/dyn-boundary.flan";
|
"programs/dyn-global.flan"; "programs/dyn-boundary.flan";
|
||||||
"programs/dyn-defer.flan";
|
"programs/dyn-defer.flan";
|
||||||
(* [unbox_option] binds the dyn it is testing to a fresh slot before
|
(* Included for the same reason every dyn program is, though this
|
||||||
reading its tag twice, and that slot is the one new place M2 item
|
check cannot see the slot M2 item 4 actually added: [%dx]/[%ax]
|
||||||
4 mints a dyn temporary the collector has to find — the same
|
are the pool-ran-dry fallback for a temporary [root_plan] COUNTED
|
||||||
question this list already asks of every other dyn-producing
|
and could not find room for, and [unbox_option]'s fresh slot is a
|
||||||
boundary. *)
|
named local [root_plan] never counted at all — a slot outside
|
||||||
|
that pool emits neither mark and this grep is silent about it
|
||||||
|
either way. That slot was checked by reading the IR directly
|
||||||
|
instead: [flan.unbox-opt]'s dyn slot is pushed with
|
||||||
|
[flan_dyn_root_push]; [flan.as-dyn]'s slot is a plain alloca with
|
||||||
|
none, correct because its element type is always scalar there.
|
||||||
|
This row is still worth keeping — it is real evidence about
|
||||||
|
every *other* dyn temporary these two programs mint through the
|
||||||
|
ordinary call-argument and return-value paths. *)
|
||||||
"programs/nil-option.flan"; "programs/some-nil.flan" ];
|
"programs/nil-option.flan"; "programs/some-nil.flan" ];
|
||||||
(* And that the defer program still runs and still runs its defer: the
|
(* And that the defer program still runs and still runs its defer: the
|
||||||
count being right is not much use if the transfer path broke getting
|
count being right is not much use if the transfer path broke getting
|
||||||
|
|||||||
@ -1205,6 +1205,16 @@ let () =
|
|||||||
rejects_check "(Some nil) is refused at compile time"
|
rejects_check "(Some nil) is refused at compile time"
|
||||||
"(defn main [] i32 (let [o (Some nil)] 0))"
|
"(defn main [] i32 (let [o (Some nil)] 0))"
|
||||||
~needle:"(Some nil) cannot be built";
|
~needle:"(Some nil) cannot be built";
|
||||||
|
(* The same refusal where the argument's own want is already (Option T)'s
|
||||||
|
inner type — a function parameter typed (Option i64), say. [nil] checked
|
||||||
|
against that inner type directly would hit [expect]'s bare-T refusal
|
||||||
|
first ("wrap the type in Option"), which is nonsense here: the type
|
||||||
|
already is one. A regression for the review that found it. *)
|
||||||
|
rejects_check "(Some nil) at an already-Option want gets Some's message, \
|
||||||
|
not expect's bare-T one"
|
||||||
|
"(defn f [o (Option i64)] i64 (match o (Some v) v None -1))\n\
|
||||||
|
(defn main [] i32 (f (Some nil)))"
|
||||||
|
~needle:"(Some nil) cannot be built";
|
||||||
(* (Option (Option T)) is legal on the typed side — nothing above refuses
|
(* (Option (Option T)) is legal on the typed side — nothing above refuses
|
||||||
the type — but boxing its Some of an inner None would box that None as
|
the type — but boxing its Some of an inner None would box that None as
|
||||||
nil, indistinguishable from the outer None, so the crossing into dyn
|
nil, indistinguishable from the outer None, so the crossing into dyn
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user