From cb60df1dc06cfe5969fea66642d97ff81962880d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 17:11:12 +0700 Subject: [PATCH] A T is wrapped in Some wherever a T? is wanted, one level at a time and never unwrapped back. --- TODO.org | 4 + lib/check.ml | 151 +++++++++++++++++++++++++++++++++++-- spec-syntax.md | 9 +++ test/programs/autowrap.fln | 85 +++++++++++++++++++++ test/test_acceptance.ml | 4 + test/test_flan.ml | 17 +++++ 6 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 test/programs/autowrap.fln diff --git a/TODO.org b/TODO.org index 6fc9c65a..5c72e615 100644 --- a/TODO.org +++ b/TODO.org @@ -39,6 +39,10 @@ Decided (133): =x?= is a bool; =if x?=, =elif x?=, =while x?= and the rest of an make a local Option its payload in the block, in place (not a copy). Assigning an Option to it there is refused rather than ending the narrowing; =e? as g= names what a test found. Rules out =if let g = x= over a plain name, which is refused toward these. +** DONE A T is wrapped where a T? is wanted +CLOSED: [2026-09-26] +Decided (138), like Swift: one level per boundary, the literal built at T first; not +inside a container. Rules out implicit unwrapping: a T? where a T is wanted stays refused. ** TODO The stepper does not step inside an optional chain =Ast.step_expr= treats a =Chain= as a leaf (its catch-all), so nothing in a chain's body gets a step point of its own. diff --git a/lib/check.ml b/lib/check.ml index 48360e96..55f8206f 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -2891,6 +2891,13 @@ let rec bind_ty ?(widen = false) ?(ro = true) subst (pat : Types.t) bind_ty ~ro:(m = Types.Const) subst p a | Types.Vec p, Types.Vec a | Types.Option p, Types.Option a -> inner p a + (* A plain value at a [$t?] parameter binds [$t] to its own type, and + [expect] wraps it (decision 138). At the top of an argument only, as the + widening below: an element of a Vec is never wrapped, so [(Vec $t?)] + meets a [(Vec i32)] as a mismatch. *) + | Types.Option p, a + when widen && (match a with Types.Dyn | Types.Never -> false | _ -> true) -> + bind_ty ~widen subst p a | Types.Array (n, p), Types.Array (m, a) -> Int64.equal n m && inner p a | Types.Map (k, v), Types.Map (k', v') -> inner k k' && inner v v' (* Each function type against its own. *) @@ -3227,6 +3234,10 @@ let rec literal_arith (e : Ast.expr) : int64 option = over literals alone. *) let lone_literal (e : Ast.expr) = is_literal e || literal_arith e <> None +(* [None] as written, which has no type until an Option is asked of it. *) +let is_none_lit (e : Ast.expr) = + match e.Ast.e with Ast.Var "None" -> true | _ -> false + (* A value whose type comes only from defaults — a literal, [nil], [(Some 3)], arithmetic over literals, a [do] ending in one — so it takes the type of whatever meets it. An arm of this kind is checked after the others, at their type, @@ -4960,7 +4971,7 @@ let const_note ?(fln = false) env ~(want : Types.t) ~(got : Types.t) = (Types.spell ~indented:fln want) (Types.spell ~indented:fln got) | _ -> "" -let expect ctx loc ~want (got : Tast.expr) = +let rec expect ctx loc ~want (got : Tast.expr) = match want with | None -> got | Some w -> @@ -5033,6 +5044,21 @@ let expect ctx loc ~want (got : Tast.expr) = | (Types.Slice (Types.Const, _) | Types.Ptr (Types.Const, _)), _ when Types.const_widens ~from:got.Tast.ty ~into:w -> { got with Tast.ty = w } + (* Decision 138, Swift's rule: a T where a (Option T) is wanted is + [Some] of it. One level each time — a T? into a T?? is [Some] of the + Option, never the Option itself — and the payload goes through this + same boundary first, so a T into a T?? is [Some (Some t)] and an i32 + into an (Option i64) is widened, then wrapped. Never the other way: + a T? where a T is wanted is still refused, and a dyn is left to the + nil <-> None arms above. When the payload is refused too, the + refusal below names the Option, as it did before. *) + | Types.Option t, g + when (match g with Types.Dyn | Types.Never -> false | _ -> true) + && not (Types.fits ~expected:w ~actual:g) -> + (match expect ctx loc ~want:(Some t) got with + | v when Types.fits ~expected:t ~actual:v.Tast.ty -> mk loc w (Tast.Some_ v) + | _ -> got + | exception Loc.Error _ -> got) | _ -> got in if Types.fits ~expected:w ~actual:got.Tast.ty then got @@ -5133,8 +5159,16 @@ let arm_join (a : Types.t) (b : Types.t) = match Types.const_join a b with | Some j -> Some j | None -> + (* A T beside a T? meets at the T?, the T wrapped in [Some] + (decision 138). Only the plain side moves, and by one level. *) + let wraps p u = + (match u with Types.Option _ | Types.Unit -> false | _ -> true) + && (Types.equal p u || Types.widens_to ~from:u ~into:p) + in (match a, b with | Types.Dyn, _ | _, Types.Dyn -> Some Types.Dyn + | Types.Option p, u when wraps p u -> Some a + | u, Types.Option p when wraps p u -> Some b | _ -> None) (* The type two untyped literals meet at: the wider of their own types, and an integer beside a float at the float — [(if c 1 2.5)] is an f32, though @@ -6095,6 +6129,18 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr = let used = ctx.used || List.memq e ctx.kept in ctx.used <- false; match e.Ast.e with + (* A literal where an (Option T) is wanted is built at T and then wrapped + (decision 138): [s = -1] over an [i64?] is [Some] of an i64 -1. It has + no type until one is asked of it, so it is asked the payload's, rather + than being built at a default and wrapped at the wrong width. *) + | Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ | Ast.Call _ | Ast.Arr (_ :: _) + when (match want with Some (Types.Option _) -> true | _ -> false) + && (lone_literal e || (match e.Ast.e with Ast.Arr _ -> true | _ -> false)) -> + let w = Option.get want in + let t = match w with Types.Option t -> t | _ -> assert false in + let v = check ctx ~want:t e in + if Types.fits ~expected:t ~actual:v.Tast.ty then mk loc w (Tast.Some_ v) + else expect ctx loc ~want v (* A negative literal in a generic body, at an instantiation that made it unsigned. The cast the ordinary refusal names would be wrong at every other type the function is called at, so the fix is one that needs no @@ -8571,6 +8617,30 @@ and check_if_once ctx ~tail ~used ?want loc c t e = expect ctx loc ~want (mk loc Types.Dyn (Tast.If (c, t, e))) | ty -> let oty = Types.Option ty in + (* A later arm that is a T? where this one is a T: the arms meet at + T? (decision 138), so the chain is a T??, as it is when the T? + arm comes first. Tried only where the arm is refused at T. *) + let nested = + match want, ty with + | None, (Types.Option _ | Types.Unit) -> None + | None, _ -> + (match trial ctx (fun () -> rest ~used:true ~want:oty ()) with + | Ok e -> Some (Error e) + | Error _ -> + let ooty = Types.Option oty in + (match trial ctx (fun () -> rest ~used:true ~want:ooty ()) with + | Ok e -> Some (Ok e) + | Error _ -> None)) + | _ -> None + in + match nested with + | Some (Ok e) -> + let ooty = Types.Option oty in + let t = mk loc ooty (Tast.Some_ (mk loc oty (Tast.Some_ t))) in + mk loc ooty (Tast.If (c, t, e)) + | Some (Error e) -> + mk loc oty (Tast.If (c, mk loc oty (Tast.Some_ t), e)) + | None -> let e = rest ~used:true ~want:oty () in expect ctx loc ~want (mk loc oty (Tast.If (c, mk loc oty (Tast.Some_ t), e)))) @@ -8585,14 +8655,30 @@ and check_if_once ctx ~tail ~used ?want loc c t e = let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)) in let e = branch ctx (fun () -> in_tail (fun () -> check ctx ?want e)) in mk loc t.Tast.ty (Tast.If (c, t, e)) - | Some e when want = None && adapts t && not (adapts e) + | Some e when want = None && (adapts t || is_none_lit t) + && not (adapts e || is_none_lit e) && not (and_sentinel e) -> (* A literal has no type of its own until something asks, so with no expectation the other arm decides: [(if c 4000000 n)] over an i64 [n] is an i64, as [(+ 4000000 n)] is. *) let e = branch ctx (fun () -> in_tail (fun () -> check ctx e)) in let twant = if e.Tast.ty = Types.Never then None else Some e.Tast.ty in - let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:twant t)) in + let then_at w = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:w t)) in + (* [None] or [Some(1)] beside a plain T: the two meet at T?, the other + arm wrapped (decision 138). Tried only once the arm is refused at T, + so an arm that fits T is never an Option. *) + let t, e = + match e.Tast.ty with + | Types.Option _ | Types.Dyn | Types.Unit | Types.Never -> then_at twant, e + | ety -> + (match trial ctx (fun () -> then_at twant) with + | Ok t -> t, e + | Error _ -> + let oty = Types.Option ety in + (match trial ctx (fun () -> then_at (Some oty)) with + | Ok t -> t, expect ctx e.Tast.loc ~want:(Some oty) e + | Error _ -> then_at twant, e)) + in let ty = if e.Tast.ty = Types.Never then t.Tast.ty else e.Tast.ty in mk loc ty (Tast.If (c, t, e)) | Some e -> @@ -8656,7 +8742,24 @@ and check_if_once ctx ~tail ~used ?want loc c t e = | Some j -> Some (j, expect ctx v.Tast.loc ~want:(Some j) v) | None -> None in - match at_then () with + (* The else arm refused at T and fine at T? — [None], [Some(1)] — + and the two meet at T?, the then arm wrapped (decision 138). *) + let at_option () = + match t.Tast.ty with + | Types.Option _ | Types.Dyn | Types.Unit -> None + | ty -> + let oty = Types.Option ty in + (match + trial ctx (fun () -> + branch ctx (fun () -> in_tail (fun () -> check ctx ~want:oty e))) + with + | Ok v when Types.equal v.Tast.ty oty -> Some (oty, v) + | _ -> None) + in + (* Only where the arms met nowhere else, so nothing that met before + meets differently: a dyn else arm still meets at dyn. *) + match + (match at_then () with | Ok v -> (match opened_dyn ~box:(to_dyn ctx) v with | Some box -> Some (Types.Dyn, box) @@ -8683,7 +8786,10 @@ and check_if_once ctx ~tail ~used ?want loc c t e = | Some r -> Some r | None -> Some (t.Tast.ty, expect ctx v.Tast.loc ~want:(Some t.Tast.ty) v)) - | Error _ -> None) + | Error _ -> None)) + with + | None -> at_option () + | j -> j in match joined with | Some (j, v) -> @@ -9731,6 +9837,14 @@ and check_the ctx ~want loc (t : Ast.texpr) (v : Ast.expr) = (* A keyword naming one of an enum's members is that member at the enum's type, not a dyn: [let d: Dir = :north]. *) && not (match ty, v.Ast.e with Types.Enum _, Ast.Kw _ -> true | _ -> false) + (* An array literal that is a dyn vector only because its elements do + not agree among themselves — [[1, None]] — is built at the annotation + when every element fits it, as [x: [2 i32?] = [1, None]] (decision + 138). Nothing is converted: the literal is built at T. *) + && not (match v.Ast.e, ty with + | Ast.Arr _, (Types.Array (_, Types.Option _) | Types.Slice (_, Types.Option _)) -> + probe ctx loc (fun () -> ignore (check ctx ~want:ty v)) <> None + | _ -> false) then begin let tn = tyname loc ty in let numeric = match ty with Types.Int _ | Types.Float _ -> true | _ -> false in @@ -10319,6 +10433,19 @@ and check_match ctx ?(tail = false) ?(used = false) ?(stmt = false) ?(opt = fals (head, (ctx.scope, ctx.ret), w, d); Error d) in + (* Where it would be refused: an arm fine at T? — [None], + [Some(1)] — meets a T join at T? (decision 138), and + [arm_join] wraps the arms before it. *) + let refused () = + let fallback () = at !want () in + match w with + | Types.Option _ | Types.Dyn | Types.Unit -> fallback () + | _ -> + let oty = Types.Option w in + (match trial ctx (at (Some oty)) with + | Ok b when Types.equal b.Tast.ty oty -> b + | _ -> fallback ()) + in match at_join () with | Ok b -> (match opened_dyn ~box:(to_dyn ctx) b with @@ -10334,10 +10461,10 @@ and check_match ctx ?(tail = false) ?(used = false) ?(stmt = false) ?(opt = fals with | Ok b -> b | Error own when String.equal own.Loc.kind not_kept -> - at !want () + refused () | Error own when is_mismatch d && not (is_mismatch own) -> at None () - | Error _ -> at !want ()) + | Error _ -> refused ()) else block ctx ?want:!want a.Ast.aloc a.Ast.body in let body = @@ -16610,6 +16737,12 @@ and generic_call ctx ~want loc name vars pats pret args = call with no type variables in it. *) | _ -> (match subst_ty !subst pat, a.Tast.ty with + (* A plain value at a [$t?] parameter, which [bind_ty] bound + through the Option: wrapped now that $t is known (decision + 138). *) + | Types.Option _ as o, at + when (match at with Types.Option _ | Types.Dyn | Types.Never -> false | _ -> true) -> + expect ctx a.Tast.loc ~want:(Some o) a | Types.Fn (ps, r), Types.CFn (ps', r') -> if Types.equal (Types.Fn (ps, r)) (Types.Fn (ps', r')) then mk a.Tast.loc (Types.Fn (ps, r)) @@ -17565,7 +17698,9 @@ let builtins : (string * string * string) list = (* Option *) ("Some", "Some [T] (Option T)", "Wraps a value as a present Option. None is the other half, and is \ - written as a name rather than as a call."); + written as a name rather than as a call. Where an (Option T) is \ + expected a T is wrapped with no Some written, one level at a time; an \ + Option is never unwrapped that way."); (* the host primitives *) ("bytes", "bytes [str Allocator?] [u8]", diff --git a/spec-syntax.md b/spec-syntax.md index d6b13f7d..3b45f7b0 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -217,6 +217,15 @@ Each item: the proposal, then the reason in one line. holds — `a?.f(x)`, `a?[i]`, `a?.b.c`. A result that is already an Option is not wrapped again, so `a?.b?.c` is one Option. A rest with no value makes the whole a statement. `~o1` is a fresh name no reader produces. + - A `T` where a `T?` is wanted is `Some` of it (decision 138): an + assignment, a `let` with a type, an argument, a return, a struct field, an + array or `Vec` element, and an `if` or `match` arm beside an Option arm. + A literal is built at `T` first, so `s = -1` over an `i64?` is `Some(-1)` + at i64. One level at a time: a `T` into a `T??` is `Some(Some(t))`, a `T?` + into a `T??` is `Some` of it. A `$t` meeting `$u?` binds `$u` to `T`. + Never inside a container (`Vec(i32)` is not a `Vec(i32?)`), and never the + other way: a `T?` where a `T` is wanted still needs `!`, `??`, `x?` or + `as`. A kept chain whose arms are a `T` and a `T?` is a `T??`. **Built.** - **Casts and type-taking builtins are calls:** `i32(x)`, `vec-new(u8)`, `max-value(u8)`, `the([3 f32], [1 2 3.5])`. A pointer cast is the type called: `Ptr(Color)(p)` reads `((Ptr Color) p)`. **Built.** diff --git a/test/programs/autowrap.fln b/test/programs/autowrap.fln new file mode 100644 index 00000000..636f94ec --- /dev/null +++ b/test/programs/autowrap.fln @@ -0,0 +1,85 @@ +;; A T where a T? is wanted is Some of it (decision 138): each position, +;; a literal built at the payload's type, nested Options, generics, and the +;; arms of an if, a match and a kept chain. + +struct P + a: i64? + b: i32? + +fn show(o: i32?) -> i32 = o ?? -9 + +fn back(b: bool, x: i32) -> i32? + if b + return x + None + +fn pick(b: bool, x: i32) -> i32? + if b then x else None + +fn pick2(b: bool, x: i32) -> i32? + if b then None else x + +fn two(o: Option(i32?)) -> str + match o + Some(i) -> if i? then "some some" else "some none" + None -> "none" + +fn first(o: $u?) -> $u = o! + +fn wrap(x: $t) -> $t? = x + +fn chain(a: bool, b: bool, opt: i32?) -> Option(i32?) + if a + 1 + elif b + opt + +fn main() + ;; assignment, and a literal at the payload's width + let s: i64? = None + s = -1 + println(s ?? 0) + ;; a let with an annotation, from a literal, a name and arithmetic + let x: i32 = 4 + let a: i32? = x + 1 + let w: i64? = x + let f: f64? = 2 + println(a ?? 0, w ?? 0, f ?? 0.0) + ;; an argument and a return value + println(show(7), back(true, 8) ?? -1, back(false, 8) ?? -1) + ;; a struct field + let p = P{.a 3 .b x} + println(p.a ?? 0, p.b ?? 0) + ;; an array and a Vec element + let xs: [3 i32?] = [1, None, x] + println(xs[0] ?? 0, xs[1] ?? 0, xs[2] ?? 0) + let v: Vec(i32?) = vec-new(i32?) + push(v, 6) + push(v, None) + println(length(v), v[0] ?? 0, v[1] ?? 0) + ;; the arms of an if and a match + println(pick(true, 2) ?? -1, pick(false, 2) ?? -1, pick2(true, 2) ?? -1, pick2(false, 2) ?? -1) + let m = match x + 4 -> x + _ -> None + println(m ?? 0) + ;; nested: a T into a T?? is Some(Some(t)), a T? is Some of it + let nn: Option(i32?) = 5 + let none: i32? = None + let nn2: Option(i32?) = none + println(two(nn), two(nn2)) + ;; a kept chain: a T arm beside a T? arm makes the chain a T?? + println(two(chain(true, false, none)), two(chain(false, true, none)), two(chain(false, false, none))) + let kk = + if x > 9 + none + elif x > 1 + 1 + println(two(kk)) + ;; generics + println(first(9), first(Some(8)), wrap(3) ?? 0) + ;; a narrowed name still takes a payload value + let o: i32? = Some(1) + if o? + o = 10 + println(o) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 1233fb1c..7fb8e826 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2274,6 +2274,10 @@ let () = outputs ~opt:"-O0" (path ^ ", -O0") ("programs/" ^ path) want; outputs ~x86:true (path ^ ", --x86") ("programs/" ^ path) want) [ ("optionals.fln", optionals_out); ("optionals-dyn.fln", optionals_dyn_out); + (* A T where a T? is wanted is Some of it (decision 138). *) + ("autowrap.fln", + "-1\n5 4 2\n7 8 -1\n3 4\n1 0 4\n2 6 0\n2 -1 -1 2\n4\nsome some some none\n\ + some some some none none\nsome some\n9 8 3\n10\n"); (* x? tests and narrows, e? as g names what it found (decision 133). *) ("presence.fln", "true false true\n6\n-1\n3\n101 209 0\n11\n42\n2\nabsent\n6\nfalse true\n3\n6\n15\n"); ("presence-dyn.fln", "true false\n103 209 0\nno pet\nann\n3 2\n") ]; (* x! over nothing traps at its site and names the expression. *) diff --git a/test/test_flan.ml b/test/test_flan.ml index 59b8221e..b7298b55 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -8402,6 +8402,23 @@ let () = parse_rejects "_ in a defgeneric's return slot" ~needle:"defgeneric's methods each have their own" "(defgeneric area [s] _)"; + (* Decision 138: a T is wrapped where a T? is wanted, and never the other + way. The program half is programs/autowrap.fln. *) + accepts "a T is Some of it at a T?" "(defn f [] (Option i64) -1)\n(defn main [] ())"; + rejects_check "a T? is not unwrapped at a T" ~needle:"expected i32, found (Option i32)" + "(defn f [o (Option i32)] i32 o)\n(defn main [] ())"; + rejects_check "a T? argument is not unwrapped" ~needle:"expected i32, found (Option i32)" + "(defn g [x i32] i32 x)\n(defn f [o (Option i32)] i32 (g o))\n(defn main [] ())"; + rejects_check "a payload that does not fit is refused at the Option" + ~needle:"expected (Option i32), found str" + "(defn f [] (Option i32) \"no\")\n(defn main [] ())"; + rejects_check "a narrowing is not wrapped" ~needle:"expected (Option i32), found i64" + "(defn f [x i64] (Option i32) x)\n(defn main [] ())"; + rejects_check "no wrap inside a container" ~needle:"expected (Vec (Option i32)), found (Vec i32)" + "(defn f [v (Vec i32)] (Vec (Option i32)) v)\n(defn main [] ())"; + rejects_check "a narrowed name still refuses an Option" + ~needle:"it cannot be given an Option here" + "(defn main [] () (let [o (the (Option i32) (Some 1))] (when (? o) (set o (Some 2)))))"; (* A plain name binds what an Option or a dyn holds; over anything else it cannot fail, and is refused toward let. The program half is programs/if-let.flan and programs/optionals.fln. *)