diff --git a/TODO.org b/TODO.org index 293250cf..70289176 100644 --- a/TODO.org +++ b/TODO.org @@ -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. diff --git a/emacs/flan-fln-mode.el b/emacs/flan-fln-mode.el index a9b39aae..f550fe30 100644 --- a/emacs/flan-fln-mode.el +++ b/emacs/flan-fln-mode.el @@ -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) diff --git a/emacs/test-flan-fln.el b/emacs/test-flan-fln.el index fd5c7976..42ec72af 100644 --- a/emacs/test-flan-fln.el +++ b/emacs/test-flan-fln.el @@ -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) diff --git a/lib/ast.ml b/lib/ast.ml index e99071b5..2d85db01 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -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) diff --git a/lib/check.ml b/lib/check.ml index 7425fed6..478554e2 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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."); diff --git a/lib/emit.ml b/lib/emit.ml index c5d95105..29787bfe 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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) diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml index df3666b6..d3b6f6b6 100644 --- a/lib/indent_reader.ml +++ b/lib/indent_reader.ml @@ -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 diff --git a/lib/load.ml b/lib/load.ml index 961fa938..1ac7734c 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -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 diff --git a/lib/parse.ml b/lib/parse.ml index 7977387d..757f8803 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -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 diff --git a/spec-syntax.md b/spec-syntax.md index 4376a35b..3a3e2546 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -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 diff --git a/test/programs/optionals-dyn.fln b/test/programs/optionals-dyn.fln index 99303a39..bc46d557 100644 --- a/test/programs/optionals-dyn.fln +++ b/test/programs/optionals-dyn.fln @@ -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 diff --git a/test/programs/optionals.fln b/test/programs/optionals.fln index 5efd5340..2bf63c3b 100644 --- a/test/programs/optionals.fln +++ b/test/programs/optionals.fln @@ -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 diff --git a/test/programs/presence-dyn.fln b/test/programs/presence-dyn.fln new file mode 100644 index 00000000..644e7d35 --- /dev/null +++ b/test/programs/presence-dyn.fln @@ -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) diff --git a/test/programs/presence.fln b/test/programs/presence.fln new file mode 100644 index 00000000..2e412f90 --- /dev/null +++ b/test/programs/presence.fln @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index c349049b..fc58eaa6 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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) -> diff --git a/test/test_syntax.ml b/test/test_syntax.ml index 1c35cfa4..bffae0d4 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -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)";