A T is wrapped in Some wherever a T? is wanted, one level at a time and never unwrapped back.

This commit is contained in:
Joseph Ferano 2026-09-26 17:11:12 +07:00
parent 1763157ef2
commit cb60df1dc0
6 changed files with 262 additions and 8 deletions

View File

@ -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.

View File

@ -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]",

View File

@ -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.**

View File

@ -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)

View File

@ -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. *)

View File

@ -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. *)