?? 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.

This commit is contained in:
Joseph Ferano 2026-09-26 15:36:49 +07:00
parent c9f33feb6c
commit 6c6b5a60cb
11 changed files with 251 additions and 99 deletions

View File

@ -19,6 +19,12 @@ prelude and vendor rewritten in .fln; tests and examples converted with =flan co
then =flan convert=, the .flan source path and =flan-mode= removed. Nothing tracks what 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 .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=. 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 ** WAIT A cheap front for dyn sequences
Decided 2026-09-26 (128) to pause: dyn vectors are mutable, so taking from the front 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 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 ** DONE defclass is a named dyn map with a shape tag
CLOSED: [2026-09-20] CLOSED: [2026-09-20]
An instance is a dyn map with its class in the object header, so =get=, =put=, 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 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 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 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 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 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 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. 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 ** DONE A conversion is legal at a bounded variable when it is legal at every type the bound admits
CLOSED: [2026-09-21] CLOSED: [2026-09-21]
A machine-type target needs =numeric?=; an enum target needs =integer?=; A machine-type target needs =is-numeric=; an enum target needs =is-integer=;
=ordered?=, =equal?= and =hashable?= admit nothing. A predicate gates an operation =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 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 ** DONE The Ptr and union arms of the fill boundary are relaxable
CLOSED: [2026-09-25] 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 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 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, 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?=, and it is not what Odin does. The five predicates are =is-ordered=, =is-equal=,
=hashable?=, =numeric?= and =integer?=. =is-hashable=, =is-numeric= and =is-integer=.
** DONE A type variable takes a $ sigil ** DONE A type variable takes a $ sigil
CLOSED: [2026-09-13] 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 ** DONE Milestone 5 was mostly already there
CLOSED: [2026-09-20] CLOSED: [2026-09-20]
What the lane added was a written integer zero standing where a =numeric?=-bounded What the lane added was a written integer zero standing where a variable bounded by
variable stands — legal because every type =numeric?= admits is an integer or a =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 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 =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] 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 instantiation, joining =print= and =println=. The membership rule is not a
headcount: either the operation cannot fail after substituting, or a declared 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 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 ** DONE plan.org's Types section lists a predicate that no longer exists
CLOSED: [2026-09-25] 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=. =copyable?=, and the section no longer names =(Handle $t)= or =pool-new=.
** DONE plan.org's Data model section still describes move-only containers ** DONE plan.org's Data model section still describes move-only containers

View File

