In .fln x? tests that a value is present and narrows a local Option to its payload in the block it guards, and e? as g names what a test found.

This commit is contained in:
Joseph Ferano 2026-09-26 15:48:36 +07:00
parent 6c6b5a60cb
commit d252eee2b2
16 changed files with 371 additions and 63 deletions

View File

@ -19,6 +19,12 @@ prelude and vendor rewritten in .fln; tests and examples converted with =flan co
then =flan convert=, the .flan source path and =flan-mode= removed. Nothing tracks what
.flan can no longer say. Step 1 is done: the name rule with the =is-=/=has-= renames
through the prelude, vendor and raylib, and =T?=, =??=, =x!=, =if let g = x= and =a?.b=.
** DONE x? tests presence, and narrows
CLOSED: [2026-09-26]
Decided (133): =x?= is a bool; =if x?=, =elif x?=, =while x?= and the rest of an =and=
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.
** 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

@ -1881,6 +1881,8 @@ lambda or a `Fn(...)' type, and not after a match arm's."
;; The words inside a line: `for i in range(n)', `if c then a else b', a
;; `where' constraint.
("[ \t]\\(then\\|else\\|in\\|where\\)[ \t]" 1 font-lock-keyword-face)
;; A test's `as', `if e? as g'.
("?[ \t]+\\(as\\)[ \t]" 1 font-lock-keyword-face)
;; `if let Some(g) = x', and a value's `if' or `when', `x = when c then a'.
("\\_<\\(?:el\\)?if[ \t]+\\(let\\)[ \t]" 1 font-lock-keyword-face)
("[ \t=(,]\\(if\\|when\\)[ \t]" 1 font-lock-keyword-face)

View File

@ -923,12 +923,13 @@ defconst(k, 3)
(test-flan-fln--is "and so is a statement's" (funcall face "when b") 'font-lock-keyword-face)))
;; Swift optionals: the marks are not part of a name, `??' continues a line,
;; and `elif let' reads as `if let' does.
(test-flan-fln--in "fn f(o: i32?) -> i32\n if let g = o\n g!\n elif let h = p?.q\n h ?? 0\n"
(test-flan-fln--in "fn f(o: i32?) -> i32\n if o? as g\n g!\n elif let Some(h) = p?.q\n h ?? 0\n"
(font-lock-ensure)
(let ((face (lambda (needle)
(save-excursion (goto-char (point-min)) (search-forward needle)
(get-text-property (match-beginning 0) 'face)))))
(test-flan-fln--is "elif let's let is a keyword" (funcall face "let h") 'font-lock-keyword-face)
(test-flan-fln--is "elif let's let is a keyword" (funcall face "let Some(h)") 'font-lock-keyword-face)
(test-flan-fln--is "a test's as is a keyword" (funcall face "as g") 'font-lock-keyword-face)
(test-flan-fln--is "a type's ? is the type's" (funcall face "?)") 'font-lock-type-face)
(test-flan-fln--is "an unwrap's ! is marked" (funcall face "!\n") 'font-lock-keyword-face)
(test-flan-fln--is "a chain's ? is marked" (funcall face "?.q") 'font-lock-keyword-face)
@ -937,8 +938,8 @@ defconst(k, 3)
(search-forward "g!")
(backward-char 1)
(test-flan-fln--is "the name at x! is x" (thing-at-point 'symbol t) "g"))
(test-flan-fln--is "after if let over a plain name, one level deeper"
(test-flan-fln--tabs "fn f() -> ()\n if let g = o\n|" 1) 4)
(test-flan-fln--is "after if x? as g, one level deeper"
(test-flan-fln--tabs "fn f() -> ()\n if o? as g\n|" 1) 4)
(with-temp-buffer
(insert "x = a ??\n b\ny = a\n ?? b\n")
(flan-fln-mode)

View File

@ -87,6 +87,10 @@ and expr_kind =
bound to it for [body]; the result is an Option (nil or a value over a
dyn), flat when [body] is already one. [n] is the reader's fresh name. *)
| Chain of string * expr * expr
(* Made by the checker, never read: [body] with each name, a local
(Option T) tested by [x?] in the condition above it, read as its
payload ([if x?] narrowing, decision 133). *)
| Narrow of string list * expr
| Struct of string * (string * expr) list (* (Cursor {.src s}) *)
(* {.src s .pos 0} with no type written in front of it. The fields alone do
not name a type, so this node carries no name and is only checkable where
@ -475,6 +479,7 @@ let map_children f (e : expr) : expr =
| Match (s, arms) -> Match (ex s, List.map arm arms)
| IfLet (s, a, e) -> IfLet (ex s, arm a, Option.map ex e)
| Chain (n, v, b) -> Chain (n, ex v, ex b)
| Narrow (ns, b) -> Narrow (ns, ex b)
| Struct (n, fs) -> Struct (n, List.map (fun (n, v) -> (n, ex v)) fs)
| Bare fs -> Bare (List.map (fun (n, v) -> (n, ex v)) fs)
| MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs)

View File

@ -3152,6 +3152,14 @@ let place_of_expr (e : Ast.expr) : Ast.place option =
let mk loc ty e : Tast.expr = { Tast.e; ty; loc }
(* A local narrowed by [if x?]: the same slot, read as its payload. *)
let narrowed_tag = "~narrowed"
let local_of loc (b : binding) =
if b.bwhat = Some narrowed_tag then
mk loc b.bty (Tast.Field (mk loc (Types.Option b.bty) (Tast.Local b.slot), 1))
else mk loc b.bty (Tast.Local b.slot)
let unit_at loc = mk loc Types.Unit Tast.Unit
(* The compiler temp an [and] leaves in its else arm; see [check_if]. *)
@ -3263,8 +3271,7 @@ let close_over ~fname (octx : ctx) (fctx : ctx) loc =
mk loc ety
(Tast.Make (ename,
List.map
(fun (_, ((b : binding), _)) ->
mk loc b.bty (Tast.Local b.slot))
(fun (_, ((b : binding), _)) -> local_of loc b)
caught))
in
let mslot = fresh_slot octx ety in
@ -6284,9 +6291,11 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr =
top of every trip — but it stays outside [in_loop], because a [break]
in a condition still means the enclosing loop and a [defer] there is
still the outer block's. *)
let names = narrows c in
let c = check_truthy ctx c in
let body = in_loop ctx ?label (fun () ->
scoped ctx (fun () -> map_lr (fun b -> check ctx b) body))
scoped ctx (fun () ->
with_narrowed ctx names (fun () -> map_lr (fun b -> check ctx b) body)))
in
(* No latch: a [while] has nothing to run between the body and the test, so
a [continue] can branch straight at the condition. *)
@ -6412,6 +6421,28 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr =
let v = check ctx ~want:Types.Dyn v in
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_slot_set" [ target; k; v; here loc ])
(* A name narrowed by [if x?] takes a value of its payload's type, which
keeps it present. An Option would end the narrowing partway through the
block, so it is refused (decision 133): the block reads x as present
throughout. *)
| Ast.Set (Ast.Pvar n, v)
when (match lookup ctx n with Some b -> b.bwhat = Some narrowed_tag | None -> false) ->
let b = Option.get (lookup ctx n) in
(match trial ctx (fun () -> check ctx ~want:b.bty v) with
| Ok vv ->
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Set (Tast.Pfield (mk loc (Types.Option b.bty) (Tast.Local b.slot), 1), vv)))
| Error d ->
(match trial ctx (fun () -> check ctx ~want:(Types.Option b.bty) v) with
| Ok { Tast.ty = Types.Option _; _ } ->
fail loc
"%s is tested with %s? above, so in this block it is %s, and it \
cannot be given an Option here: the block reads it as present \
throughout. Assign a %s, or test a new name, as in while %s? as \
item, and assign %s from that"
n n (tyname loc b.bty) (tyname loc b.bty) n n
| _ -> raise (Loc.Error d)))
| Ast.Set ((Ast.Pvar n as p), v) when lit_recorded ctx n <> None ->
let key = Option.get (lit_recorded ctx n) in
let p, pty = check_place ctx loc p in
@ -6484,6 +6515,9 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr =
| Ast.IfLet (scrutinee, arm, els) ->
check_if_let ctx ~tail ~used ?want loc scrutinee arm els
| Ast.Chain (n, v, body) -> check_chain ctx ~want loc n v body
| Ast.Narrow (names, body) ->
with_narrowed ctx names (fun () ->
ctx.tail <- tail; ctx.used <- used; check ctx ?want body)
(* Constant integer arithmetic where a type variable is wanted is folded to
the literal it computes first, so [(+ x (+ 1 2))] is admitted wherever
[(+ x 3)] is. The instantiation re-checks the form unfolded, at a concrete
@ -6876,7 +6910,7 @@ and var ctx ?(qualified = false) loc ~want name =
s.dirty <- true;
mk loc t (Tast.Local b.slot))
| Some b ->
expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot))
expect ctx loc ~want (local_of loc b)
(* A local of the enclosing function, in a body that was lifted out of it:
captured by value, here, where it is first named. Asked *before* the
globals, because that is what the name means at the place it is
@ -8437,6 +8471,11 @@ and check_if ctx ?(tail = false) ?(used = false) ?want loc c t e =
raise ex)
and check_if_once ctx ~tail ~used ?want loc c t e =
let t =
match narrows c with
| [] -> t
| names -> { t with Ast.e = Ast.Narrow (names, t) }
in
let c = check_truthy ctx c in
(* Both arms are the tail, and a one-armed [if] counts: [(when c (recur ...))]
is how nearly every loop is written, and the branch is still the last
@ -10572,6 +10611,58 @@ and if_let_name ctx ~tail ~used ?want loc scrutinee n (arm : Ast.arm) els =
(if fln then Printf.sprintf "let %s = ..." n
else Printf.sprintf "(let [%s ...] ...)" n)
(* The locals a condition tests with [x?], through [and]: those it narrows
in the block it guards (decision 133). Not through [or] or [not], where
the test holding says nothing about [x]. *)
and narrows (c : Ast.expr) =
match c.Ast.e with
| Ast.Call ({ Ast.e = Ast.Var "?"; _ }, [ { Ast.e = Ast.Var x; _ } ]) -> [ x ]
| Ast.If (p, q, Some { Ast.e = Ast.Var "false"; _ }) -> narrows p @ narrows q
| _ -> []
(* [f] with each of [names] that is a local (Option T) read as its payload:
the same slot, so a field set through it lands in the Option itself. A
dyn stays as it is; a name that is not a local is not narrowed. Assigning
the name a T writes the payload and it stays present; assigning it an
Option is refused ([narrowed_set]). *)
and with_narrowed : 'a. ctx -> string list -> (unit -> 'a) -> 'a = fun ctx names f ->
if names = [] then f ()
else
scoped ctx (fun () ->
List.iter
(fun n ->
match lookup ctx n with
| Some ({ bty = Types.Option t; _ } as b) when b.bwhat <> Some narrowed_tag ->
ctx.scope <- (n, { b with bty = t; bwhat = Some narrowed_tag; blit = None })
:: ctx.scope
| _ -> ())
names;
f ())
(* [x?]: whether x holds a value — an Option that is Some, a dyn that is not
nil. *)
and check_present ctx ~want loc (args : Ast.expr list) =
match args with
| [ x ] ->
let xv = check ctx x in
let s = fresh_slot ctx xv.Tast.ty in
let sv = mk loc xv.Tast.ty (Tast.Local s) in
let held e = mk loc Types.Bool (Tast.Let ([ (s, xv) ], [ e ])) in
(match xv.Tast.ty with
| Types.Option _ -> expect ctx loc ~want (held (opt_is_some loc sv))
| Types.Dyn -> expect ctx loc ~want (held (dyn_not_nil loc sv))
| t ->
let hint =
match x.Ast.e with
| Ast.Var n ->
Printf.sprintf ". A name cannot end in ? either: a yes-or-no name \
starts with is- or has-, as in is-%s" n
| _ -> ""
in
fail loc "%s is %s, which always holds a value, so %s? has nothing to \
test%s" (source_text x) (tyname loc t) (source_text x) hint)
| _ -> fail loc "? tests one value: x?"
(* The tag test and the payload of an Option held in a local, and the nil
test of a dyn: the shapes [box_option] and [unbox_option] build. *)
and opt_is_some loc (sv : Tast.expr) =
@ -11026,6 +11117,9 @@ and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t =
"%s is a parameter, and a parameter is not assignable — bind a \
local with let" name
end;
if b.bwhat = Some narrowed_tag then
(Tast.Pfield (mk loc (Types.Option b.bty) (Tast.Local b.slot), 1), b.bty)
else
Tast.Plocal b.slot, b.bty
| None ->
(* Not in scope here at all: a name of the enclosing function, which a
@ -12898,6 +12992,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
ordinary_call ctx ~want loc name args
(* .fln's [x ?? d] and [x!]; no .fln name can take them over. *)
| "??" -> check_coalesce ctx ~want loc args
| "?" -> check_present ctx ~want loc args
| "!!" -> check_unwrap ctx ~want loc args
(* ── arithmetic and comparison ─────────────────────────────────── *)
(* (- x) negates, Clojure's rule. A literal operand is the negative literal,
@ -15464,7 +15559,7 @@ and ordinary_call ctx ~want loc name args =
(* The binding the guard already found, read directly. Going back through
[check] would repeat the lookup. *)
(match lookup ctx name with
| Some b -> call_value ctx ~want loc (mk loc b.bty (Tast.Local b.slot)) args
| Some b -> call_value ctx ~want loc (local_of loc b) args
| None ->
match capture ctx loc name with
| Some b -> call_value ctx ~want loc (mk loc b.bty (Tast.Local b.slot)) args
@ -16908,6 +17003,9 @@ let builtins : (string * string * string) list =
"x ?? d in .fln: what x holds, or d when x is None (nil over a dyn). d is \
evaluated only then. a ?? b ?? c reads from the right, and a default \
that is itself an Option keeps the whole an Option.");
("?", "? [(Option T)|dyn] bool",
"x? in .fln: whether x holds a value — Some, or a dyn that is not nil. \
In if x?, elif x? and while x?, a local x is its payload in the block.");
("!!", "!! [(Option T)|dyn] T",
"x! in .fln: what x holds. When x is None (nil over a dyn) the program \
stops there, naming x.");

View File

@ -3006,6 +3006,9 @@ and place f (p : Tast.place) : string * Types.t =
match p with
| Tast.Plocal i -> f.slots.(i), f.slot_tys.(i)
| Tast.Pglobal n -> global_addr f n, Hashtbl.find f.md.globals n
(* A local narrowed by [if x?] is written through its Option's payload. *)
| Tast.Pfield (({ Tast.ty = Types.Option t; _ } as target), i) ->
field_addr f target i, (if i = 0 then Types.Int Types.I8 else t)
| Tast.Pfield (target, i) ->
let sn = match target.Tast.ty with
| Types.Named n -> n | t -> internal "field of %s" (Types.to_string t)

View File

@ -1235,9 +1235,9 @@ and postfix p =
[ sym t.loc "?.";
Form.make (Form.Vec [ sym t.loc h; f ]) f.loc;
rest ]), 12)
(* [T?]. Where a type is read, after any name; where a value is, after
what can only be a type — [vec-new(i32?)] — and refused after a
name or a value. *)
(* [T?] where a type is read, and after what can only be a type
where a value is, [vec-new(i32?)]. On a value, [x?] tests that it
holds one (decision 133): [(? x)]. *)
| QUEST ->
let typish =
match f.v with
@ -1247,24 +1247,10 @@ and postfix p =
h <> "" && h.[0] >= 'A' && h.[0] <= 'Z'
| _ -> false
in
if not (!in_type || typish) then begin
match f.v with
| Form.List [ { v = Form.Sym h; loc = hl }; _ ] when String.length h > 1 && h.[0] = '.' ->
name_refused { hl with Loc.eline = t.loc.Loc.eline; ecol = t.loc.Loc.ecol }
(String.sub h 1 (String.length h - 1) ^ "?")
| Form.Sym n ->
name_refused ~typed:true
{ f.loc with Loc.eline = t.loc.Loc.eline; ecol = t.loc.Loc.ecol }
(n ^ "?")
| _ ->
failk "value-question" t.loc
"? after %s is not read: ? goes after a type, as in i32?. On a \
value that may hold nothing, %s?.field reads through it, %s! \
unwraps it and %s ?? d gives a default"
(text_of f) (text_of f) (text_of f) (text_of f)
end;
ignore (advance p);
loop (mk p l0 (Form.List [ sym l0 "Option"; f ]), 12)
if !in_type || typish then
loop (mk p l0 (Form.List [ sym l0 "Option"; f ]), 12)
else loop (mk p l0 (Form.List [ sym t.loc "?"; f ]), 12)
| BANG ->
ignore (advance p);
loop (mk p l0 (Form.List [ sym t.loc "!!"; f ]), 12)
@ -1385,6 +1371,11 @@ and if_expr p =
let word = match t.tok with NAME w -> w | _ -> "if" in
let letp = if word = "if" then if_let_head p else None in
let c = match letp with Some m -> m | None -> fst (binary p 1) in
let letp, c =
match letp with
| Some _ -> (letp, c)
| None -> (match as_head p c with Some m when word = "if" -> (Some m, m) | _ -> (None, c))
in
(match (peek p).tok with
| NAME "then" -> ignore (advance p)
| _ ->
@ -1418,9 +1409,43 @@ and if_let_head p =
let pat, _ = unary p in
expect_name p "=" ~what:"= and the value the pattern is matched against";
let v, _ = binary p 1 in
(match pat.v with
| Form.Sym g
when g <> "" && g.[0] >= 'a' && g.[0] <= 'z' && not (String.contains g '.')
&& g <> "true" && g <> "false" ->
failk "if-let-name" pat.loc
"if let %s = %s has no pattern to test. To test that %s holds a \
value, write if %s?, and in the block it is what it holds; to name \
what it holds, write if %s? as %s"
g (text_of v) (text_of v) (text_of v) (text_of v) g
| _ -> ());
Some (mk p lt.loc (Form.Vec [ pat; v ]))
| _ -> None
(* [e? as g]: after a test [e?], the name what [e] holds is bound to, as
the head [[g e]] an [if let] over a plain name stands as (decision 133). *)
and as_head p (c : Form.t) =
match (peek p).tok with
| NAME "as" ->
let at = advance p in
(match c.v with
| Form.List [ { v = Form.Sym "?"; _ }; e ] ->
let g =
match (peek p).tok with
| NAME g when g <> "" && g.[0] <> '.' ->
let gt = advance p in
check_name gt g;
sym gt.loc g
| tk ->
failk "as-name" (where_ p) "as takes the name to bind, and found %s" (show tk)
in
Some (Form.make (Form.Vec [ g; e ]) c.loc)
| _ ->
failk "as-test" at.loc
"as names what a test found, and %s is not one. Write %s? as name"
(text_of c) (text_of c))
| _ -> None
(* The if an [if let] head was read into, rewritten to (if-let [P v] then
else): [(if [P v] a b)], [(when [P v] body ...)] and an elif chain's
[(cond [P v] a c2 b2 ...)], whose rest is the else. *)
@ -2661,6 +2686,11 @@ and header (s : st) w : Form.t =
| "if" | "when" ->
let letp = if w = "if" then if_let_head p else None in
let c = match letp with Some m -> m | None -> fst (binary p 1) in
let letp, c =
match letp with
| Some _ -> (letp, c)
| None -> (match as_head p c with Some m when w = "if" -> (Some m, m) | _ -> (None, c))
in
(* The elif and else clauses at the if's column, then the whole form.
[oneline] when the if was [if c then a]: its clauses may then be
one-line too, [elif c then x] and [else y], or take blocks. *)
@ -2677,7 +2707,11 @@ and header (s : st) w : Form.t =
let c =
match if_let_head p with
| Some m -> elif_lets := m :: !elif_lets; m
| None -> fst (binary p 1)
| None ->
let c = fst (binary p 1) in
(match as_head p c with
| Some m -> elif_lets := m :: !elif_lets; m
| None -> c)
in
(match (peek p).tok with
| NAME "then" when oneline ->
@ -2790,9 +2824,20 @@ and header (s : st) w : Form.t =
| _ -> []
in
let c, _ = expr p in
expect_line_end p ~after:(w ^ " " ^ text_of c);
let body = block s ~after:w in
form (label @ (c :: body))
(match (if w = "while" then as_head p c else None) with
(* [while e? as g]: [(while true (if-let [g e] (do body) (break)))]. A
break or continue in the body is this loop's. *)
| Some m ->
expect_line_end p ~after:(w ^ " " ^ text_of c ^ " as ...");
let body = block s ~after:w in
let at = c.Form.loc in
let f items = Form.make (Form.List items) at in
form (label @ [ sym at "true";
f [ sym at "if-let"; m; f (sym at "do" :: body); f [ sym at "break" ] ] ])
| None ->
expect_line_end p ~after:(w ^ " " ^ text_of c);
let body = block s ~after:w in
form (label @ (c :: body)))
| "for" ->
let label =
match (peek p).tok with

View File

@ -294,6 +294,7 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
Option.map go e')
| Ast.Chain (n, v, b) ->
Ast.Chain (n, go v, rename_expr owned alias (n :: bound) b)
| Ast.Narrow (ns, b) -> Ast.Narrow (ns, go b)
(* A quoted symbol naming something the package declares.
[(Form.Sym {.s "Cursor"})] is what a quasiquote desugars to, and it is
the one place a package's name survives into a *string* — which is
@ -857,6 +858,7 @@ let rec expr_uses acc (e : Ast.expr) =
go sc; List.iter (fun (a : Ast.arm) -> gos a.Ast.body) arms
| Ast.IfLet (sc, a, e') -> go sc; gos a.Ast.body; Option.iter go e'
| Ast.Chain (_, v, b) -> go v; go b
| Ast.Narrow (_, b) -> go b
| Ast.Struct (n, kvs) ->
acc := (n, e.Ast.loc) :: !acc;
List.iter (fun (_, v) -> go v) kvs

View File

@ -1314,11 +1314,18 @@ and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
let tvar = { Ast.e = Ast.Var t; loc = ex.Ast.loc } in
let bind = { Ast.bname = t; bty = None; bval = ex; bloc = ex.Ast.loc } in
let rest = go rest in
let body =
if is_and then mk (Ast.If (tvar, rest, Some tvar))
else mk (Ast.If (tvar, tvar, Some rest))
in
mk (Ast.Let ([ bind ], [ body ]))
(match ex.Ast.e with
(* [x? and ...]: the test stays the If's own condition, so what it
narrows reaches the rest of the chain (decision 133). A bool, so
false is the same answer the temp would give. *)
| Ast.Call ({ Ast.e = Ast.Var "?"; _ }, _) when is_and ->
mk (Ast.If (ex, rest, Some (mk (Ast.Var "false"))))
| _ ->
let body =
if is_and then mk (Ast.If (tvar, rest, Some tvar))
else mk (Ast.If (tvar, tvar, Some rest))
in
mk (Ast.Let ([ bind ], [ body ])))
in
go args

View File

@ -165,7 +165,7 @@ Each item: the proposal, then the reason in one line.
- **Precedence**, low to high: `or` < `and` < `not` < comparisons
(`== != < <= > >=`) < `??` < `||` < `^^` < `&&` < `<< >>` < `+ -` < `* / %` <
prefix `-` and `~~` < postfix (call, index, field, `!`, `?.`). **Built.** An operator
prefix `-` and `~~` < postfix (call, index, field, `!`, `?`, `?.`). **Built.** An operator
glued to `(` is always a call. The bit operators sit where Python and Rust
put them, so `x && mask == 0` is `(x && mask) == 0`.
- **The bit operators** are `a && b`, `a || b`, `a ^^ b` and `~~a`, reading
@ -286,10 +286,22 @@ Each item: the proposal, then the reason in one line.
`elif let P = v` is a further `if-let` nested in that else.
Kept with no `else` at the end of its chain, it gives an Option as `when` does.
`P` is any `match` pattern, and its names are bound in the block only. One
line: `if let Some(g) = o then g else 0`. A plain name, `if let g = o`,
binds what an Option holds (`if let Some(g) = o`), or a dyn when it is not
`nil`; over any other type, and for `_`, it cannot fail and is refused
toward `let`. **Built.**
line: `if let Some(g) = o then g else 0`. A plain name, `if let g = o`, is
refused toward `if o?` and `if o? as g` below; `_` is refused toward `let`.
**Built.**
- **`x?` tests that a value is present** (decision 133): a bool, true when an
Option is `Some` and when a dyn is not `nil`. It reads `(? x)`. In `if x?`,
`elif x?` and `while x?`, and in the rest of an `and` after the test, a local
`x` that is an Option is its payload in the block (a dyn stays a dyn). It is
the same storage, so `x.count += 1` there changes the Option's payload. Not
in the `else`, not after the block, and not through `or` or `not`. In the
block `x` may be given a value of the payload's type, which keeps it
present; giving it an Option is refused. **Built.**
- **`e? as g`** names what a test found, for an `e` that is not a plain name:
`if get(grid, r, c)? as cell` reads `(if-let [cell (get grid r c)] …)`, an
`if-let` over a plain name, which binds what an Option holds or a dyn that
is not `nil`. It works after `if`, `elif` and `while`; `while e? as g` plus
a block reads `(while true (if-let [g e] (do …) (break)))`. **Built.**
- **`while c`, `until c`**, optional label first: `while :outer c`. **Built.**
- **`for i in range(n)`**, `range(a, b)`, `range(a, b, step)` read as
`dotimes`. `range` here is syntax, not a function. `..` is avoided because
@ -437,7 +449,10 @@ After `:` and `->`, a small type grammar that reads to today's type forms:
`i32`, `$t`, `()`, `[T]`, `[const T]`, `[n T]`, `Vec(T)`, `Map(K, V)`,
`Option(T)`, `Ptr(T)`, `Ptr(const T)`, `Fn(A, B) -> R`, `CFn(A) -> R`,
`rl/Vector2`. `T?` is `Option(T)` anywhere a type is written: `[i32?]`,
`Vec(Shape?)`, `Option(i32)?`. **Built** (the arrow is read only in a type position; inside a
`Vec(Shape?)`, `Option(i32)?`, and a lowercase type's `grain?`. Where a value
is written, `?` after a capitalised or primitive type's name is still the
type, `vec-new(i32?)`; after anything else it is the test `x?`. `i32??` and
`x!!` are refused toward `Option(i32?)` and `(x!)!`. **Built** (the arrow is read only in a type position; inside a
value, `vec-new(Fn([i32], i32))` is the call spelling).
### Macro templates

View File

@ -1,5 +1,5 @@
;; Swift optionals over dyn values: nil is the absence. ??, !, if let over a
;; plain name, and chaining through a map's field.
;; Swift optionals over dyn values: nil is the absence. ??, !, a test's as,
;; and chaining through a map's field.
let calls = 0
@ -8,16 +8,16 @@ fn fallback(v)
v
fn describe(o, p)
if let g = o
if o? as g
g + 100
elif let h = p
elif p? as h
h + 200
else
0
;; A kept if let with no else: the value, or nil.
fn kept(o) -> dyn
if let g = o then g * 2
if o? as g then g * 2
fn power(car) = car.engine?.power

View File

@ -1,5 +1,4 @@
;; Swift optionals over typed values: T?, ??, !, if let over a plain name,
;; and chaining through a field and a function held in a field.
;; Swift optionals over typed values: T?, ??, !, a test's as, and chaining through a field and a function held in a field.
struct Engine
power: i32
@ -24,9 +23,9 @@ fn fallback(v: i32) -> i32
v
fn describe(o: i32?) -> i32
if let g = o
if o? as g
g + 100
elif let h = find([None, Some(5)], 1)
elif find([None, Some(5)], 1)? as h
h
else
0

View File

@ -0,0 +1,31 @@
;; x? over a dyn: true when it is not nil. A dyn x stays a dyn in the block,
;; and e? as g binds a dyn that is present (decision 133).
fn describe(a, b)
if a?
a + 100
elif b? and b > 5
b + 200
else
0
fn run(a, n)
println(a?, n?)
println(describe(a, n), describe(n, 9), describe(n, 2))
let m = {:name "ann" :pet nil}
if m.pet? as pet
println(pet)
else
println("no pet")
if m.name? as who
println(who)
let xs = [1, 2, nil, 4]
let i = 0
let total = 0
while get(xs, i)? as x
total += x
i += 1
println(total, i)
fn main()
run(3, nil)

View File

@ -0,0 +1,76 @@
;; x? tests that a value is present, and in if x?, elif x? and while x? a
;; local x is its payload inside the block (decision 133). e? as g names what
;; a test found.
struct Cell
count: i32
struct Node
v: i32
next: i32?
fn find(xs: [3 i32], k: i32) -> i32?
for i in range(3)
if xs[i] == k
return Some(i)
None
fn describe(a: i32?, b: i32?) -> i32
if a?
a + 100
elif b? and b > 5
b + 200
else
0
fn main()
let a: i32? = Some(3)
let n: i32? = None
println(a?, n?, not n?)
;; Narrowed in the block and in the rest of the condition.
if a? and a > 1
println(a * 2)
;; Not in the else, and not after the block: there a is still an Option.
if n?
println(n + 1)
else
println(n ?? -1)
println(a ?? 0)
println(describe(Some(1), None), describe(None, Some(9)), describe(None, Some(2)))
;; A field set through a narrowed name lands in the Option itself.
let c: Cell? = Some(Cell{.count 1})
if c?
c.count += 10
println(c!.count)
;; Assigning the payload's type keeps it present.
let m: i32? = Some(1)
if m?
m = m + 41
println(m!)
;; as names what a test found, when what is tested is not a plain name.
if find([4, 5, 6], 6)? as at
println(at)
if find([4, 5, 6], 7)? as at
println(at)
else
println("absent")
;; while as: pop until there is nothing left.
let nodes = [Node{.v 1 .next Some(1)}, Node{.v 2 .next Some(2)}, Node{.v 3 .next None}]
let cur: i32? = Some(0)
let total = 0
while cur? as i
total += nodes[i].v
cur = nodes[i].next
println(total)
;; while x? narrows the body.
let k: i32? = Some(3)
let steps = 0
while k?
steps += k
k = k - 1
if k == 0
break
println(steps)
;; A kept test with no else gives an Option.
let w = if a? then a * 5
println(w ?? 0)

View File

@ -2273,7 +2273,9 @@ let () =
outputs path ("programs/" ^ path) want;
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) ];
[ ("optionals.fln", optionals_out); ("optionals-dyn.fln", optionals_dyn_out);
(* 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\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. *)
List.iter
(fun (x86, arg, want) ->

View File

@ -1392,7 +1392,6 @@ let () =
refuses "a renamed prelude question" "starts-with?(a, b)"
"indent/question-name" "has-prefix";
refuses "a ? in a binding" "let ok? = 1" "indent/question-name" "is-ok";
refuses "a ? on a field" "x.done?" "indent/question-name" "is-done";
refuses "a ! in a name" "set!(x)" "indent/mark-in-name" "Leave it out: set";
refuses "a ? inside a name" "a?b" "indent/mark-in-name" "cannot contain ?";
refuses "a glued ??" "x??y" "indent/unspaced-operator" "x ?? y";
@ -1406,11 +1405,31 @@ let () =
reads "?. reads the rest over a fresh name" "x = a?.b.c(1)?.d"
"(set x (?. [~o1 a] (?. [~o2 ((.c (.b ~o1)) 1)] (.d ~o2))))";
reads "?[ indexes" "x = f(a)?[2]" "(set x (?. [~o1 (f a)] (at ~o1 2)))";
reads "if let over a plain name" "if let g = x\n g\nelif let h = y\n h"
"(if-let [g x] g (if-let [h y] h))";
refused "if-let-name-i32.fln"
"fn main()\n let x = 5\n if let g = x\n println(g)\n"
[ "always holds a value"; "let g = ..." ];
(* Decision 133: x? tests, narrows a local, and e? as g names what it
found; if let over a plain name is refused toward those. *)
refuses "if let over a plain name" "if let g = x\n g" "indent/if-let-name"
"write if x?, and in the block it is what it holds; to name what it holds, \
write if x? as g";
reads "x? is a test" "y = f(x)? and not z.w?" "(set y (and (? (f x)) (not (? (.w z)))))";
reads "e? as g" "if f(x)? as g\n g\nelif y? as h\n h\nelse\n 0"
"(if-let [g (f x)] g (if-let [h y] h 0))";
reads "one-line e? as g" "v = if y? as h then h else 0" "(set v (if-let [h y] h 0))";
reads "while e? as g" "while pop(s)? as x\n f(x)"
"(while true (if-let [x (pop s)] (do (f x)) (break)))";
refuses "as after no test" "if x as y\n y" "indent/as-test" "Write x? as name";
refused "present-i32.fln" "fn main()\n let x = 5\n println(x?)\n"
[ "x is i32, which always holds a value, so x? has nothing to test";
"a yes-or-no name starts with is- or has-, as in is-x" ];
refused "narrowed-set.fln"
"fn main()\n let x: i32? = Some(1)\n if x?\n x = None\n println(x ?? 0)\n"
[ "x is tested with x? above, so in this block it is i32";
"while x? as item" ];
refused "not-narrowed-in-else.fln"
"fn main()\n let x: i32? = None\n if x?\n println(x + 1)\n else\n println(x + 1)\n"
[ "Option(i32)" ];
refused "not-narrowed-through-or.fln"
"fn main()\n let x: i32? = None\n if x? or true\n println(x + 1)\n"
[ "Option(i32)" ];
refused "coalesce-i32.fln"
"fn main()\n let x = 5\n println(x ?? 1)\n"
[ "the left side of ?? is i32, which always holds a value" ];
@ -1425,9 +1444,6 @@ let () =
refuses "x!! is not read" "y = x!!" "indent/double-unwrap" "(x!)!";
refuses "!= with its space missing names what follows" "y = x!= z"
"indent/unspaced-operator" "x != z";
refuses "? on a value" "y = f(x)?" "indent/value-question" "f(x)?.field reads through it";
refuses "? on a name in a value" "y = ready?" "indent/question-name"
"If ready is a type, ready? is Option(ready)";
refuses "? on a parameter's name" "fn f(ok?: bool) = ok" "indent/question-name" "is-ok";
reads "a lowercase type takes ?" "fn f(a: grain?, b: [grain?]) -> grain? = a"
"(defn f [a (Option grain) b [(Option grain)]] (Option grain) a)";