diff --git a/lib/check.ml b/lib/check.ml index afba075..df6400b 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -5547,8 +5547,16 @@ and named_call ctx ~want loc name args = does. *) | "Some" -> 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 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 = if not (Types.equal a.Tast.ty Types.Dyn) then a else if is_nil_lit a then diff --git a/lib/emit.ml b/lib/emit.ml index a4d05c8..049fbc4 100644 --- a/lib/emit.ml +++ b/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. Refused in [Check] rather than described wrongly here. *) | 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 in List.sort_uniq compare (go [] 0 t []) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 6d1a7e7..778396c 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -3773,11 +3773,19 @@ level "1" "programs/dyn-struct.flan"; "programs/dyn-global.flan"; "programs/dyn-boundary.flan"; "programs/dyn-defer.flan"; - (* [unbox_option] binds the dyn it is testing to a fresh slot before - reading its tag twice, and that slot is the one new place M2 item - 4 mints a dyn temporary the collector has to find — the same - question this list already asks of every other dyn-producing - boundary. *) + (* Included for the same reason every dyn program is, though this + check cannot see the slot M2 item 4 actually added: [%dx]/[%ax] + are the pool-ran-dry fallback for a temporary [root_plan] COUNTED + and could not find room for, and [unbox_option]'s fresh slot is a + 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" ]; (* 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 diff --git a/test/test_flan.ml b/test/test_flan.ml index 9640ab1..d9174ec 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1205,6 +1205,16 @@ let () = rejects_check "(Some nil) is refused at compile time" "(defn main [] i32 (let [o (Some nil)] 0))" ~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 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