@ -3040,6 +3040,19 @@ let rec reaches_dyn (t : Types.t) =
which is the whole reason predicates exist — under the no-constraint rule 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 [(sort xs)] had to become [(sort-by xs (fn [a b] (< a b)))] at every call
site in the corpus. *) 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) = let unconstrained env loc op ~needs (t : Types.t) =
if generic_ty t then if generic_ty t then
match tyvar_of t with 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 Loc.failk "check/unconstrained-type-variable" loc
"%s over the type variable %s: nothing declares %s %s. Write \ "%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" a parameter, a (Fn [%s %s] ...), and call it here"
op (tyname loc t) (tyname loc t) needs needs op (tyname loc t) (tyname loc t) (pred_word needs)
(tyname loc t) (tyname loc t) (tyname loc t) (where_text loc needs (tyname loc t))
(tyname loc t) (tyname loc t)
(* ── The runaway instantiation, refused by name rather than by depth ──── (* ── 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" (rt loc Types.Unit "flan_dev_reg_note_res_done"
[ mk loc Types.String (Tast.Str owner) ]) [ mk loc Types.String (Tast.Str owner) ])
| _ -> fail loc "internal: %%res-done takes a name — a compiler bug") | _ -> 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 | Ast.Var name -> named_call ctx ~want loc name args
(* ((Ptr Color) p): a pointer cast, spelled the way (i32 x) is — the type (* ((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 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 -> | ps ->
Printf.sprintf Printf.sprintf
"The where clause says %s is %s, and that does not make it %s" v "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 in
let fix = let fix =
let alt clause = let alt clause =
@ -11423,8 +11444,11 @@ and cast_operand ctx loc name ~needs ?also ~what ~is v =
| None -> "" | None -> ""
in in
if ctx.env.tvpreds = [] then if ctx.env.tvpreds = [] then
Printf.sprintf "write {:where (%s $%s)} at the head of the body%s" Printf.sprintf "write %s at the head of the body%s"
needs v (alt "{:where (%s $%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 else
Printf.sprintf "add (%s $%s) to the where clause%s" needs v Printf.sprintf "add (%s $%s) to the where clause%s" needs v
(alt "(%s $%s)") (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, (* = 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. 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 = and string_compare ctx ~want loc name p args =
(match name with (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 int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
| _ -> | _ ->
fail a.Ast.loc 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 (tyname loc ty) name
in in
expect ctx loc ~want v 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 match List.assoc_opt p.Ast.pvar !subst with
| Some (Types.Var v) when not (declares ctx.env.tvpreds v p.Ast.pname) -> | Some (Types.Var v) when not (declares ctx.env.tvpreds v p.Ast.pname) ->
Loc.failk "check/predicate-not-carried" loc 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 \ type variable $%s, which nothing here declares %s. Add \
{:where (%s $%s)} to this function's own clause" %s to this function's own clause"
name p.Ast.pname p.Ast.pvar v p.Ast.pname p.Ast.pname v 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) -> | Some t when not (open_ty t) && not (pred_holds p.Ast.pname t) ->
Loc.failk "check/predicate-unsatisfied" loc 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" 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); gfn.Ast.fwhere);
expect ctx loc ~want (mk loc cret (Tast.Call (name, targs))) 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 if not (pred_holds p.Ast.pname t) then
Loc.failk "check/predicate-unsatisfied" loc Loc.failk "check/predicate-unsatisfied" loc
"this call instantiates %s at $%s = %s, and %s is not %s. %s is \ "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) 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; fn.Ast.fwhere;
if Hashtbl.mem env.fns sym then if Hashtbl.mem env.fns sym then
fail loc fail loc

View File

@ -479,7 +479,8 @@ and list f h args =
((if t <> "" && t.[0] = '~' then "~(" ^ t ^ ")" else "~" ^ t), 13) ((if t <> "" && t.[0] = '~' then "~(" ^ t ^ ")" else "~" ^ t), 13)
| Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 13 x, 13) | Form.Sym "unquote-splicing", [ x ] -> ("~@" ^ at 13 x, 13)
| Form.Sym s, _ :: _ :: _ | 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 op = infix_op s in
let lvl = Option.get (R.binop_level op) in let lvl = Option.get (R.binop_level op) in
let first = List.hd args and rest = List.tl args in let first = List.hd args and rest = List.tl args in

View File

@ -56,7 +56,11 @@ let binops =
[ ("or", 1); ("and", 2); [ ("or", 1); ("and", 2);
("==", 4); ("!=", 4); ("<", 4); ("<=", 4); (">", 4); (">=", 4); ("==", 4); ("!=", 4); ("<", 4); ("<=", 4); (">", 4); (">=", 4);
("||", 5); ("^^", 6); ("&&", 7); ("||", 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 binop_level s = List.assoc_opt s binops
let is_binop s = binop_level s <> None 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]. *) (* The reader's fresh names for an optional chain's payload, per [read_all]. *)
let opt_n = ref 0 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 (* A [-] glued to one of these starts a negation: [-x] is [(- x)]. Anything
else keeps the Lisp reading, so [--], [->] and [-=] stay names. *) else keeps the Lisp reading, so [--], [->] and [-=] stay names. *)
let is_neg_char c = let is_neg_char c =
@ -215,15 +223,20 @@ let question_fix name =
in in
pkg ^ fixed 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 drop c s = String.concat "" (String.split_on_char c s) in
let n = String.length whole in let n = String.length whole in
if n > 1 && whole.[n - 1] = '?' && not (String.contains (String.sub whole 0 (n - 1)) '?') if n > 1 && whole.[n - 1] = '?' && not (String.contains (String.sub whole 0 (n - 1)) '?')
&& not (String.contains whole '!') then && not (String.contains whole '!') then
let base = String.sub whole 0 (n - 1) in
Loc.failk "indent/question-name" loc Loc.failk "indent/question-name" loc
"%s is not a name: a name cannot contain ?.\n\n\ "%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" A name for a yes-or-no question starts with is- or has- instead: %s%s"
whole (question_fix (String.sub whole 0 (n - 1))) 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 else
let c = if String.contains whole '!' then '!' else '?' in let c = if String.contains whole '!' then '!' else '?' in
Loc.failk "indent/mark-in-name" loc 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) | Some d -> String.sub pre (d + 1) (String.length pre - d - 1)
| None -> pre | None -> pre
in 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 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 if rest <> "" && rest.[0] = '?' && pre <> "" then
failk "unspaced-operator" (piece line at 2) failk "unspaced-operator" (piece line at 2)
"?? is an operator here, and a binary operator has a space on \ "?? is an operator here, and a binary operator has a space on \
each side: %s ?? %s" pre each side: %s ?? %s" pre (after_word (tail rest));
(let r = String.sub rest 1 (String.length rest - 1) in (* A [?] at the end of a name is a type's, [T?], or one the
if r = "" then "d" else r); 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 ends = rest = "" && next <> '(' in
let chain = (rest <> "" && rest.[0] = '.') || (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; some_part ~after col pre;
emit QUEST (piece line at 1); emit QUEST (piece line at 1);
marks ~after:true (at + 1) rest 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 if rest <> "" && rest.[0] = '=' then
failk "unspaced-operator" (piece line at 2) failk "unspaced-operator" (piece line at 2)
"!= is an operator here, and a binary operator has a space on \ "!= 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 if (rest = "" && next <> '(') || (rest <> "" && rest.[0] = '.') then begin
some_part ~after col pre; some_part ~after col pre;
emit BANG (piece line at 1); emit BANG (piece line at 1);
@ -351,7 +385,7 @@ let lex ?(line = 1) ?(col = 1) ~file src : token list =
end end
in in
let wordy = String.exists (fun c -> Reader.is_digit c || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) body 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) if colon then emit COLON (piece line (col + n - 1) 1)
end end
in in
@ -817,7 +851,19 @@ let negative_literal = function
(* Something followed a complete value where nothing may. The two shapes that (* 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 get their own sentence are the ones a Lisp hand writes: [a -1] and
[f (x)]. *) [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 = let stray p ~after =
mark_after p;
let t = peek p in let t = peek p in
match t.tok with match t.tok with
| ATOM _ when t.sp && negative_literal t.tok -> | ATOM _ when t.sp && negative_literal t.tok ->
@ -863,13 +909,14 @@ let expect p tok ~what =
let t = peek p in let t = peek p in
if t.tok = tok then ignore (advance p) if t.tok = tok then ignore (advance p)
else else
let () = mark_after p in
failk "expected" (where_ p) "expected %s here, and found %s" what failk "expected" (where_ p) "expected %s here, and found %s" what
(show t.tok) (show t.tok)
let expect_name p s ~what = let expect_name p s ~what =
match (peek p).tok with match (peek p).tok with
| NAME n when n = s -> ignore (advance p) | 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 (* 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 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 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 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 = let unclosed p c l0 =
failk "unclosed" l0 failk "unclosed" l0
~notes:[ Loc.note (where_ p) "the input ends here, still inside it" ] ~notes:[ Loc.note (where_ p) "the input ends here, still inside it" ]
@ -1171,8 +1235,34 @@ and postfix p =
[ sym t.loc "?."; [ sym t.loc "?.";
Form.make (Form.Vec [ sym t.loc h; f ]) f.loc; Form.make (Form.Vec [ sym t.loc h; f ]) f.loc;
rest ]), 12) 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 -> | 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); ignore (advance p);
loop (mk p l0 (Form.List [ sym l0 "Option"; f ]), 12) loop (mk p l0 (Form.List [ sym l0 "Option"; f ]), 12)
| BANG -> | BANG ->
@ -1393,10 +1483,12 @@ and inline_stmt p : Form.t =
let e, _ = expr p in let e, _ = expr p in
match (peek p).tok with match (peek p).tok with
| NAME "=" -> | NAME "=" ->
no_place e;
let eq = advance p in let eq = advance p in
let v, _ = expr p in let v, _ = expr p in
mk p t.loc (Form.List [ sym eq.loc "set"; e; v ]) mk p t.loc (Form.List [ sym eq.loc "set"; e; v ])
| NAME op when List.mem_assoc op assign_ops -> | NAME op when List.mem_assoc op assign_ops ->
no_place e;
let eq = advance p in let eq = advance p in
let v, _ = expr 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)) 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)]. *) type, [Fn(A, B) -> R], which reads as [(Fn [A B] R)]. *)
let rec ty p : Form.t = let rec ty p : Form.t =
let l0 = (peek p).loc in 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 match f.v, (peek p).tok with
| Form.List (({ v = Form.Sym ("Fn" | "CFn"); _ } as h) :: args), NAME "->" | Form.List (({ v = Form.Sym ("Fn" | "CFn"); _ } as h) :: args), NAME "->"
when (last p).tok = RP -> when (last p).tok = RP ->
@ -2195,10 +2289,12 @@ and expr_stmt (s : st) : Form.t =
let e, _ = expr p in let e, _ = expr p in
match (peek p).tok with match (peek p).tok with
| NAME "=" -> | NAME "=" ->
no_place e;
let eq = advance p in let eq = advance p in
let v = value_line s ~after:(text_of e ^ " =") in let v = value_line s ~after:(text_of e ^ " =") in
mk p t0.loc (Form.List [ sym eq.loc "set"; e; v ]) mk p t0.loc (Form.List [ sym eq.loc "set"; e; v ])
| NAME op when List.mem_assoc op assign_ops -> | NAME op when List.mem_assoc op assign_ops ->
no_place e;
let eq = advance p in let eq = advance p in
let v = value_line s ~after:(text_of e ^ " " ^ op) in let v = value_line s ~after:(text_of e ^ " " ^ op) in
let o = List.assoc op assign_ops in let o = List.assoc op assign_ops in

View File

@ -1688,7 +1688,7 @@ let source = {flan|
;; different parts of the input, and a caller can see which one came first. ;; 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 ;; 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 ;; 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. ;; 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 ;; So this is the shape a generic takes when the operation it needs is not a

View File

@ -37,7 +37,7 @@
(+ x x)) (+ x x))
;; is-equal admits = and !=; is-ordered admits < <= > >= min max, and entails ;; is-equal admits = and !=; is-ordered admits < <= > >= min max, and entails
;; equal?. ;; is-equal.
(defn count-of [s [$t] x $t] i32 (defn count-of [s [$t] x $t] i32
{:where (is-equal $t)} {:where (is-equal $t)}
(let [n 0] (let [n 0]
@ -64,7 +64,7 @@
;; re-checks (> x 0) with $t substituted, and *that* is where the literal is ;; re-checks (> x 0) with $t substituted, and *that* is where the literal is
;; built at the concrete width and range-checked. ;; 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 ;; 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. ;; 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)) (defn is-pos-t [x $t] bool {:where (is-numeric $t)} (> x 0))

View File

@ -111,7 +111,7 @@
(println (abs-f64 -1.5)) (println (abs-f64 -1.5))
(println (abs-f32 -2.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 255 3))
(println (low-bits (u16 65535) (u16 4))) (println (low-bits (u16 65535) (u16 4)))
(println (low-bits (i64 1023) (i64 5))) (println (low-bits (i64 1023) (i64 5)))

View File

@ -3812,13 +3812,13 @@ let () =
onto whichever of the two buffers the branch chose. onto whichever of the two buffers the branch chose.
The six [true]s and the five numbers after the first [8] are the 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?/ literal-at-a-type-variable family: three written bodies — is-pos-t, is-neg-t,
zero-p?, next-after and plus-300 — reaching i8, u8, u16, i32, i64, f32 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 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 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 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 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 names — and the middle one is is-zero at -0.0, which IEEE says is zero
and which this does not second-guess. *) and which this does not second-guess. *)
let generics_out = let generics_out =
@ -3849,7 +3849,7 @@ let () =
lines are the collapsed abs at six widths and both signed minimums lines are the collapsed abs at six widths and both signed minimums
(which answer themselves; the negation wraps). The [0 0] after them is (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 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 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 $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 the wider type (TODO.org, "abs is one generic, and a bound joins to the
@ -3954,7 +3954,7 @@ let () =
and cannot change. *) and cannot change. *)
refuses "a package generic's bound, refused at the call" refuses "a package generic's bound, refused at the call"
"programs/pkg-generic-reject.flan" "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" refuses "and the refusal quotes the clause the package wrote"
"programs/pkg-generic-reject.flan" "{:where (is-ordered $t)}"; "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. *) chain of instantiations and not a depth it gave up at. *)
refuses "an unconstrained operator in a generic body" refuses "an unconstrained operator in a generic body"
"programs/generic-reject.flan" "programs/generic-reject.flan"
"nothing declares $t is-numeric"; "nothing declares $t numeric (is-numeric)";
refuses "an unconstrained operator names the way out" refuses "an unconstrained operator names the way out"
"programs/generic-reject.flan" "{:where (is-numeric $t)}"; "programs/generic-reject.flan" "{:where (is-numeric $t)}";
refuses "a runaway instantiation" "programs/generic-runaway.flan" 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 requirement the author wrote down. What is asserted is that it names
the type passed and the predicate it failed, and not the body. *) 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" 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" refuses "and it names the type the call site asked for"
"programs/generic-map-reject.flan" "at $t = f64"; "programs/generic-map-reject.flan" "at $t = f64";

