From 6c6b5a60cbf6d5ffb0ae5f124bfcd663cf4e409b Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sat, 26 Sep 2026 15:36:49 +0700
Subject: [PATCH] ?? continues a line, a chain or an unwrap is no place to
assign, nested marks and a ? on a value say what to write, a lowercase type
takes ?, and a predicate reads as a word with its name beside it.
---
TODO.org | 32 +++++----
lib/check.ml | 57 +++++++++++----
lib/indent_printer.ml | 3 +-
lib/indent_reader.ml | 122 +++++++++++++++++++++++++++++----
lib/prelude.ml | 2 +-
test/programs/generics.flan | 4 +-
test/programs/int-generic.flan | 2 +-
test/test_acceptance.ml | 14 ++--
test/test_flan.ml | 48 ++++++-------
test/test_syntax.ml | 22 ++++++
web/index.html | 44 ++++++------
11 files changed, 251 insertions(+), 99 deletions(-)
diff --git a/TODO.org b/TODO.org
index a81294e6..293250cf 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=.
+** 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.
+** WAIT flan convert prints the reader's names for the optionals
+=a?.b= prints as =(?. [~o1 a] (.b ~o1))= and =x!= as =(!! x)=. Parked: convert is being
+removed (130).
** WAIT A cheap front for dyn sequences
Decided 2026-09-26 (128) to pause: dyn vectors are mutable, so taking from the front
shifts every element. Options were a linked list (cons/first/rest) or storing the dyn
@@ -649,7 +655,7 @@ exact convention, which is the direction the author is interested in.
** DONE defclass is a named dyn map with a shape tag
CLOSED: [2026-09-20]
An instance is a dyn map with its class in the object header, so =get=, =put=,
-=has-key?= and =length= need no new operation. CLOS dispatch and Clojure's
+=has-key= and =length= need no new operation. CLOS dispatch and Clojure's
arbitrary dispatch are one mechanism: a class dispatcher is the shape tag of the
first argument used as the dispatch function. Method bodies are inlined into one
dispatcher function, so a generic is one top-level name and one cell — adding a
@@ -823,15 +829,15 @@ CLOSED: [2026-09-20]
Numeric scalars bound to one type variable resolve to the join of them all, which
walks back the same day's "widening does not cross a generic binding". A joinless
pair is deferred and re-asked against the final binding, which is what makes
-acceptance order-independent. =integer?= exists because =numeric?= admits floats,
+acceptance order-independent. =is-integer= exists because =is-numeric= admits floats,
where the branch spelling of =abs= hands back a negative zero.
** DONE A conversion is legal at a bounded variable when it is legal at every type the bound admits
CLOSED: [2026-09-21]
-A machine-type target needs =numeric?=; an enum target needs =integer?=;
-=ordered?=, =equal?= and =hashable?= admit nothing. A predicate gates an operation
+A machine-type target needs =is-numeric=; an enum target needs =is-integer=;
+=is-ordered=, =is-equal= and =is-hashable= admit nothing. A predicate gates an operation
by what it claims, not by the set it happens to denote this week — which is why
-=ordered?= is refused even though every type it admits today converts.
+=is-ordered= is refused even though every type it admits today converts.
** DONE The Ptr and union arms of the fill boundary are relaxable
CLOSED: [2026-09-25]
@@ -854,8 +860,8 @@ A =where= clause tells the abstract pass what it may assume, so the body checks
the definition and the call stays =(sort xs)=. Not a type class — a predicate
carries nothing and gates a builtin the compiler already has. The fork that said
an unconstrained =+= over a type variable must be rejected turned out to be false,
-and it is not what Odin does. The five predicates are =ordered?=, =equal?=,
-=hashable?=, =numeric?= and =integer?=.
+and it is not what Odin does. The five predicates are =is-ordered=, =is-equal=,
+=is-hashable=, =is-numeric= and =is-integer=.
** DONE A type variable takes a $ sigil
CLOSED: [2026-09-13]
@@ -873,15 +879,15 @@ the name as written, where the tables are keyed on the bare name.
** DONE Milestone 5 was mostly already there
CLOSED: [2026-09-20]
-What the lane added was a written integer zero standing where a =numeric?=-bounded
-variable stands — legal because every type =numeric?= admits is an integer or a
+What the lane added was a written integer zero standing where a variable bounded by
+=is-numeric= stands — legal because every type =is-numeric= admits is an integer or a
float — the widening boundary, and a refusal for instantiating a type variable at
=dyn=, which names =defgeneric=/=defmethod= as the other spelling. A float literal
-is still refused at a =numeric?= variable, since the predicate covers both halves.
+is still refused at an =is-numeric= variable, since the predicate covers both halves.
-** DONE hashable? gates the type and not the operations
+** DONE is-hashable gates the type and not the operations
CLOSED: [2026-09-13]
-=put=, =get=, =has-key?=, =reserve=, =clone= and =map-remove= are deferred to the
+=put=, =get=, =has-key=, =reserve=, =clone= and =map-remove= are deferred to the
instantiation, joining =print= and =println=. The membership rule is not a
headcount: either the operation cannot fail after substituting, or a declared
predicate gives its failure somewhere to land. A generic that does not declare the
@@ -2192,7 +2198,7 @@ implicit, any other is written. The rest of the page is left for its rewrite.
** DONE plan.org's Types section lists a predicate that no longer exists
CLOSED: [2026-09-25]
-The five predicates are the checker's five, with =integer?= in place of
+The five predicates are the checker's five, with =is-integer= in place of
=copyable?=, and the section no longer names =(Handle $t)= or =pool-new=.
** DONE plan.org's Data model section still describes move-only containers
diff --git a/lib/check.ml b/lib/check.ml
index 4b5746b3..7425fed6 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -3040,6 +3040,19 @@ let rec reaches_dyn (t : Types.t) =
which is the whole reason predicates exist — under the no-constraint rule
[(sort xs)] had to become [(sort-by xs (fn [a b] (< a b)))] at every call
site in the corpus. *)
+(* A predicate as a word in a sentence, [ordered (is-ordered)], and a where
+ clause as the file at [loc] spells one. [v] is the variable with its [$]. *)
+let pred_word p =
+ let w =
+ if String.length p > 3 && String.sub p 0 3 = "is-" then String.sub p 3 (String.length p - 3)
+ else p
+ in
+ if w = p then p else Printf.sprintf "%s (%s)" w p
+
+let where_text loc p v =
+ if Source.indented_at loc then Printf.sprintf "where %s(%s)" p v
+ else Printf.sprintf "{:where (%s %s)}" p v
+
let unconstrained env loc op ~needs (t : Types.t) =
if generic_ty t then
match tyvar_of t with
@@ -3047,10 +3060,11 @@ let unconstrained env loc op ~needs (t : Types.t) =
| _ ->
Loc.failk "check/unconstrained-type-variable" loc
"%s over the type variable %s: nothing declares %s %s. Write \
- {:where (%s %s)} at the head of the body, or take the operation as \
+ %s at the head of the body, or take the operation as \
a parameter, a (Fn [%s %s] ...), and call it here"
- op (tyname loc t) (tyname loc t) needs needs
- (tyname loc t) (tyname loc t) (tyname loc t)
+ op (tyname loc t) (tyname loc t) (pred_word needs)
+ (where_text loc needs (tyname loc t))
+ (tyname loc t) (tyname loc t)
(* ── The runaway instantiation, refused by name rather than by depth ────
@@ -11201,6 +11215,13 @@ and check_call ctx ~want loc (head : Ast.expr) (args : Ast.expr list) =
(rt loc Types.Unit "flan_dev_reg_note_res_done"
[ mk loc Types.String (Tast.Str owner) ])
| _ -> fail loc "internal: %%res-done takes a name — a compiler bug")
+ (* [i32?] where a value goes: the type, not a value. *)
+ | Ast.Var "Option" when fln_source loc ->
+ fail loc
+ "%s is an Option type, and a value is wanted here. On a value that may \
+ hold nothing, x?.field reads through it, x! unwraps it and x ?? d gives \
+ a default"
+ (match Loc.snippet loc with Some t -> t | None -> "this")
| Ast.Var name -> named_call ctx ~want loc name args
(* ((Ptr Color) p): a pointer cast, spelled the way (i32 x) is — the type
is the head. It changes what the pointer is said to point at and nothing
@@ -11412,7 +11433,7 @@ and cast_operand ctx loc name ~needs ?also ~what ~is v =
| ps ->
Printf.sprintf
"The where clause says %s is %s, and that does not make it %s" v
- (String.concat " and " ps) is
+ (String.concat " and " (List.map pred_word ps)) is
in
let fix =
let alt clause =
@@ -11423,8 +11444,11 @@ and cast_operand ctx loc name ~needs ?also ~what ~is v =
| None -> ""
in
if ctx.env.tvpreds = [] then
- Printf.sprintf "write {:where (%s $%s)} at the head of the body%s"
- needs v (alt "{:where (%s $%s)}")
+ Printf.sprintf "write %s at the head of the body%s"
+ (where_text loc needs ("$" ^ v))
+ (if Source.indented_at loc then alt "where %s($%s)" else alt "{:where (%s $%s)}")
+ else if Source.indented_at loc then
+ Printf.sprintf "add %s($%s) to the where clause%s" needs v (alt "%s($%s)")
else
Printf.sprintf "add (%s $%s) to the where clause%s" needs v
(alt "(%s $%s)")
@@ -12632,7 +12656,7 @@ and peeks_string ctx (a : Ast.expr) =
(* = and != over a String and a String or a str: the texts' bytes compared,
through the str view each has, which is the comparison str already has.
- The orderings are refused as they are on a str, naming bytes. *)
+ The orderings are refused as they are on a str, naming is-bytes-less. *)
and string_compare ctx ~want loc name p args =
(match name with
| "=" | "!=" -> ()
@@ -13228,7 +13252,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
| _ ->
fail a.Ast.loc
- "%s takes a is-numeric type, and %s is not one — as in (%s i32)" name
+ "%s takes a numeric type (is-numeric), and %s is not one — as in (%s i32)" name
(tyname loc ty) name
in
expect ctx loc ~want v
@@ -16132,15 +16156,17 @@ and generic_call ctx ~want loc name vars pats pret args =
match List.assoc_opt p.Ast.pvar !subst with
| Some (Types.Var v) when not (declares ctx.env.tvpreds v p.Ast.pname) ->
Loc.failk "check/predicate-not-carried" loc
- "%s is written {:where (%s $%s)}, and this call passes the \
+ "%s is written %s, and this call passes the \
type variable $%s, which nothing here declares %s. Add \
- {:where (%s $%s)} to this function's own clause"
- name p.Ast.pname p.Ast.pvar v p.Ast.pname p.Ast.pname v
+ %s to this function's own clause"
+ name (where_text loc p.Ast.pname ("$" ^ p.Ast.pvar)) v
+ (pred_word p.Ast.pname) (where_text loc p.Ast.pname ("$" ^ v))
| Some t when not (open_ty t) && not (pred_holds p.Ast.pname t) ->
Loc.failk "check/predicate-unsatisfied" loc
- "%s is written {:where (%s $%s)}, and this call passes %s, \
+ "%s is written %s, and this call passes %s, \
which is not %s"
- name p.Ast.pname p.Ast.pvar (tyname loc t) p.Ast.pname
+ name (where_text loc p.Ast.pname ("$" ^ p.Ast.pvar)) (tyname loc t)
+ (pred_word p.Ast.pname)
| _ -> ())
gfn.Ast.fwhere);
expect ctx loc ~want (mk loc cret (Tast.Call (name, targs)))
@@ -16190,9 +16216,10 @@ and instantiate env loc gname vars subst cparams cret =
if not (pred_holds p.Ast.pname t) then
Loc.failk "check/predicate-unsatisfied" loc
"this call instantiates %s at $%s = %s, and %s is not %s. %s is \
- written {:where (%s $%s)} — pass a type the predicate admits"
+ written %s — pass a type the predicate admits"
gname p.Ast.pvar (tyname loc t) (tyname loc t)
- p.Ast.pname gname p.Ast.pname p.Ast.pvar)
+ (pred_word p.Ast.pname) gname
+ (where_text loc p.Ast.pname ("$" ^ p.Ast.pvar)))
fn.Ast.fwhere;
if Hashtbl.mem env.fns sym then
fail loc
diff --git a/lib/indent_printer.ml b/lib/indent_printer.ml
index b3d8ef5b..50e1db4e 100644
--- a/lib/indent_printer.ml
+++ b/lib/indent_printer.ml
@@ -479,7 +479,8 @@ and list f h args =
((if t <> "" && t.[0] = '~' then "~(" ^ t ^ ")" else "~" ^ t), 13)
| Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 13 x, 13)
| Form.Sym s, _ :: _ :: _
- when R.is_binop (infix_op s) && s <> "==" && not (s = "!=" && List.length args > 2) ->
+ when R.is_binop (infix_op s) && s <> "==" && s <> "??"
+ && not (s = "!=" && List.length args > 2) ->
let op = infix_op s in
let lvl = Option.get (R.binop_level op) in
let first = List.hd args and rest = List.tl args in
diff --git a/lib/indent_reader.ml b/lib/indent_reader.ml
index 6be6eb13..df3666b6 100644
--- a/lib/indent_reader.ml
+++ b/lib/indent_reader.ml
@@ -56,7 +56,11 @@ let binops =
[ ("or", 1); ("and", 2);
("==", 4); ("!=", 4); ("<", 4); ("<=", 4); (">", 4); (">=", 4);
("||", 5); ("^^", 6); ("&&", 7);
- ("<<", 8); (">>", 8); ("+", 9); ("-", 9); ("*", 10); ("/", 10); ("%", 10) ]
+ ("<<", 8); (">>", 8); ("+", 9); ("-", 9); ("*", 10); ("/", 10); ("%", 10);
+ (* [??] is here for the line rules — a line ending in it, or one starting
+ with it, continues — and is read by [operand] between 4 and 5, so no
+ level of [binary]'s is its own. *)
+ ("??", 0) ]
let binop_level s = List.assoc_opt s binops
let is_binop s = binop_level s <> None
@@ -149,6 +153,10 @@ let cmp_fresh () = incr cmp_n; Printf.sprintf "~cmp%d" !cmp_n
(* The reader's fresh names for an optional chain's payload, per [read_all]. *)
let opt_n = ref 0
+(* Whether a type is being read, set for [ty]'s extent: there a [?] after
+ any name is [Option], [grain?] included. *)
+let in_type = ref false
+
(* A [-] glued to one of these starts a negation: [-x] is [(- x)]. Anything
else keeps the Lisp reading, so [--], [->] and [-=] stay names. *)
let is_neg_char c =
@@ -215,15 +223,20 @@ let question_fix name =
in
pkg ^ fixed
-let name_refused loc whole =
+let name_refused ?(typed = false) loc whole =
let drop c s = String.concat "" (String.split_on_char c s) in
let n = String.length whole in
if n > 1 && whole.[n - 1] = '?' && not (String.contains (String.sub whole 0 (n - 1)) '?')
&& not (String.contains whole '!') then
+ let base = String.sub whole 0 (n - 1) in
Loc.failk "indent/question-name" loc
"%s is not a name: a name cannot contain ?.\n\n\
- A name for a yes-or-no question starts with is- or has- instead: %s"
- whole (question_fix (String.sub whole 0 (n - 1)))
+ A name for a yes-or-no question starts with is- or has- instead: %s%s"
+ whole (question_fix base)
+ (if typed then
+ Printf.sprintf "\n\nIf %s is a type, %s is Option(%s), written where a \
+ type goes, after : or ->" base whole base
+ else "")
else
let c = if String.contains whole '!' then '!' else '?' in
Loc.failk "indent/mark-in-name" loc
@@ -321,16 +334,33 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list =
| Some d -> String.sub pre (d + 1) (String.length pre - d - 1)
| None -> pre
in
+ (* The word after this run, for an operator written without its
+ spaces: [x!= y] is [x != y]. *)
+ let after_word r =
+ if r <> "" then r
+ else
+ let j = ref st.Reader.pos in
+ while !j < String.length src && src.[!j] = ' ' do incr j done;
+ let k = ref !j in
+ while !k < String.length src && not (Reader.is_delimiter src.[!k]) do incr k done;
+ if !k > !j then String.sub src !j (!k - !j) else "b"
+ in
+ let tail r = String.sub r 1 (String.length r - 1) in
if ch = '?' then begin
+ if rest = "?" && pre <> "" && type_like last && next <> '(' then
+ failk "nested-option" (piece line (col + wa) (String.length whole))
+ "%s is not read: ?? is the default operator. An Option of an \
+ Option is written Option(%s?)" whole last;
if rest <> "" && rest.[0] = '?' && pre <> "" then
failk "unspaced-operator" (piece line at 2)
"?? is an operator here, and a binary operator has a space on \
- each side: %s ?? %s" pre
- (let r = String.sub rest 1 (String.length rest - 1) in
- if r = "" then "d" else r);
+ each side: %s ?? %s" pre (after_word (tail rest));
+ (* A [?] at the end of a name is a type's, [T?], or one the
+ parser refuses as part of a name; which, only the parser
+ knows. Before [(] or inside a name it is a name's. *)
let ends = rest = "" && next <> '(' in
let chain = (rest <> "" && rest.[0] = '.') || (rest = "" && next = '[') in
- if (ends && (pre = "" || type_like last)) || chain then begin
+ if ends || chain then begin
some_part ~after col pre;
emit QUEST (piece line at 1);
marks ~after:true (at + 1) rest
@@ -341,7 +371,11 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list =
if rest <> "" && rest.[0] = '=' then
failk "unspaced-operator" (piece line at 2)
"!= is an operator here, and a binary operator has a space on \
- each side: %s != %s" pre (String.sub rest 1 (String.length rest - 1));
+ each side: %s != %s" pre (after_word (tail rest));
+ if rest <> "" && rest.[0] = '!' then
+ failk "double-unwrap" (piece line at 2)
+ "!! is not read. To unwrap an Option of an Option, unwrap \
+ each level: (%s!)!" (if pre = "" then "x" else pre);
if (rest = "" && next <> '(') || (rest <> "" && rest.[0] = '.') then begin
some_part ~after col pre;
emit BANG (piece line at 1);
@@ -351,7 +385,7 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list =
end
in
let wordy = String.exists (fun c -> Reader.is_digit c || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) body in
- if wordy || body = "?" || body = "!" then marks col body else plain col body;
+ if wordy || body = "?" || body = "!" || body = "!!" then marks col body else plain col body;
if colon then emit COLON (piece line (col + n - 1) 1)
end
in
@@ -817,7 +851,19 @@ let negative_literal = function
(* Something followed a complete value where nothing may. The two shapes that
get their own sentence are the ones a Lisp hand writes: [a -1] and
[f (x)]. *)
+(* A [?] glued to the name just read, somewhere a type is not: the name
+ is what was meant, [let ok? = 1] or [fn f(ok?: bool)]. *)
+let mark_after p =
+ let t = peek p in
+ match t.tok, (last p).tok with
+ | QUEST, NAME n when (not t.sp) && p.i > 0 && n <> "" ->
+ let l = (last p).loc in
+ let n = if n.[0] = '.' then String.sub n 1 (String.length n - 1) else n in
+ name_refused ~typed:(type_like n) { l with Loc.eline = t.loc.Loc.eline; ecol = t.loc.Loc.ecol } (n ^ "?")
+ | _ -> ()
+
let stray p ~after =
+ mark_after p;
let t = peek p in
match t.tok with
| ATOM _ when t.sp && negative_literal t.tok ->
@@ -863,13 +909,14 @@ let expect p tok ~what =
let t = peek p in
if t.tok = tok then ignore (advance p)
else
+ let () = mark_after p in
failk "expected" (where_ p) "expected %s here, and found %s" what
(show t.tok)
let expect_name p s ~what =
match (peek p).tok with
| NAME n when n = s -> ignore (advance p)
- | t -> failk "expected" (where_ p) "expected %s here, and found %s" what (show t)
+ | t -> mark_after p; failk "expected" (where_ p) "expected %s here, and found %s" what (show t)
(* The lets whose first value is being read, innermost first: the column
of the let's first name, the name, and the let's line. A line at that
@@ -938,6 +985,23 @@ let text_of (f : Form.t) =
let s = match from_source with Some t -> t | None -> Form.to_source f in
if String.length s > 40 then String.sub s 0 37 ^ "..." else s
+(* An assignment's place that is an optional chain or an unwrap: neither
+ names storage, and the forms they read to would show the reader's names. *)
+let no_place (e : Form.t) =
+ match e.v with
+ | Form.List ({ v = Form.Sym "?."; _ } :: _) ->
+ failk "chain-assign" e.loc
+ "%s is an optional chain, and a chain cannot be assigned to: when it \
+ holds nothing there is no place to write. Unwrap it first with if let, \
+ then assign through the name it binds"
+ (text_of e)
+ | Form.List [ { v = Form.Sym "!!"; _ }; _ ] ->
+ failk "chain-assign" e.loc
+ "%s unwraps a value, and a value cannot be assigned to. Unwrap it with \
+ if let, then assign through the name it binds"
+ (text_of e)
+ | _ -> ()
+
let unclosed p c l0 =
failk "unclosed" l0
~notes:[ Loc.note (where_ p) "the input ends here, still inside it" ]
@@ -1171,8 +1235,34 @@ and postfix p =
[ sym t.loc "?.";
Form.make (Form.Vec [ sym t.loc h; f ]) f.loc;
rest ]), 12)
- (* [T?]: the lexer lets a [?] end only a type's name or a closer. *)
+ (* [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. *)
| QUEST ->
+ let typish =
+ match f.v with
+ | Form.Sym n -> type_like n
+ | Form.Vec _ -> true
+ | Form.List ({ v = Form.Sym h; _ } :: _) ->
+ 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)
| BANG ->
@@ -1393,10 +1483,12 @@ and inline_stmt p : Form.t =
let e, _ = expr p in
match (peek p).tok with
| NAME "=" ->
+ no_place e;
let eq = advance p in
let v, _ = expr p in
mk p t.loc (Form.List [ sym eq.loc "set"; e; v ])
| NAME op when List.mem_assoc op assign_ops ->
+ no_place e;
let eq = advance p in
let v, _ = expr p in
mk p t.loc (compound eq.loc (List.assoc op assign_ops) e v (span p e.loc))
@@ -1686,7 +1778,9 @@ and map_items p open_loc =
type, [Fn(A, B) -> R], which reads as [(Fn [A B] R)]. *)
let rec ty p : Form.t =
let l0 = (peek p).loc in
- let f, _ = postfix p in
+ let was = !in_type in
+ in_type := true;
+ let f, _ = Fun.protect ~finally:(fun () -> in_type := was) (fun () -> postfix p) in
match f.v, (peek p).tok with
| Form.List (({ v = Form.Sym ("Fn" | "CFn"); _ } as h) :: args), NAME "->"
when (last p).tok = RP ->
@@ -2195,10 +2289,12 @@ and expr_stmt (s : st) : Form.t =
let e, _ = expr p in
match (peek p).tok with
| NAME "=" ->
+ no_place e;
let eq = advance p in
let v = value_line s ~after:(text_of e ^ " =") in
mk p t0.loc (Form.List [ sym eq.loc "set"; e; v ])
| NAME op when List.mem_assoc op assign_ops ->
+ no_place e;
let eq = advance p in
let v = value_line s ~after:(text_of e ^ " " ^ op) in
let o = List.assoc op assign_ops in
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 35a2dd65..d723c9e3 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -1688,7 +1688,7 @@ let source = {flan|
;; different parts of the input, and a caller can see which one came first.
;;
;; It keeps a name of its own rather than collapsing into sort, and the
-;; reason is the point of the predicates: a [u8] is not is-ordered and cannot
+;; reason is the point of the predicates: a [u8] is not ordered (is-ordered) and cannot
;; be, because < is defined on machine numbers and comparing two slices
;; lexicographically is a loop and not an instruction. is-bytes-less is that loop.
;; So this is the shape a generic takes when the operation it needs is not a
diff --git a/test/programs/generics.flan b/test/programs/generics.flan
index 8cbea700..48f564fb 100644
--- a/test/programs/generics.flan
+++ b/test/programs/generics.flan
@@ -37,7 +37,7 @@
(+ x x))
;; is-equal admits = and !=; is-ordered admits < <= > >= min max, and entails
-;; equal?.
+;; is-equal.
(defn count-of [s [$t] x $t] i32
{:where (is-equal $t)}
(let [n 0]
@@ -64,7 +64,7 @@
;; re-checks (> x 0) with $t substituted, and *that* is where the literal is
;; built at the concrete width and range-checked.
;;
-;; The -t? suffix is because the prelude now carries pos?/neg?/is-zero itself.
+;; The -t suffix is because the prelude now carries is-pos/is-neg/is-zero itself.
;; These are the same three bodies written in an ordinary program, which is
;; what says the machinery belongs to the language and not to the prelude.
(defn is-pos-t [x $t] bool {:where (is-numeric $t)} (> x 0))
diff --git a/test/programs/int-generic.flan b/test/programs/int-generic.flan
index 6a48f4cf..4dd594db 100644
--- a/test/programs/int-generic.flan
+++ b/test/programs/int-generic.flan
@@ -111,7 +111,7 @@
(println (abs-f64 -1.5))
(println (abs-f32 -2.5))
- ;; The integer?-only operations, per width.
+ ;; The is-integer-only operations, per width.
(println (low-bits 255 3))
(println (low-bits (u16 65535) (u16 4)))
(println (low-bits (i64 1023) (i64 5)))
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index 70dfeafc..c349049b 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -3812,13 +3812,13 @@ let () =
onto whichever of the two buffers the branch chose.
The six [true]s and the five numbers after the first [8] are the
- literal-at-a-type-variable family: three written bodies — pos?/neg?/
- zero-p?, next-after and plus-300 — reaching i8, u8, u16, i32, i64, f32
+ literal-at-a-type-variable family: three written bodies — is-pos-t, is-neg-t,
+ is-zero-t, next-after and plus-300 — reaching i8, u8, u16, i32, i64, f32
and f64. [255] is next-after at u8 and is the one that would say
whether the placeholder width the abstract pass builds had leaked into
a copy; [301] is plus-300 at i32, whose range check belongs to the copy
and not to the definition. The [true true false] after them is the
- prelude's own pos?/zero?/is-neg — the same three bodies under their real
+ prelude's own is-pos/is-zero/is-neg — the same three bodies under their real
names — and the middle one is is-zero at -0.0, which IEEE says is zero
and which this does not second-guess. *)
let generics_out =
@@ -3849,7 +3849,7 @@ let () =
lines are the collapsed abs at six widths and both signed minimums
(which answer themselves; the negation wraps). The [0 0] after them is
the libm float pair at -0.0, the sign-bit clear no integer body
- spells. Then the integer?-only operations at several widths, and last
+ spells. Then the is-integer-only operations at several widths, and last
the join family: [true true], [6 6] and [42] are mixed widths at one
$t answering identically in both argument orders, from one copy at
the wider type (TODO.org, "abs is one generic, and a bound joins to the
@@ -3954,7 +3954,7 @@ let () =
and cannot change. *)
refuses "a package generic's bound, refused at the call"
"programs/pkg-generic-reject.flan"
- "str is not is-ordered";
+ "str is not ordered (is-ordered)";
refuses "and the refusal quotes the clause the package wrote"
"programs/pkg-generic-reject.flan" "{:where (is-ordered $t)}";
@@ -3987,7 +3987,7 @@ let () =
chain of instantiations and not a depth it gave up at. *)
refuses "an unconstrained operator in a generic body"
"programs/generic-reject.flan"
- "nothing declares $t is-numeric";
+ "nothing declares $t numeric (is-numeric)";
refuses "an unconstrained operator names the way out"
"programs/generic-reject.flan" "{:where (is-numeric $t)}";
refuses "a runaway instantiation" "programs/generic-runaway.flan"
@@ -4015,7 +4015,7 @@ let () =
requirement the author wrote down. What is asserted is that it names
the type passed and the predicate it failed, and not the body. *)
refuses "a generic over maps, instantiated at a key that cannot be hashed"
- "programs/generic-map-reject.flan" "f64 is not is-hashable";
+ "programs/generic-map-reject.flan" "f64 is not hashable (is-hashable)";
refuses "and it names the type the call site asked for"
"programs/generic-map-reject.flan" "at $t = f64";
diff --git a/test/test_flan.ml b/test/test_flan.ml
index 936bbd2f..4c77fe92 100644
--- a/test/test_flan.ml
+++ b/test/test_flan.ml
@@ -1553,7 +1553,7 @@ let () =
accepts "all-distinct over a type variable"
"(defn three [a $t b $t c $t] bool {:where (is-equal $t)} (!= a b c))";
rejects_check "a chain still wants the right predicate"
- ~needle:"nothing declares $t is-ordered"
+ ~needle:"nothing declares $t ordered (is-ordered)"
"(defn between [a $t b $t c $t] bool {:where (is-equal $t)} (< a b c))";
(* One operand and none. Both would have to be [true] whatever they were
handed, which is a typo carrying a value. *)
@@ -6818,7 +6818,7 @@ let () =
accepts "is-numeric admits +"
"(defn add [a $t b $t] $t {:where (is-numeric $t)} (+ a b))";
rejects_check "is-equal does not admit <"
- ~needle:"nothing declares $t is-ordered"
+ ~needle:"nothing declares $t ordered (is-ordered)"
"(defn less [a $t b $t] bool {:where (is-equal $t)} (< a b))";
(* The entailments, which are the reason a signature is one predicate long
rather than two. Every type the language orders is a number or an enum,
@@ -6846,36 +6846,36 @@ let () =
accepts "is-integer admits the shifts"
"(defn dbl [x $t] $t {:where (is-integer $t)} (<< x 1))";
rejects_check "is-numeric does not admit bit-and"
- ~needle:"nothing declares $t is-integer"
+ ~needle:"nothing declares $t integer (is-integer)"
"(defn is-low [x $t] bool {:where (is-numeric $t)} (= (bit-and x 1) 1))";
rejects_check "nor the shifts"
- ~needle:"nothing declares $t is-integer"
+ ~needle:"nothing declares $t integer (is-integer)"
"(defn dbl [x $t] $t {:where (is-numeric $t)} (<< x 1))";
- (* An integer?-bounded caller satisfies a numeric?-bounded callee: the
- entailment carries across generic calls exactly as ordered?-over-equal?
+ (* A caller bounded by is-integer satisfies a callee bounded by is-numeric: the
+ entailment carries across generic calls exactly as is-ordered-over-equal?
does. *)
- accepts "is-integer carries a is-numeric callee"
+ accepts "is-integer carries an is-numeric callee"
"(defn z? [x $t] bool {:where (is-numeric $t)} (= x 0))\n\
(defn odd-z? [x $t] bool {:where (is-integer $t)} (z? (bit-and x 1)))";
(* The integer literal is admitted at a bounded variable by the same arm
under both bounds — the bound promises the literal a meaning at every
type the variable can become, and is-integer's types are a subset of
is-numeric's. *)
- accepts "an integer literal stands where an integer?-bounded $t is wanted"
+ accepts "an integer literal stands where an is-integer-bounded $t is wanted"
"(defn bump [x $t] $t {:where (is-integer $t)} (+ x 300))";
(* A float at is-integer, refused at the call that asked, naming the bound. *)
- rejects_check "a float does not instantiate an integer?-bounded variable"
- ~needle:"f64 is not is-integer"
+ rejects_check "a float does not instantiate an is-integer-bounded variable"
+ ~needle:"f64 is not integer (is-integer)"
"(defn bump [x $t] $t {:where (is-integer $t)} (+ x 1))\n\
(defn main [] () (println (bump 1.5)))";
(* And dyn is refused by the bound too — the clause's own refusal, the more
- specific of the two answers, exactly as at numeric?. *)
- rejects_check "dyn does not instantiate an integer?-bounded variable"
- ~needle:"dyn is not is-integer"
+ specific of the two answers, exactly as at is-numeric. *)
+ rejects_check "dyn does not instantiate an is-integer-bounded variable"
+ ~needle:"dyn is not integer (is-integer)"
"(defn bump [x $t] $t {:where (is-integer $t)} (+ x 1))\n\
(defonce d dyn 5)\n\
(defn main [] () (println (bump d)))";
- (* A float literal inside an integer?-bounded body is refused at the
+ (* A float literal inside an is-integer-bounded body is refused at the
definition, in the bound's own words: there is no instantiation at which
it means anything. *)
rejects_check "a float literal has no meaning under is-integer"
@@ -6914,7 +6914,7 @@ let () =
what is a number, so a conversion under one of them alone is refused —
and the message says which predicate to write. *)
rejects_check "is-ordered does not admit a conversion"
- ~needle:"The where clause says t is is-ordered, and that does not make it \
+ ~needle:"The where clause says t is ordered (is-ordered), and that does not make it \
a number or an enum — add (is-numeric $t) to the where clause, or \
(is-enum $t) for an enum"
"(defn to32 [x $t] i32 {:where (is-ordered $t)} (i32 x))";
@@ -6933,7 +6933,7 @@ let () =
"(defn to32 [x $t] i32 (i32 x))";
(* is-enum is the other bound a conversion to a number takes: it admits the
enums, which convert as an i32, and entails is-ordered and is-equal but not
- numeric?. The running side is programs/enum-generic.flan. *)
+ is-numeric. The running side is programs/enum-generic.flan. *)
accepts "is-enum admits the conversion from an enum"
"(defn code [x $t] i32 {:where (is-enum $t)} (i32 x))";
accepts "and compares, being is-ordered and is-equal"
@@ -6942,7 +6942,7 @@ let () =
~needle:"$t"
"(defn sum [a $t b $t] $t {:where (is-enum $t)} (+ a b))";
rejects_check "and admits no integer at the call"
- ~needle:"i32 is not is-enum"
+ ~needle:"i32 is not enum (is-enum)"
"(defn code [x $t] i32 {:where (is-enum $t)} (i32 x))\n\
(defn f [] i32 (code (i32 3)))";
rejects_check "nor the conversion to an enum, which needs an integer"
@@ -6952,7 +6952,7 @@ let () =
(* The operand of a cast to a *variable* target is asked the same question
the target was: the target's bound says nothing about a second variable
standing in the argument. *)
- accepts "a cast to a variable target takes a is-numeric operand"
+ accepts "a cast to a variable target takes an is-numeric operand"
"(defn conv [x $u y $t] $t {:where [(is-numeric $t) (is-numeric $u)]} \
(if (< y y) (t x) (t x)))";
rejects_check "a cast to a variable target refuses an is-ordered operand"
@@ -6968,7 +6968,7 @@ let () =
(defn as-k [n $t] K {:where (is-integer $t)} (K n))";
rejects_check "is-numeric does not, because it admits floats"
~needle:"K converts an integer to an enum. The where clause says t is \
- is-numeric, and that does not make it an integer — add \
+ numeric (is-numeric), and that does not make it an integer — add \
(is-integer $t) to the where clause"
"(defenum K [lo -1 hi 1])\n\
(defn as-k [n $t] K {:where (is-numeric $t)} (K n))";
@@ -7000,7 +7000,7 @@ let () =
"(defn same [a $t b $t] bool {:where (is-equal $t)} (= a b)) \
(defn f [] bool (same \"a\" \"b\"))";
rejects_check "is-ordered $t instantiated at string"
- ~needle:"is not is-ordered"
+ ~needle:"is not ordered (is-ordered)"
"(defn less [a $t b $t] bool {:where (is-ordered $t)} (< a b)) \
(defn f [] bool (less \"a\" \"b\"))";
@@ -7255,7 +7255,7 @@ let () =
there is no instantiation of a [is-numeric] variable at which the literal
has no meaning. That is the whole rule, and the four pins below are its
two halves and its one asymmetry. *)
- accepts "an integer literal stands where a is-numeric type variable is wanted"
+ accepts "an integer literal stands where an is-numeric type variable is wanted"
"(defn above-zero? [x $t] bool {:where (is-numeric $t)} (> x 0))";
accepts "and in arithmetic, answering the variable"
"(defn next [x $t] $t {:where (is-numeric $t)} (+ x 1))";
@@ -7292,7 +7292,7 @@ let () =
pass defers to the instantiation — but only under the predicate, which is
what gives the deferred refusal somewhere to land. Without one the type
itself is refused where it is written, at the definition. *)
- rejects_check "a map keyed by a type variable that is not is-hashable"
+ rejects_check "a map keyed by a type variable that is not hashable (is-hashable)"
~needle:"is not a map key"
"(defn f [m (Map $t i32)] i32 {:where (is-numeric $t)} (length m))";
accepts "and is-hashable is what says it is"
@@ -7559,7 +7559,7 @@ let () =
"(defstruct Pair [a $t b $t]) \
(defn f [] f64 (let [p (Pair 1 2.5)] (+ (.a p) (.b p))))";
rejects_check "a callee's predicate names the caller's variable with its $"
- ~needle:"passes the type variable $t, which nothing here declares is-ordered"
+ ~needle:"passes the type variable $t, which nothing here declares ordered (is-ordered)"
"(defn f [s [$t]] () (sort s))";
accepts "a defonce of a generic struct's copy"
"(defstruct Pair [a $t b $t]) (defonce g (Pair i32)) \
@@ -7885,7 +7885,7 @@ let () =
"(defn f [x $t] $t {:where (is-integer $t)} (max-value t))";
rejects_check "max-value at a type that is not a number names the bound"
"(defn f [] str (max-value str))"
- ~needle:"max-value takes a is-numeric type, and str is not one";
+ ~needle:"max-value takes a numeric type (is-numeric), and str is not one";
rejects_check "max-value of a value says it takes a type"
"(defn f [x i32] i32 (max-value x))" ~needle:"max-value takes a type";
accepts "max-of of a slice is the prelude's reduction"
diff --git a/test/test_syntax.ml b/test/test_syntax.ml
index 8da71697..1c35cfa4 100644
--- a/test/test_syntax.ml
+++ b/test/test_syntax.ml
@@ -1414,6 +1414,28 @@ let () =
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" ];
+ (* After review: ?? continues a line, a chain is no place, nested marks
+ and a ? on a value say what to write, and a lowercase type takes ?. *)
+ reads "a line ending in ?? continues" "x = a ??\n 5" "(set x (?? a 5))";
+ reads "a line starting with ?? continues" "x = a\n ?? 5" "(set x (?? a 5))";
+ refuses "a chain is not a place" "q?.x = 5" "indent/chain-assign" "Unwrap it first with if let";
+ refuses "nor under +=" "q?.x += 5" "indent/chain-assign" "cannot be assigned to";
+ refuses "an unwrap is not a place" "x! = 5" "indent/chain-assign" "a value cannot be assigned to";
+ refuses "T?? is not read" "fn f(a: i32??) = a" "indent/nested-option" "Option(i32?)";
+ 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)";
+ refused "option-value.fln" "fn main()\n let y = i32?\n"
+ [ "i32? is an Option type, and a value is wanted here" ];
+ refused "where-fln.fln"
+ "fn big(a: $t, b: $t) -> bool = a < b\n\nfn main()\n println(big(1, 2))\n"
+ [ "nothing declares $t ordered (is-ordered)"; "Write where is-ordered($t)" ];
refused "chain-i32.fln"
"struct P\n x: i32\n\nfn main()\n let p = P{.x 1}\n println(p?.x)\n"
[ "?. has nothing to test. Write . instead" ]
diff --git a/web/index.html b/web/index.html
index f0e73b16..f76e4765 100644
--- a/web/index.html
+++ b/web/index.html
@@ -698,7 +698,7 @@ program was edited elsewhere would not be worth having.
Braces and brackets in expression position write dyn literals:
{:a 1 :b "two"} is a dyn map and [1 2 3] is a dyn vector.
-get, put, has-key?, at and
+get, put, has-key, at and
length read and write them, the same names the typed
Map and Vec answer to. (.hp m) is
(get m :hp) and (at m :hp) is too; set on
@@ -839,7 +839,7 @@ its fields, and omitted fields are zeroed.
Those are the bytes h and i. There is no character type — a
byte is a u8 — but there is a byte literal, so \h is 104 and
-\space is 32, and the prelude's digit? reads as
+\space is 32, and the prelude's is-digit reads as
(and (>= b \0) (<= b \9)). To see a byte as a letter rather than as a
number, print a slice of them: print writes a [u8] as
its bytes.
@@ -1282,7 +1282,7 @@ whichever call site first reaches a type that happens to work. That is deliberat
not Odin's rule, which checks a polymorphic body per instantiation:
+ over the type variable t is refused: a type variable supports only what it is
-declared to support, and nothing here says t is numeric?. Write {:where (is-numeric
+declared to support, and nothing declares t numeric (is-numeric). Write {:where (is-numeric
$t)} at the head of the body, or take the operation as a parameter — a
(Fn [t t] ...) — and call it here
@@ -1294,21 +1294,21 @@ are six predicates, and each gates builtins the compiler already has:
They entail each other in one direction, so one clause usually does:
-integer? gives numeric?, numeric? gives
-ordered?, and ordered? gives equal?;
-enum? gives ordered? too. A
-sort that compares its elements declares ordered? and
-nothing else, and the prelude's abs declares integer?
+is-integer gives is-numeric, is-numeric gives
+is-ordered, and is-ordered gives is-equal;
+is-enum gives is-ordered too. A
+sort that compares its elements declares is-ordered and
+nothing else, and the prelude's abs declares is-integer
alone — the bound is what keeps its integer body away from the floats, whose
abs-f32/abs-f64 are libm's sign-bit clear.
@@ -1330,7 +1330,7 @@ the predicate admits
their legality is only decidable after substituting: println over a
variable, which selects the structural printer per copy, and the Map
operations over a variable key, whose hash and equality are concrete symbols chosen from
-the concrete key type. The Map half is what hashable? buys —
+the concrete key type. The Map half is what is-hashable buys —
without the clause, the type (Map $t i32) is refused where it is
written, and with it the refusal moves to the call site that names an
unhashable key.
@@ -1416,17 +1416,17 @@ over.
| Group | Names |
| slice algorithms, over one type variable | swap, reverse, sort, sort-by, index-of, min-of, max-of, map-in-place, reduce, filter |
| the per-type layer that stays | sum-i32, sum-f32 — the element and the accumulator are different types, which one variable cannot say |
-| bytes | bytes=?, bytes<?, bytes-ci=?, starts-with?, ends-with?, index-of-bytes, trim, digit?, space?, sort-bytes |
+| bytes | is-bytes-equal, is-bytes-less, is-bytes-ci-equal, has-prefix, has-suffix, index-of-bytes, trim, is-digit, is-space, sort-bytes |
| parsing | parse-i64, parse-f64 |
| text | split-on-byte, split-next, split, lower-ascii, upper-ascii, to-lower, to-upper |
| building bytes | append, append-i64, append-f64, concat, join, repeat-bytes, replace-bytes, slices-new, format-f64 |
-| UTF-8 | decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune |
+| UTF-8 | decode-rune, rune-at, rune-count, rune-size, is-rune-start, is-valid-utf8, encode-rune |
| numbers | sign-f32, lerp, clamp, floor-f32, ceil-f32, round-f32, abs (generic over every integer width), the constants pi-f32, pi-f64, tau-f32, tau-f64, and libm through a declare at both widths: sqrt, abs, floor, ceil, round, fmod, sin, cos, tan, asin, acos, atan, atan2, log, log2, log10, exp, pow, hypot, cbrt — each spelled -f32 or -f64 |
| time | monotonic-ns, monotonic-seconds, unix-ns, unix-seconds, sleep-ns, sleep-seconds, and ns-per-second and its two smaller siblings |
-| files | file-exists? and file-size, which answer a value; slurp, barf, delete-file, rename-file and make-directory, which signal FileError under retry and use-value |
+| files | file-exists and file-size, which answer a value; slurp, barf, delete-file, rename-file and make-directory, which signal FileError under retry and use-value |
| the operating system | getenv, which answers an (Option [u8]) viewing the process environment |
| random | rand-seed, rand-int (a u64), rand (an f64 in [0, 1)), rand-bool, rand-int-range (an i64), rand-float-range. A seeded PRNG written in Flan, one draw per call, so the same seed gives the same numbers on every target. Reproducible and predictable are the same property: a holder of one result can work back to the state and know the rest. Right for a grid or a shuffle, wrong for a key |
-| forms, for macros | form-nil, form-cons, form-append, form-rest, form-items, form-pair, form-sym?, form-is-sym?, gensym, and unless and into, which are macros written here rather than special forms |
+| forms, for macros | form-nil, form-cons, form-append, form-rest, form-items, form-pair, is-form-named, is-form-sym, gensym, and unless and into, which are macros written here rather than special forms |
| the rest | pause, which signals the Pause condition the break loop stops on, and embed-find |
@@ -1477,7 +1477,7 @@ integer-exact in nanoseconds for a hundred days of process life, which is why th
monotonic origin is the first read and not boot.
The file surface is split by whether a handler could do anything.
-file-exists? and file-size answer a bool and an
+file-exists and file-size answer a bool and an
(Option i64): absence is the reply, not a fault, and a condition would make
the ordinary case pay for a handler search. slurp, barf,
delete-file, rename-file and make-directory signal
@@ -1908,8 +1908,8 @@ Two directives:
# behind three innocuous Flan names.
exclude Mem*
-# The kebab rule gives is-window-ready. Lisp spells a predicate with a ?.
-name IsWindowReady window-ready?
+# A Flan face the kebab rule would not give.
+name GetFPS fps
An excluded function still says it was excluded rather than going quiet, and a name
override changes only the Flan face — the C symbol is kept verbatim in the declaration,