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:
parent
6c6b5a60cb
commit
d252eee2b2
6
TODO.org
6
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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
108
lib/check.ml
108
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.");
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
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
|
||||
(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))
|
||||
form (label @ (c :: body)))
|
||||
| "for" ->
|
||||
let label =
|
||||
match (peek p).tok with
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
(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 ]))
|
||||
mk (Ast.Let ([ bind ], [ body ])))
|
||||
in
|
||||
go args
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
31
test/programs/presence-dyn.fln
Normal file
31
test/programs/presence-dyn.fln
Normal 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)
|
||||
76
test/programs/presence.fln
Normal file
76
test/programs/presence.fln
Normal 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)
|
||||
@ -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) ->
|
||||
|
||||
@ -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)";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user