A pause mark on any column of a condition keeps what it binds, an as in parentheses gets its own refusal, and what an as finds is held in one slot.

This commit is contained in:
Joseph Ferano 2026-09-26 17:52:06 +07:00
parent c4547adb25
commit e4d0e9c60e
8 changed files with 137 additions and 29 deletions

View File

@ -819,6 +819,10 @@ One spelling for one operation; != stays, and not= is refused with a suggestion
of !=. of !=.
* Checker * Checker
** TODO An error in a callee's condition adds a bogus one at main
=fn f(a)= with a refused condition (=if n + 1= over an i32), and =fn main()= calling =f(3)=
last, also reports "main returns i32 or nothing, not Never": the recovered body reads as
Never and main's last form inherits it.
** TODO A u64 above the i64 maximum becomes -1 when it crosses into dyn ** TODO A u64 above the i64 maximum becomes -1 when it crosses into dyn
=(+ z u)= and =(max u 0 z)= with u = u64 max read u as -1, silently. It should trap at =(+ z u)= and =(max u 0 z)= with u = u64 max read u as -1, silently. It should trap at
the crossing, as a u64 field read through a view already does. the crossing, as a u64 field read through a view already does.

View File

@ -91,6 +91,10 @@ and expr_kind =
(Option T) tested by [x?] in the condition above it, read as its (Option T) tested by [x?] in the condition above it, read as its
payload ([if x?] narrowing, decision 133). *) payload ([if x?] narrowing, decision 133). *)
| Narrow of string list * expr | Narrow of string list * expr
(* Made by the checker, never read: [body] with each [(g, h)] reading [g]
as the binding of the hidden name [h], what an [as] in the condition
above found (decision 136). *)
| Alias of (string * string) list * expr
| Struct of string * (string * expr) list (* (Cursor {.src s}) *) | 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 (* {.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 not name a type, so this node carries no name and is only checkable where
@ -480,6 +484,7 @@ let map_children f (e : expr) : expr =
| IfLet (s, a, e) -> IfLet (ex s, arm a, Option.map ex e) | IfLet (s, a, e) -> IfLet (ex s, arm a, Option.map ex e)
| Chain (n, v, b) -> Chain (n, ex v, ex b) | Chain (n, v, b) -> Chain (n, ex v, ex b)
| Narrow (ns, b) -> Narrow (ns, ex b) | Narrow (ns, b) -> Narrow (ns, ex b)
| Alias (ps, b) -> Alias (ps, ex b)
| Struct (n, fs) -> Struct (n, List.map (fun (n, v) -> (n, ex v)) fs) | 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) | 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) | MapLit (tag, kvs) -> MapLit (tag, List.map (fun (k, v) -> (ex k, ex v)) kvs)
@ -604,7 +609,14 @@ let mark_pause ?fn ~line ~col (ds : decl list) : decl list option =
reports, and the line DWARF names, nowhere. *) reports, and the line DWARF names, nowhere. *)
{ e with e = Do [ pause_call ?fn e.loc; e ] } { e with e = Do [ pause_call ?fn e.loc; e ] }
end end
else map_children walk e else
match e.e with
(* A call's name is not a form of its own: a mark on it, the [?] of
[x?] or the [>] of [(> a b)], stops before the call. *)
| Call ({ e = Var _; loc = hl }, _) when at hl ->
hit := true;
{ e with e = Do [ pause_call ?fn e.loc; e ] }
| _ -> map_children walk e
in in
let body es = List.map walk es in let body es = List.map walk es in
let decl (d : decl) = let decl (d : decl) =
@ -641,7 +653,18 @@ let as_name g =
g <> "" && (match g.[0] with 'A' .. 'Z' -> false | _ -> true) g <> "" && (match g.[0] with 'A' .. 'Z' -> false | _ -> true)
&& g <> "true" && g <> "false" && g <> "true" && g <> "false"
(* [x] when [c] is [x] with a pause mark in front of it, [Do [pause; x]]:
[mark_pause] wraps whatever starts at the column marked, a test of a
condition's chain included, and the chain still means what it did. *)
let unpause (c : expr) =
match c.e with
| Do [ { e = Call ({ e = Var _; _ }, []); loc }; x ] when loc = x.loc -> Some x
| _ -> None
let rec as_binds (c : expr) = let rec as_binds (c : expr) =
match unpause c with
| Some x -> as_binds x
| None ->
match c.e with match c.e with
| If (_, q, Some { e = Var "false"; _ }) -> as_binds q | If (_, q, Some { e = Var "false"; _ }) -> as_binds q
| IfLet (_, { pat = Pctor (g, []); body = [ q ]; _ }, Some { e = Var "false"; _ }) | IfLet (_, { pat = Pctor (g, []); body = [ q ]; _ }, Some { e = Var "false"; _ })

View File

@ -3184,8 +3184,12 @@ let unnarrowable_in (body : Ast.expr list) =
List.iter (walk ~in_fn:false) body; List.iter (walk ~in_fn:false) body;
!out !out
(* A name an [as] bound to what an Option held: the slot holding the Option,
read as its payload, as a narrowed local is, but not assignable. *)
let as_tag = "~as"
let local_of loc (b : binding) = let local_of loc (b : binding) =
if b.bwhat = Some narrowed_tag then if b.bwhat = Some narrowed_tag || b.bwhat = Some as_tag then
mk loc b.bty (Tast.Field (mk loc (Types.Option b.bty) (Tast.Local b.slot), 1)) 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) else mk loc b.bty (Tast.Local b.slot)
@ -6559,6 +6563,15 @@ and check_value ctx ?want (e : Ast.expr) : Tast.expr =
| Ast.Narrow (names, body) -> | Ast.Narrow (names, body) ->
with_narrowed ctx names (fun () -> with_narrowed ctx names (fun () ->
ctx.tail <- tail; ctx.used <- used; check ctx ?want body) ctx.tail <- tail; ctx.used <- used; check ctx ?want body)
| Ast.Alias (pairs, body) ->
scoped ctx (fun () ->
List.iter
(fun (g, h) ->
match lookup ctx h with
| Some b -> ctx.scope <- (g, b) :: ctx.scope
| None -> ())
pairs;
ctx.tail <- tail; ctx.used <- used; check ctx ?want body)
(* Constant integer arithmetic where a type variable is wanted is folded to (* Constant integer arithmetic where a type variable is wanted is folded to
the literal it computes first, so [(+ x (+ 1 2))] is admitted wherever 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 [(+ x 3)] is. The instantiation re-checks the form unfolded, at a concrete
@ -8542,11 +8555,7 @@ and check_if_tested ctx ~tail ~used ?want loc c t e =
write, bound in the scope this if was given; the else is checked write, bound in the scope this if was given; the else is checked
without it. *) without it. *)
let cv, named = as_cond ctx c in let cv, named = as_cond ctx c in
let bnd (g, h) = (cv, { t with Ast.e = Ast.Alias (named, t) })
{ Ast.bname = g; bty = None; bval = { Ast.e = Ast.Var h; loc = t.Ast.loc };
bloc = t.Ast.loc }
in
(cv, { t with Ast.e = Ast.Let (List.map bnd named, [ t ]) })
in in
(* Both arms are the tail, and a one-armed [if] counts: [(when c (recur ...))] (* 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 is how nearly every loop is written, and the branch is still the last
@ -10687,6 +10696,9 @@ and if_let_name ctx ~tail ~used ?want loc scrutinee n (arm : Ast.arm) els =
in the block it guards (decision 133). Not through [or] or [not], where in the block it guards (decision 133). Not through [or] or [not], where
the test holding says nothing about [x]. *) the test holding says nothing about [x]. *)
and narrows (c : Ast.expr) = and narrows (c : Ast.expr) =
match Ast.unpause c with
| Some x -> narrows x
| None ->
match c.Ast.e with match c.Ast.e with
| Ast.Call ({ Ast.e = Ast.Var "?"; _ }, [ { Ast.e = Ast.Var x; _ } ]) -> [ x ] | 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 | Ast.If (p, q, Some { Ast.e = Ast.Var "false"; _ }) -> narrows p @ narrows q
@ -10701,13 +10713,21 @@ and as_name g = Ast.as_name g
(* A condition with [as] in it, as the bool it tests, and each name it binds (* A condition with [as] in it, as the bool it tests, and each name it binds
with the hidden name the block reads it through. The chain runs left to with the hidden name the block reads it through. The chain runs left to
right and stops at the first test that fails, so each value is found right and stops at the first test that fails, so each value is found
once; what an [as] finds is copied into its name's slot there, and a once. What an [as] tests is held in one slot, and its name reads that
later test and the block read that slot. *) slot: the payload of an Option held there, or the dyn itself. *)
and as_cond ctx (c : Ast.expr) = and as_cond ctx (c : Ast.expr) =
let named = ref [] in let named = ref [] in
let no loc = mk loc Types.Bool (Tast.Bool false) in let no loc = mk loc Types.Bool (Tast.Bool false) in
let rec go (c : Ast.expr) = let rec go (c : Ast.expr) =
let loc = c.Ast.loc in let loc = c.Ast.loc in
match Ast.unpause c, c.Ast.e with
(* A pause mark on a test of the chain stops before it and leaves the
chain as it was. *)
| Some x, Ast.Do [ pause; _ ] ->
let pv = check ctx pause in
let xv = go x in
mk loc Types.Bool (Tast.Do [ pv; xv ])
| _, _ ->
match c.Ast.e with match c.Ast.e with
| Ast.If (p, q, Some { Ast.e = Ast.Var "false"; _ }) when as_binds q <> [] -> | Ast.If (p, q, Some { Ast.e = Ast.Var "false"; _ }) when as_binds q <> [] ->
let pv = check_truthy ctx p in let pv = check_truthy ctx p in
@ -10718,10 +10738,10 @@ and as_cond ctx (c : Ast.expr) =
let ev = check ctx e in let ev = check ctx e in
let hs = fresh_slot ctx ev.Tast.ty in let hs = fresh_slot ctx ev.Tast.ty in
let hv = mk loc ev.Tast.ty (Tast.Local hs) in let hv = mk loc ev.Tast.ty (Tast.Local hs) in
let test, payload, ty = let test, what, ty =
match ev.Tast.ty with match ev.Tast.ty with
| Types.Option t -> (opt_is_some loc hv, opt_payload loc t hv, t) | Types.Option t -> (opt_is_some loc hv, Some as_tag, t)
| Types.Dyn -> (dyn_not_nil loc hv, hv, Types.Dyn) | Types.Dyn -> (dyn_not_nil loc hv, None, Types.Dyn)
| t -> | t ->
Loc.failk "check/as-not-optional" e.Ast.loc Loc.failk "check/as-not-optional" e.Ast.loc
"%s is %s, which always holds a value, so as has nothing to test. \ "%s is %s, which always holds a value, so as has nothing to test. \
@ -10730,21 +10750,12 @@ and as_cond ctx (c : Ast.expr) =
name, as in i32(x)" name, as in i32(x)"
(source_text e) (tyname loc t) (source_text e) (tyname loc t)
in in
let slot, qv = let b = { slot = hs; bty = ty; assignable = false; bwhat = what; blit = None } in
scoped ctx (fun () -> incr held_n;
let slot = bind ctx g ty ~assignable:false in named := (g, Printf.sprintf "~as%d" !held_n, b) :: !named;
(match lookup ctx g with let qv = scoped ctx (fun () -> ctx.scope <- (g, b) :: ctx.scope; go q) in
| Some b ->
incr held_n;
named := (g, Printf.sprintf "~as%d" !held_n, b) :: !named
| None -> ());
(slot, go q))
in
mk loc Types.Bool mk loc Types.Bool
(Tast.Let ([ (hs, ev) ], (Tast.Let ([ (hs, ev) ], [ mk loc Types.Bool (Tast.If (test, qv, no loc)) ]))
[ mk loc Types.Bool
(Tast.If (test, mk loc Types.Bool (Tast.Let ([ (slot, payload) ], [ qv ])),
no loc)) ]))
| _ -> check_truthy ctx c | _ -> check_truthy ctx c
in in
let cv = go c in let cv = go c in