View File

@ -1553,7 +1553,7 @@ let () =
accepts "all-distinct over a type variable" accepts "all-distinct over a type variable"
"(defn three [a $t b $t c $t] bool {:where (is-equal $t)} (!= a b c))"; "(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" 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))"; "(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 (* One operand and none. Both would have to be [true] whatever they were
handed, which is a typo carrying a value. *) handed, which is a typo carrying a value. *)
@ -6818,7 +6818,7 @@ let () =
accepts "is-numeric admits +" accepts "is-numeric admits +"
"(defn add [a $t b $t] $t {:where (is-numeric $t)} (+ a b))"; "(defn add [a $t b $t] $t {:where (is-numeric $t)} (+ a b))";
rejects_check "is-equal does not admit <" 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))"; "(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 (* 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, 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" accepts "is-integer admits the shifts"
"(defn dbl [x $t] $t {:where (is-integer $t)} (<< x 1))"; "(defn dbl [x $t] $t {:where (is-integer $t)} (<< x 1))";
rejects_check "is-numeric does not admit bit-and" 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))"; "(defn is-low [x $t] bool {:where (is-numeric $t)} (= (bit-and x 1) 1))";
rejects_check "nor the shifts" 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))"; "(defn dbl [x $t] $t {:where (is-numeric $t)} (<< x 1))";
(* An integer?-bounded caller satisfies a numeric?-bounded callee: the (* A caller bounded by is-integer satisfies a callee bounded by is-numeric: the
entailment carries across generic calls exactly as ordered?-over-equal? entailment carries across generic calls exactly as is-ordered-over-equal?
does. *) 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 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)))"; (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 (* 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 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 type the variable can become, and is-integer's types are a subset of
is-numeric's. *) 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))"; "(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. *) (* 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" rejects_check "a float does not instantiate an is-integer-bounded variable"
~needle:"f64 is not is-integer" ~needle:"f64 is not integer (is-integer)"
"(defn bump [x $t] $t {:where (is-integer $t)} (+ x 1))\n\ "(defn bump [x $t] $t {:where (is-integer $t)} (+ x 1))\n\
(defn main [] () (println (bump 1.5)))"; (defn main [] () (println (bump 1.5)))";
(* And dyn is refused by the bound too — the clause's own refusal, the more (* And dyn is refused by the bound too — the clause's own refusal, the more
specific of the two answers, exactly as at numeric?. *) specific of the two answers, exactly as at is-numeric. *)
rejects_check "dyn does not instantiate an integer?-bounded variable" rejects_check "dyn does not instantiate an is-integer-bounded variable"
~needle:"dyn is not is-integer" ~needle:"dyn is not integer (is-integer)"
"(defn bump [x $t] $t {:where (is-integer $t)} (+ x 1))\n\ "(defn bump [x $t] $t {:where (is-integer $t)} (+ x 1))\n\
(defonce d dyn 5)\n\ (defonce d dyn 5)\n\
(defn main [] () (println (bump d)))"; (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 definition, in the bound's own words: there is no instantiation at which
it means anything. *) it means anything. *)
rejects_check "a float literal has no meaning under is-integer" 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 — what is a number, so a conversion under one of them alone is refused —
and the message says which predicate to write. *) and the message says which predicate to write. *)
rejects_check "is-ordered does not admit a conversion" 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 \ a number or an enum — add (is-numeric $t) to the where clause, or \
(is-enum $t) for an enum" (is-enum $t) for an enum"
"(defn to32 [x $t] i32 {:where (is-ordered $t)} (i32 x))"; "(defn to32 [x $t] i32 {:where (is-ordered $t)} (i32 x))";
@ -6933,7 +6933,7 @@ let () =
"(defn to32 [x $t] i32 (i32 x))"; "(defn to32 [x $t] i32 (i32 x))";
(* is-enum is the other bound a conversion to a number takes: it admits the (* 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 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" accepts "is-enum admits the conversion from an enum"
"(defn code [x $t] i32 {:where (is-enum $t)} (i32 x))"; "(defn code [x $t] i32 {:where (is-enum $t)} (i32 x))";
accepts "and compares, being is-ordered and is-equal" accepts "and compares, being is-ordered and is-equal"
@ -6942,7 +6942,7 @@ let () =
~needle:"$t" ~needle:"$t"
"(defn sum [a $t b $t] $t {:where (is-enum $t)} (+ a b))"; "(defn sum [a $t b $t] $t {:where (is-enum $t)} (+ a b))";
rejects_check "and admits no integer at the call" 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 code [x $t] i32 {:where (is-enum $t)} (i32 x))\n\
(defn f [] i32 (code (i32 3)))"; (defn f [] i32 (code (i32 3)))";
rejects_check "nor the conversion to an enum, which needs an integer" 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 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 the target was: the target's bound says nothing about a second variable
standing in the argument. *) 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)]} \ "(defn conv [x $u y $t] $t {:where [(is-numeric $t) (is-numeric $u)]} \
(if (< y y) (t x) (t x)))"; (if (< y y) (t x) (t x)))";
rejects_check "a cast to a variable target refuses an is-ordered operand" 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))"; (defn as-k [n $t] K {:where (is-integer $t)} (K n))";
rejects_check "is-numeric does not, because it admits floats" rejects_check "is-numeric does not, because it admits floats"
~needle:"K converts an integer to an enum. The where clause says t is \ ~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" (is-integer $t) to the where clause"
"(defenum K [lo -1 hi 1])\n\ "(defenum K [lo -1 hi 1])\n\
(defn as-k [n $t] K {:where (is-numeric $t)} (K 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 same [a $t b $t] bool {:where (is-equal $t)} (= a b)) \
(defn f [] bool (same \"a\" \"b\"))"; (defn f [] bool (same \"a\" \"b\"))";
rejects_check "is-ordered $t instantiated at string" 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 less [a $t b $t] bool {:where (is-ordered $t)} (< a b)) \
(defn f [] bool (less \"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 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 has no meaning. That is the whole rule, and the four pins below are its
two halves and its one asymmetry. *) 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))"; "(defn above-zero? [x $t] bool {:where (is-numeric $t)} (> x 0))";
accepts "and in arithmetic, answering the variable" accepts "and in arithmetic, answering the variable"
"(defn next [x $t] $t {:where (is-numeric $t)} (+ x 1))"; "(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 pass defers to the instantiation — but only under the predicate, which is
what gives the deferred refusal somewhere to land. Without one the type what gives the deferred refusal somewhere to land. Without one the type
itself is refused where it is written, at the definition. *) 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" ~needle:"is not a map key"
"(defn f [m (Map $t i32)] i32 {:where (is-numeric $t)} (length m))"; "(defn f [m (Map $t i32)] i32 {:where (is-numeric $t)} (length m))";
accepts "and is-hashable is what says it is" accepts "and is-hashable is what says it is"
@ -7559,7 +7559,7 @@ let () =
"(defstruct Pair [a $t b $t]) \ "(defstruct Pair [a $t b $t]) \
(defn f [] f64 (let [p (Pair 1 2.5)] (+ (.a p) (.b p))))"; (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 $" 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))"; "(defn f [s [$t]] () (sort s))";
accepts "a defonce of a generic struct's copy" accepts "a defonce of a generic struct's copy"
"(defstruct Pair [a $t b $t]) (defonce g (Pair i32)) \ "(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))"; "(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" rejects_check "max-value at a type that is not a number names the bound"
"(defn f [] str (max-value str))" "(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" 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"; "(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" accepts "max-of of a slice is the prelude's reduction"

View File

@ -1414,6 +1414,28 @@ let () =
refused "coalesce-i32.fln" refused "coalesce-i32.fln"
"fn main()\n let x = 5\n println(x ?? 1)\n" "fn main()\n let x = 5\n println(x ?? 1)\n"
[ "the left side of ?? is i32, which always holds a value" ]; [ "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" refused "chain-i32.fln"
"struct P\n x: i32\n\nfn main()\n let p = P{.x 1}\n println(p?.x)\n" "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" ] [ "?. has nothing to test. Write . instead" ]

View File

@ -698,7 +698,7 @@ program was edited elsewhere would not be worth having.</p>
<p>Braces and brackets in expression position write dyn literals: <p>Braces and brackets in expression position write dyn literals:
<code>{:a 1 :b "two"}</code> is a dyn map and <code>[1 2 3]</code> is a dyn vector. <code>{:a 1 :b "two"}</code> is a dyn map and <code>[1 2 3]</code> is a dyn vector.
<code>get</code>, <code>put</code>, <code>has-key?</code>, <code>at</code> and <code>get</code>, <code>put</code>, <code>has-key</code>, <code>at</code> and
<code>length</code> read and write them, the same names the typed <code>length</code> read and write them, the same names the typed
<code>Map</code> and <code>Vec</code> answer to. <code>(.hp m)</code> is <code>Map</code> and <code>Vec</code> answer to. <code>(.hp m)</code> is
<code>(get m :hp)</code> and <code>(at m :hp)</code> is too; <code>set</code> on <code>(get m :hp)</code> and <code>(at m :hp)</code> is too; <code>set</code> on
@ -839,7 +839,7 @@ its fields, and omitted fields are zeroed.</p>
<p>Those are the bytes <code>h</code> and <code>i</code>. There is no character type — a <p>Those are the bytes <code>h</code> and <code>i</code>. There is no character type — a
byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is 104 and byte is a <code>u8</code> — but there is a byte literal, so <code>\h</code> is 104 and
<code>\space</code> is 32, and the prelude's <code>digit?</code> reads as <code>\space</code> is 32, and the prelude's <code>is-digit</code> reads as
<code>(and (&gt;= b \0) (&lt;= b \9))</code>. To see a byte as a letter rather than as a <code>(and (&gt;= b \0) (&lt;= b \9))</code>. To see a byte as a letter rather than as a
number, print a slice of them: <code>print</code> writes a <code>[u8]</code> as number, print a slice of them: <code>print</code> writes a <code>[u8]</code> as
its bytes.</p> its bytes.</p>
@ -1282,7 +1282,7 @@ whichever call site first reaches a type that happens to work. That is deliberat
<em>not</em> Odin's rule, which checks a polymorphic body per instantiation:</p> <em>not</em> Odin's rule, which checks a polymorphic body per instantiation:</p>
<pre><code class="sh">+ over the type variable t is refused: a type variable supports only what it is <pre><code class="sh">+ 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 $t)} at the head of the body, or take the operation as a parameter — a
(Fn [t t] ...) — and call it here</code></pre> (Fn [t t] ...) — and call it here</code></pre>
@ -1294,21 +1294,21 @@ are six predicates, and each gates builtins the compiler already has:</p>
<div class="scroll"> <div class="scroll">
<table> <table>
<tr><th>Predicate</th><th>What it admits</th></tr> <tr><th>Predicate</th><th>What it admits</th></tr>
<tr><td><code>integer?</code></td><td><code>bit-and</code> <code>bit-or</code> <code>bit-xor</code> <code>&lt;&lt;</code> <code>&gt;&gt;</code> — every integer type, no float</td></tr> <tr><td><code>is-integer</code></td><td><code>bit-and</code> <code>bit-or</code> <code>bit-xor</code> <code>&lt;&lt;</code> <code>&gt;&gt;</code> — every integer type, no float</td></tr>
<tr><td><code>numeric?</code></td><td><code>+</code> <code>-</code> <code>*</code> <code>/</code> <code>%</code>, and a cast <code>(t x)</code></td></tr> <tr><td><code>is-numeric</code></td><td><code>+</code> <code>-</code> <code>*</code> <code>/</code> <code>%</code>, and a cast <code>(t x)</code></td></tr>
<tr><td><code>enum?</code></td><td>a cast to a number, <code>(i32 x)</code> — every enum type</td></tr> <tr><td><code>is-enum</code></td><td>a cast to a number, <code>(i32 x)</code> — every enum type</td></tr>
<tr><td><code>ordered?</code></td><td><code>&lt;</code> <code>&lt;=</code> <code>&gt;</code> <code>&gt;=</code> <code>min</code> <code>max</code></td></tr> <tr><td><code>is-ordered</code></td><td><code>&lt;</code> <code>&lt;=</code> <code>&gt;</code> <code>&gt;=</code> <code>min</code> <code>max</code></td></tr>
<tr><td><code>equal?</code></td><td><code>=</code> and <code>!=</code></td></tr> <tr><td><code>is-equal</code></td><td><code>=</code> and <code>!=</code></td></tr>
<tr><td><code>hashable?</code></td><td>the variable as a <code>Map</code> key — <code>(map-new t V)</code>, <code>get</code>, <code>put</code>, <code>has-key?</code></td></tr> <tr><td><code>is-hashable</code></td><td>the variable as a <code>Map</code> key — <code>(map-new t V)</code>, <code>get</code>, <code>put</code>, <code>has-key</code></td></tr>
</table> </table>
</div> </div>
<p>They entail each other in one direction, so one clause usually does: <p>They entail each other in one direction, so one clause usually does:
<code>integer?</code> gives <code>numeric?</code>, <code>numeric?</code> gives <code>is-integer</code> gives <code>is-numeric</code>, <code>is-numeric</code> gives
<code>ordered?</code>, and <code>ordered?</code> gives <code>equal?</code>; <code>is-ordered</code>, and <code>is-ordered</code> gives <code>is-equal</code>;
<code>enum?</code> gives <code>ordered?</code> too. A <code>is-enum</code> gives <code>is-ordered</code> too. A
<code>sort</code> that compares its elements declares <code>ordered?</code> and <code>sort</code> that compares its elements declares <code>is-ordered</code> and
nothing else, and the prelude's <code>abs</code> declares <code>integer?</code> nothing else, and the prelude's <code>abs</code> declares <code>is-integer</code>
alone — the bound is what keeps its integer body away from the floats, whose alone — the bound is what keeps its integer body away from the floats, whose
<code>abs-f32</code>/<code>abs-f64</code> are libm's sign-bit clear.</p> <code>abs-f32</code>/<code>abs-f64</code> are libm's sign-bit clear.</p>
@ -1330,7 +1330,7 @@ the predicate admits</code></pre>
their legality is only decidable after substituting: <code>println</code> over a their legality is only decidable after substituting: <code>println</code> over a
variable, which selects the structural printer per copy, and the <code>Map</code> variable, which selects the structural printer per copy, and the <code>Map</code>
operations over a variable key, whose hash and equality are concrete symbols chosen from operations over a variable key, whose hash and equality are concrete symbols chosen from
the concrete key type. The <code>Map</code> half is what <code>hashable?</code> buys — the concrete key type. The <code>Map</code> half is what <code>is-hashable</code> buys —
without the clause, the type <code>(Map $t i32)</code> is refused where it is without the clause, the type <code>(Map $t i32)</code> is refused where it is
<em>written</em>, and with it the refusal moves to the call site that names an <em>written</em>, and with it the refusal moves to the call site that names an
unhashable key.</p> unhashable key.</p>
@ -1416,17 +1416,17 @@ over.</p>
<tr><th>Group</th><th>Names</th></tr> <tr><th>Group</th><th>Names</th></tr>
<tr><td>slice algorithms, over one type variable</td><td><code>swap</code>, <code>reverse</code>, <code>sort</code>, <code>sort-by</code>, <code>index-of</code>, <code>min-of</code>, <code>max-of</code>, <code>map-in-place</code>, <code>reduce</code>, <code>filter</code></td></tr> <tr><td>slice algorithms, over one type variable</td><td><code>swap</code>, <code>reverse</code>, <code>sort</code>, <code>sort-by</code>, <code>index-of</code>, <code>min-of</code>, <code>max-of</code>, <code>map-in-place</code>, <code>reduce</code>, <code>filter</code></td></tr>
<tr><td>the per-type layer that stays</td><td><code>sum-i32</code>, <code>sum-f32</code> — the element and the accumulator are different types, which one variable cannot say</td></tr> <tr><td>the per-type layer that stays</td><td><code>sum-i32</code>, <code>sum-f32</code> — the element and the accumulator are different types, which one variable cannot say</td></tr>
<tr><td>bytes</td><td><code>bytes=?</code>, <code>bytes&lt;?</code>, <code>bytes-ci=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code>, <code>sort-bytes</code></td></tr> <tr><td>bytes</td><td><code>is-bytes-equal</code>, <code>is-bytes-less</code>, <code>is-bytes-ci-equal</code>, <code>has-prefix</code>, <code>has-suffix</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>is-digit</code>, <code>is-space</code>, <code>sort-bytes</code></td></tr>
<tr><td>parsing</td><td><code>parse-i64</code>, <code>parse-f64</code></td></tr> <tr><td>parsing</td><td><code>parse-i64</code>, <code>parse-f64</code></td></tr>
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next</code>, <code>split</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>to-lower</code>, <code>to-upper</code></td></tr> <tr><td>text</td><td><code>split-on-byte</code>, <code>split-next</code>, <code>split</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>to-lower</code>, <code>to-upper</code></td></tr>
<tr><td>building bytes</td><td><code>append</code>, <code>append-i64</code>, <code>append-f64</code>, <code>concat</code>, <code>join</code>, <code>repeat-bytes</code>, <code>replace-bytes</code>, <code>slices-new</code>, <code>format-f64</code></td></tr> <tr><td>building bytes</td><td><code>append</code>, <code>append-i64</code>, <code>append-f64</code>, <code>concat</code>, <code>join</code>, <code>repeat-bytes</code>, <code>replace-bytes</code>, <code>slices-new</code>, <code>format-f64</code></td></tr>
<tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>rune-start?</code>, <code>valid-utf8?</code>, <code>encode-rune</code></td></tr> <tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>is-rune-start</code>, <code>is-valid-utf8</code>, <code>encode-rune</code></td></tr>
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, <code>abs</code> (generic over every integer width), the constants <code>pi-f32</code>, <code>pi-f64</code>, <code>tau-f32</code>, <code>tau-f64</code>, and libm through a <code>declare</code> at both widths: <code>sqrt</code>, <code>abs</code>, <code>floor</code>, <code>ceil</code>, <code>round</code>, <code>fmod</code>, <code>sin</code>, <code>cos</code>, <code>tan</code>, <code>asin</code>, <code>acos</code>, <code>atan</code>, <code>atan2</code>, <code>log</code>, <code>log2</code>, <code>log10</code>, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> — each spelled <code>-f32</code> or <code>-f64</code></td></tr> <tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, <code>abs</code> (generic over every integer width), the constants <code>pi-f32</code>, <code>pi-f64</code>, <code>tau-f32</code>, <code>tau-f64</code>, and libm through a <code>declare</code> at both widths: <code>sqrt</code>, <code>abs</code>, <code>floor</code>, <code>ceil</code>, <code>round</code>, <code>fmod</code>, <code>sin</code>, <code>cos</code>, <code>tan</code>, <code>asin</code>, <code>acos</code>, <code>atan</code>, <code>atan2</code>, <code>log</code>, <code>log2</code>, <code>log10</code>, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> — each spelled <code>-f32</code> or <code>-f64</code></td></tr>
<tr><td>time</td><td><code>monotonic-ns</code>, <code>monotonic-seconds</code>, <code>unix-ns</code>, <code>unix-seconds</code>, <code>sleep-ns</code>, <code>sleep-seconds</code>, and <code>ns-per-second</code> and its two smaller siblings</td></tr> <tr><td>time</td><td><code>monotonic-ns</code>, <code>monotonic-seconds</code>, <code>unix-ns</code>, <code>unix-seconds</code>, <code>sleep-ns</code>, <code>sleep-seconds</code>, and <code>ns-per-second</code> and its two smaller siblings</td></tr>
<tr><td>files</td><td><code>file-exists?</code> and <code>file-size</code>, which answer a value; <code>slurp</code>, <code>barf</code>, <code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code>, which signal <code>FileError</code> under <code>retry</code> and <code>use-value</code></td></tr> <tr><td>files</td><td><code>file-exists</code> and <code>file-size</code>, which answer a value; <code>slurp</code>, <code>barf</code>, <code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code>, which signal <code>FileError</code> under <code>retry</code> and <code>use-value</code></td></tr>
<tr><td>the operating system</td><td><code>getenv</code>, which answers an <code>(Option [u8])</code> viewing the process environment</td></tr> <tr><td>the operating system</td><td><code>getenv</code>, which answers an <code>(Option [u8])</code> viewing the process environment</td></tr>
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-int</code> (a <code>u64</code>), <code>rand</code> (an <code>f64</code> in [0, 1)), <code>rand-bool</code>, <code>rand-int-range</code> (an <code>i64</code>), <code>rand-float-range</code>. 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</td></tr> <tr><td>random</td><td><code>rand-seed</code>, <code>rand-int</code> (a <code>u64</code>), <code>rand</code> (an <code>f64</code> in [0, 1)), <code>rand-bool</code>, <code>rand-int-range</code> (an <code>i64</code>), <code>rand-float-range</code>. 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</td></tr>
<tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>form-sym?</code>, <code>form-is-sym?</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr> <tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>is-form-named</code>, <code>is-form-sym</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr>
<tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr> <tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr>
</table> </table>
</div> </div>
@ -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.</p> monotonic origin is the first read and not boot.</p>
<p><strong>The file surface is split by whether a handler could do anything.</strong> <p><strong>The file surface is split by whether a handler could do anything.</strong>
<code>file-exists?</code> and <code>file-size</code> answer a <code>bool</code> and an <code>file-exists</code> and <code>file-size</code> answer a <code>bool</code> and an
<code>(Option i64)</code>: absence is the reply, not a fault, and a condition would make <code>(Option i64)</code>: absence is the reply, not a fault, and a condition would make
the ordinary case pay for a handler search. <code>slurp</code>, <code>barf</code>, the ordinary case pay for a handler search. <code>slurp</code>, <code>barf</code>,
<code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code> signal <code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code> signal
@ -1908,8 +1908,8 @@ Two directives:</p>
# behind three innocuous Flan names. # behind three innocuous Flan names.
exclude Mem* exclude Mem*
# The kebab rule gives is-window-ready. Lisp spells a predicate with a ?. # A Flan face the kebab rule would not give.
name IsWindowReady window-ready?</code></pre> name GetFPS fps</code></pre>
<p>An excluded function still says it was excluded rather than going quiet, and a name <p>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, override changes only the Flan face — the C symbol is kept verbatim in the declaration,