From b814aca187619169d6463bb806f8d319e5d03788 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 26 Sep 2026 18:02:43 +0700 Subject: [PATCH] A kept when or else-less chain over an Option arm is that Option, flattened one level, unless an Option of it is wanted. --- TODO.org | 10 +++- lib/check.ml | 109 ++++++++++++++++++++++------------ spec-syntax.md | 11 +++- test/programs/autowrap.fln | 17 +++++- test/programs/if-let-kept.fln | 31 ++++++++++ test/programs/when-value.flan | 16 ++++- test/test_acceptance.ml | 9 +-- 7 files changed, 153 insertions(+), 50 deletions(-) diff --git a/TODO.org b/TODO.org index c59863e3..e8c909bd 100644 --- a/TODO.org +++ b/TODO.org @@ -49,6 +49,12 @@ found. Rules out =if let g = x= over a plain name, which is refused toward these 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. +** DONE A kept when over an Option body flattens one level +CLOSED: [2026-09-26] +Decided (140), reversing 125a: a kept =when=, else-less =if=/=elif= or =if let= chain whose +arm is already a =T?= is a =T?=, and =T= beside =T?= arms is =T?=; a =T??= arm stays =T??=. +Where =T??= is wanted the arm is Some of it. Rules out telling "no branch matched" apart +from "a branch gave None" without asking for =T??=. ** 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. @@ -68,8 +74,8 @@ Option or a dyn holds (130); over any other type, and =_=, it is refused toward ** DONE when as a value, and get as a checked lookup CLOSED: [2026-09-26] Every one-armed =if= (and a =cond= with no =:else=) is a =when=; kept — a =let= value, a -call's argument, a lambda's return — it is =Option(T)=, nested over an Option body -(Rust's =bool::then=), and body-or-nil where a dyn is wanted. A =_=-inferred return's +call's argument, a lambda's return — it is =Option(T)=, and body-or-nil where a dyn is +wanted. Over an Option body it was nested (Rust's =bool::then=, 125a); 140 reversed that. A =_=-inferred return's last form is not kept. =get= over dyn text or vec is nil when out of range; =.field= still traps. ** NEXT str and String diff --git a/lib/check.ml b/lib/check.ml index d1816a4a..da0617d2 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -8618,12 +8618,31 @@ and check_if_once ctx ~tail ~used ?want loc c t e = | Some Types.Dyn -> Some Types.Dyn | _ -> None in - let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:tw t)) in + let arm w = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:w t)) in + (* At an Option want the arm is asked the payload first, and the chain + wraps it; refused there, it is asked the Option itself, which it + then is (decision 140). So a T?? wanted of a T? arm is Some of it, + and the chain's own None stays the outer one. *) + let t, at_payload = + match want with + | Some (Types.Option _) -> + (match trial ctx (fun () -> arm tw) with + | Ok t -> t, true + | Error d -> + (match trial ctx (fun () -> arm want) with + | Ok t -> t, false + | Error _ -> raise (Loc.Error d))) + | _ -> arm tw, false + in let rest ~used ?want () = branch ctx (fun () -> ctx.tail <- tail; ctx.used <- used; check ctx ?want e) in match t.Tast.ty with + | ty when at_payload && not (Types.equal ty Types.Never) -> + let oty = Option.get want in + let e = rest ~used:true ~want:oty () in + mk loc oty (Tast.If (c, mk loc oty (Tast.Some_ t), e)) | Types.Unit -> expect ctx loc ~want (mk loc Types.Unit (Tast.If (c, t, rest ~used:false ()))) @@ -8633,32 +8652,14 @@ and check_if_once ctx ~tail ~used ?want loc c t e = | Types.Dyn -> let e = rest ~used:true ~want:Types.Dyn () in expect ctx loc ~want (mk loc Types.Dyn (Tast.If (c, t, e))) + (* An arm that is already an Option is the chain's value as it is, + and [None] when no test holds — one level flattened (decision + 140): an arm's None and no arm running are one answer. *) + | Types.Option _ as o -> + let e = rest ~used:true ~want:o () in + expect ctx loc ~want (mk loc o (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)))) @@ -8899,9 +8900,9 @@ and check_if_once ctx ~tail ~used ?want loc c t e = branch evaluates to. Kept — a [let]'s value, an argument, a return, or anything else with a type wanted of it — it answers (Option T): [Some] of the branch when the test held and [None] when it did not. A branch that - is already an Option is not flattened: the answer is (Option (Option T)), - Rust's [bool::then], so [None] from the branch and a failed test stay two - answers. + is already an Option is that Option, flattened one level (decision 140, + Kotlin's [?.] rather than Rust's [bool::then]): [None] from the branch and + a failed test are one answer. A (Option (Option T)) branch stays one. Dyn has no Option. Where a dyn is wanted, or the branch is a dyn, a false test answers nil and a true one the branch's value — one absence, as a @@ -8921,17 +8922,26 @@ and check_when ctx ~used ?want loc c | Some Types.Dyn -> let t = branch_at ~want:Types.Dyn () in mk loc Types.Dyn (Tast.If (c, t, nil ())) - | Some (Types.Option inner) -> - let t = branch_at ~want:inner () in - let oty = Types.Option inner in - let some = if t.Tast.ty = Types.Never then t else mk loc oty (Tast.Some_ t) in - mk loc oty (Tast.If (c, some, mk loc oty Tast.None_)) + | Some (Types.Option inner as oty) -> + (* The payload first, wrapped; refused there, the Option itself, which + the branch then is (decision 140). *) + let t = + match trial ctx (fun () -> branch_at ~want:inner ()) with + | Ok t -> if t.Tast.ty = Types.Never then t else mk loc oty (Tast.Some_ t) + | Error d -> + (match trial ctx (fun () -> branch_at ~want:oty ()) with + | Ok t -> if t.Tast.ty = Types.Never then t else expect ctx loc ~want:(Some oty) t + | Error _ -> raise (Loc.Error d)) + in + mk loc oty (Tast.If (c, t, mk loc oty Tast.None_)) | None when not used -> stmt (branch_at ()) | _ -> let t = branch_at () in if valueless t then stmt t else if Types.equal t.Tast.ty Types.Dyn then expect ctx loc ~want (mk loc Types.Dyn (Tast.If (c, t, nil ()))) + else if (match t.Tast.ty with Types.Option _ -> true | _ -> false) then + expect ctx loc ~want (mk loc t.Tast.ty (Tast.If (c, t, mk loc t.Tast.ty Tast.None_))) else let oty = Types.Option t.Tast.ty in expect ctx loc ~want @@ -10021,7 +10031,7 @@ and check_array_gen ctx ~want loc dims f = ~element:(fun idxs -> mk loc elem (Tast.CallPtr (fv, idxs)))) and check_match ctx ?(tail = false) ?(used = false) ?(stmt = false) ?(opt = false) - ?opt_rest ?want loc scrutinee arms = + ?(flat = false) ?opt_rest ?want loc scrutinee arms = (* [stmt] is an [if let] with no else: a statement, Unit whatever its arm answers, as a one-armed [if] is when nothing keeps it. [opt] is one that is kept: its arm answers [Some], and the arm with no body [None]. *) @@ -10030,7 +10040,11 @@ and check_match ctx ?(tail = false) ?(used = false) ?(stmt = false) ?(opt = fals let want0 = want in let want = if stmt then None - else if opt then (match want with Some (Types.Option i) -> Some i | _ -> None) + else if opt then + (match want with + | Some (Types.Option _) when flat -> want + | Some (Types.Option i) -> Some i + | _ -> None) else want in let s = check ctx scrutinee in @@ -10629,6 +10643,14 @@ and check_match ctx ?(tail = false) ?(used = false) ?(stmt = false) ?(opt = fals match r with | Some e -> e | None -> rt a.Ast.aloc Types.Dyn "flan_dyn_nil" []) + (* Already an Option: the whole, one level flattened (decision 140). *) + | Some (Types.Option _ as o) when flat || want0 = None -> + opt_result := Some o; + let r = rest ~used:true ~want:o () in + fill Fun.id (fun a -> + match r with + | Some e -> e + | None -> mk a.Ast.aloc o Tast.None_) | Some t -> let oty = Types.Option t in opt_result := Some oty; @@ -10791,9 +10813,22 @@ and check_if_let ctx ~tail ~used ?want loc scrutinee (arm : Ast.arm) els = ctx.tail <- tail; ctx.used <- used; check ctx ?want e)) els in + (* The arm at the payload first, as [check_when] asks it; refused + there, at the Option itself (decision 140). *) + let go ~flat () = + check_match ctx ~tail ~used:true ~opt:true ~flat ?opt_rest ?want loc scrutinee + [ arm; wild [] ] + in expect ctx loc ~want - (check_match ctx ~tail ~used:true ~opt:true ?opt_rest ?want loc scrutinee - [ arm; wild [] ])) + (match want with + | Some (Types.Option _) -> + (match trial ctx (go ~flat:false) with + | Ok r -> r + | Error d -> + (match trial ctx (go ~flat:true) with + | Ok r -> r + | Error _ -> raise (Loc.Error d))) + | _ -> go ~flat:false ())) | Some e -> check_match ctx ~tail ~used ?want loc scrutinee [ arm; wild [ e ] ] | None -> diff --git a/spec-syntax.md b/spec-syntax.md index 9f922c86..7c85a91f 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -252,7 +252,8 @@ Each item: the proposal, then the reason in one line. 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.** + `as`. A kept chain whose arms are a `T` and a `T?` is a `T?` (decision + 140, under `when c`). **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.** @@ -316,7 +317,13 @@ Each item: the proposal, then the reason in one line. A `when` whose value is kept (a `let`'s value, an argument, a return) gives `Some(a)` when `c` holds and `None` when it does not; where a `dyn` is wanted, `a` or `nil`. As a statement it gives nothing. An `if`/`elif` chain - with no `else` is the same when kept: `None` when no test holds. **Built.** + with no `else` is the same when kept: `None` when no test holds. When `a` + is already an Option it is not wrapped again (decision 140): `when c then + o` over an `i32?` is an `i32?`, `None` when `c` fails or `o` is `None`, and + a chain mixing `T` and `T?` arms is a `T?`. One level only: an arm that is + a `T??` gives a `T??`. Where an Option of the arm's type is wanted, as a + `T??` over a `T?` arm, the arm is `Some` of its value and a failed test is + the outer `None`. `if let` with no `else` follows the same rule. **Built.** - **`if let P = v`** plus a block reads as `(if-let [P v] then)`; `elif` and `else` follow as for `if`, the rest of the chain being the `if-let`'s else. `elif let P = v` is a further `if-let` nested in that else. diff --git a/test/programs/autowrap.fln b/test/programs/autowrap.fln index 0db6c6ea..ef62defb 100644 --- a/test/programs/autowrap.fln +++ b/test/programs/autowrap.fln @@ -40,12 +40,24 @@ fn arms(k: i32, x: i32) -> i32? fn big(x: i64?) -> i64 = x ?? 0 +;; A T?? is wanted, so each arm is Some of its value and the chain's None is +;; the outer one. fn chain(a: bool, b: bool, opt: i32?) -> Option(i32?) if a 1 elif b opt +;; Nothing wanted: a T arm beside a T? arm makes the chain a T?, flattened +;; one level (decision 140). +fn flat(a: bool, b: bool, opt: i32?) -> i32? + let r = + if a + 1 + elif b + opt + r + fn main() ;; assignment, and a literal at the payload's width let s: i64? = None @@ -85,14 +97,15 @@ fn main() 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?? + ;; kept chains over T and T? arms println(two(chain(true, false, none)), two(chain(false, true, none)), two(chain(false, false, none))) + println(flat(true, false, none) ?? -1, flat(false, true, Some(6)) ?? -1, flat(false, true, none) ?? -1, flat(false, false, none) ?? -1) let kk = if x > 9 none elif x > 1 1 - println(two(kk)) + println(kk ?? -1) ;; generics println(first(9), first(Some(8)), wrap(3) ?? 0, through(5)) ;; a literal local takes the payload's type from an Option use, as it diff --git a/test/programs/if-let-kept.fln b/test/programs/if-let-kept.fln index 0fb4714c..bf27e7c1 100644 --- a/test/programs/if-let-kept.fln +++ b/test/programs/if-let-kept.fln @@ -21,6 +21,28 @@ fn early(a: Option(i32)) -> Option(i32) elif true 3 +;; An arm that is already an Option is the whole, flattened (decision 140). +fn flat(a: Option(i32), o: Option(i32)) -> Option(i32) + if let Some(x) = a then o + +;; An Option of it wanted: the arm is Some of its value, and no match is the +;; outer None. +fn nest(a: Option(i32), o: Option(i32)) -> Option(Option(i32)) + if let Some(x) = a then o + +fn level(oo: Option(Option(i32))) + match oo + Some(o) -> if o? then println(o) else println("some none") + None -> println("none") + +fn flat_chain(a: Option(i32), k: i32) + let r = + if let Some(x) = a + x + elif k > 0 + None + show(r) + fn dyn_only(a: Option(i32)) -> dyn if let Some(x) = a then x @@ -40,6 +62,15 @@ fn main() show(lead(1, None)) show(early(None)) show(early(Some(1))) + show(flat(Some(1), Some(4))) + show(flat(Some(1), None)) + show(flat(None, Some(4))) + level(nest(Some(1), Some(4))) + level(nest(Some(1), None)) + level(nest(None, Some(4))) + flat_chain(Some(3), 0) + flat_chain(None, 1) + flat_chain(None, 0) println(dyn_only(Some(5))) println(dyn_only(None)) ;; As a statement it is unchanged. diff --git a/test/programs/when-value.flan b/test/programs/when-value.flan index c1c907b4..ce3ecb23 100644 --- a/test/programs/when-value.flan +++ b/test/programs/when-value.flan @@ -1,7 +1,8 @@ ;;;; A when whose value is kept answers an Option: Some of its body when the ;;;; test holds, None when it does not. As a statement it answers nothing. ;;;; Where a dyn is wanted it answers the body or nil, since dyn has no -;;;; Option. A body that is already an Option is not flattened. +;;;; Option. A body that is already an Option is that Option, flattened one +;;;; level (decision 140), unless an Option of it is what is wanted. (defn show [o (Option i32)] () (match o (Some v) (println v) None (println "none"))) @@ -9,10 +10,13 @@ ;; Returned: the return type is the want. (defn half [n i32] (Option i32) (when (= 0 (% n 2)) (/ n 2))) -;; Nested, as Rust's bool::then: None from the body stays apart from a -;; failed test. +;; An (Option (Option i32)) is wanted, so the body is Some of it and the +;; failed test is the outer None. (defn wrap [c bool o (Option i32)] (Option (Option i32)) (when c o)) +;; Flattened: None from the body and a failed test are one answer. +(defn flat [c bool o (Option i32)] (Option i32) (when c o)) + (defn level [oo (Option (Option i32))] () (match oo (Some o) (match o (Some v) (println v) None (println "some none")) @@ -45,6 +49,12 @@ (level (wrap true (Some 1))) (level (wrap true None)) (level (wrap false (Some 1))) + (show (flat true (Some 4))) + (show (flat true None)) + (show (flat false (Some 4))) + (let [o (the (Option i32) (Some 8)) + f (when true o)] + (show f)) (println (dyn-when true)) (println (dyn-when nil)) (show (early None)) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 09dfbad6..e2328216 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2226,8 +2226,8 @@ let () = dyn_if_truthy_out; (* A kept when is an Option; get is a checked lookup; if let. *) let when_value_out = - "5\nnone\n42\nnone\n9\n1\nsome none\nnone\n5\nnil\n3\nnone\n6\nnone\n20\nnone\n2\n\ - a\nnil\ncond stmt\nran\nend\n" + "5\nnone\n42\nnone\n9\n1\nsome none\nnone\n4\nnone\nnone\n8\n5\nnil\n3\nnone\n6\nnone\n\ + 20\nnone\n2\na\nnil\ncond stmt\nran\nend\n" in outputs "when as a value" "programs/when-value.flan" when_value_out; outputs ~opt:"-O0" "when as a value, -O0" "programs/when-value.flan" when_value_out; @@ -2246,7 +2246,8 @@ let () = "5\n100\n0\n9\n1\n20\n100\n0\n1\n7\nabsent\nnorth\n6\n-1\n-2\n14\nwhen block\n" in let if_let_kept_out = - "1\n4\nnone\n6\nnone\n9\n2\nnone\n3\nnone\n5\nnil\n7\n" + "1\n4\nnone\n6\nnone\n9\n2\nnone\n3\nnone\n4\nnone\nnone\n4\nsome none\nnone\n\ + 3\nnone\nnone\n5\nnil\n7\n" in outputs "a kept if let chain" "programs/if-let-kept.fln" if_let_kept_out; outputs ~opt:"-O0" "a kept if let chain, -O0" "programs/if-let-kept.fln" if_let_kept_out; @@ -2277,7 +2278,7 @@ let () = (* A T where a T? is wanted is Some of it (decision 138). *) ("autowrap.fln", "-1\n5 4 2\n7 8 -1\n3 4\n7\n1 0 4\n5\n2 6 0\n2 -1 -1 2\n4\n-1 3 0\n\ - some some some none\nsome some some none none\nsome some\n9 8 3 5\n\ + some some some none\nsome some some none none\n1 6 -1 -1\n1\n9 8 3 5\n\ 4000000000 4000000000 4 4\n4 4000000000\n3.5 3.5 7 7\n200 200\n-1 5\n1 0\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") ];