View File

@ -1456,6 +1456,13 @@ and primary p : Form.t * int =
(match (peek p).tok with (match (peek p).tok with
| RP -> ignore (advance p) | RP -> ignore (advance p)
| EOF -> unclosed p '(' l0 | EOF -> unclosed p '(' l0
| NAME "as" ->
failk "as-paren" (peek p).loc
"as names what a test found for the block of the if, elif, while \
or when it is a test of, so it stands in that condition's and \
chain and not inside parentheses. Write it without them: if %s as \
g and ..."
(text_of e)
| COMMA -> | COMMA ->
failk "tuple" (peek p).loc failk "tuple" (peek p).loc
"parentheses group one value, and this comma starts a second. \ "parentheses group one value, and this comma starts a second. \

View File

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

View File

@ -1315,7 +1315,9 @@ and is_as (f : Form.t) =
match f.v with List ({ v = Sym "as"; _ } :: _) -> true | _ -> false match f.v with List ({ v = Sym "as"; _ } :: _) -> true | _ -> false
and as_chain f (args : Form.t list) : Ast.expr = and as_chain f (args : Form.t list) : Ast.expr =
let no (x : Form.t) = { Ast.e = Ast.Var "false"; loc = x.loc } in (* At no position, so a pause mark cannot land on the chain's own
[false]: it has none in the source. *)
let no (_ : Form.t) = { Ast.e = Ast.Var "false"; loc = Loc.unknown } in
let rec go = function let rec go = function
| [] -> { Ast.e = Ast.Var "true"; loc = f.loc } | [] -> { Ast.e = Ast.Var "true"; loc = f.loc }
| [ x ] when not (is_as x) -> expr x | [ x ] when not (is_as x) -> expr x
@ -1325,7 +1327,10 @@ and as_chain f (args : Form.t list) : Ast.expr =
| x :: _ when is_as x -> fail x "as is (as name value)" | x :: _ when is_as x -> fail x "as is (as name value)"
| x :: rest -> { Ast.e = Ast.If (expr x, go rest, Some (no x)); loc = x.loc } | x :: rest -> { Ast.e = Ast.If (expr x, go rest, Some (no x)); loc = x.loc }
in in
go args (* The chain's own node is at the [and], so a pause mark there has a form
to stop before. *)
let top = go args in
{ top with Ast.loc = f.loc }
and shortcircuit f (args : Form.t list) ~is_and : Ast.expr = and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
let mk e = { Ast.e; loc = f.loc } in let mk e = { Ast.e; loc = f.loc } in

View File

@ -2038,4 +2038,57 @@ let () =
| exception Loc.Error _ -> () | exception Loc.Error _ -> ()
| exception Loc.Errors _ -> fail "one error came as a list")); | exception Loc.Errors _ -> fail "one error came as a list"));
(* A pause mark at any column of a condition leaves what it binds bound:
an [as] (decision 136), and an [x?] narrowing through [and] (133).
[mark_pause] wraps a test of the chain in [(do (pause) test)]. *)
(let t, _ = Session.create ~file:"programs/reload.flan" () in
let marks name origin src line =
let text = List.nth (String.split_on_char '\n' src) (line - 1) in
let hits = ref 0 in
for col = 1 to String.length text do
let syntax = if Filename.check_suffix origin ".fln" then Source.Indented else Source.Paren in
match
Source.with_code ~syntax ~at:None (fun () ->
Session.eval ~origin ~pause:(line, col) t src)
with
| _ -> incr hits
| exception Loc.Error d when has d.Loc.dmsg "nothing to pause" -> ()
| exception Loc.Error d ->
fail "%s, a mark at %d:%d: %s" name line col d.Loc.dmsg
| exception e -> fail "%s, a mark at %d:%d: %s" name line col (Printexc.to_string e)
done;
if !hits < 3 then fail "%s: only %d columns took a mark" name !hits
in
marks "if o? as g" "mark-a.fln" "fn pa(o: i32?) -> i32\n if o? as g then g else 0\n" 2;
marks "if o as g and" "mark-b.fln" "fn pb(o: i32?) -> i32\n if o as g and g > 1 then g else 0\n" 2;
marks "if o? and" "mark-c.fln" "fn pc(o: i32?) -> i32\n if o? and o > 1 then o else 0\n" 2;
let paren = "(defn pd [o (Option i32)] i32 (if (and (as g o) (> g 1)) g 0))" in
marks "(and (as g o) ...)" "<eval>" paren 1;
(* The chain has a node of its own at the [(and]. *)
let col =
let rec find i = if String.sub paren i 4 = "(and" then i + 1 else find (i + 1) in
find 0
in
(match Session.eval ~pause:(1, col) t paren with
| _ -> ()
| exception Loc.Error d -> fail "a mark at (and: %s" d.Loc.dmsg);
(* What an as finds is held once, and its name reads that slot: one
binding in the function, where a copy into g's own slot and another
into the block's would be three. *)
ignore
(Source.with_code ~syntax:Source.Indented ~at:None (fun () ->
Session.eval ~origin:"copies.fln" t
"fn pe(o: i32?) -> i32\n if o? as g and g > 1 then g + 1 else 0\n"));
match
List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "pe") t.Session.program.Tast.fns
with
| None -> fail "pe was not installed"
| Some f ->
let n = ref 0 in
List.iter
(Tast.walk (fun (e : Tast.expr) ->
match e.Tast.e with Tast.Let (bs, _) -> n := !n + List.length bs | _ -> ()))
f.Tast.body;
if !n <> 1 then fail "if o? as g binds %d slots, wanted 1" !n);
Test_support.report ~label:"session" () Test_support.report ~label:"session" ()

View File

@ -1432,6 +1432,10 @@ let () =
refuses "or after as" "if f(x) as g or b\n g" "indent/as-or" "nothing to name"; refuses "or after as" "if f(x) as g or b\n g" "indent/as-or" "nothing to name";
refuses "or later in the chain" "if f(x) as g and a or b\n g" "indent/as-or" "nothing to name"; refuses "or later in the chain" "if f(x) as g and a or b\n g" "indent/as-or" "nothing to name";
refuses "as under not" "if not f(x) as g\n g" "indent/as-not" "not turns the test around"; refuses "as under not" "if not f(x) as g\n g" "indent/as-not" "not turns the test around";
refuses "as in parentheses under not" "if not (a as g)\n g" "indent/as-paren"
"not inside parentheses";
refuses "as in a bracketed chain" "if (a as g and g > 1) and b\n g" "indent/as-paren"
"if a as g and";
refuses "as after until" "until f(x) as g\n g" "indent/as-until" "Write while"; refuses "as after until" "until f(x) as g\n g" "indent/as-until" "Write while";
refused "as-not-optional.fln" "fn main()\n let n = 5\n if n as g and g > 1\n println(g)\n" refused "as-not-optional.fln" "fn main()\n let n = 5\n if n as g and g > 1\n println(g)\n"
[ "n is i32, which always holds a value, so as has nothing to test"; [ "n is i32, which always holds a value, so as has nothing to test";