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 () @@ -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:

- - - - - - + + + + + +
PredicateWhat it admits
integer?bit-and bit-or bit-xor << >> — every integer type, no float
numeric?+ - * / %, and a cast (t x)
enum?a cast to a number, (i32 x) — every enum type
ordered?< <= > >= min max
equal?= and !=
hashable?the variable as a Map key — (map-new t V), get, put, has-key?
is-integerbit-and bit-or bit-xor << >> — every integer type, no float
is-numeric+ - * / %, and a cast (t x)
is-enuma cast to a number, (i32 x) — every enum type
is-ordered< <= > >= min max
is-equal= and !=
is-hashablethe variable as a Map key — (map-new t V), get, put, has-key

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.

GroupNames slice algorithms, over one type variableswap, reverse, sort, sort-by, index-of, min-of, max-of, map-in-place, reduce, filter the per-type layer that stayssum-i32, sum-f32 — the element and the accumulator are different types, which one variable cannot say -bytesbytes=?, bytes<?, bytes-ci=?, starts-with?, ends-with?, index-of-bytes, trim, digit?, space?, sort-bytes +bytesis-bytes-equal, is-bytes-less, is-bytes-ci-equal, has-prefix, has-suffix, index-of-bytes, trim, is-digit, is-space, sort-bytes parsingparse-i64, parse-f64 textsplit-on-byte, split-next, split, lower-ascii, upper-ascii, to-lower, to-upper building bytesappend, append-i64, append-f64, concat, join, repeat-bytes, replace-bytes, slices-new, format-f64 -UTF-8decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune +UTF-8decode-rune, rune-at, rune-count, rune-size, is-rune-start, is-valid-utf8, encode-rune numberssign-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 timemonotonic-ns, monotonic-seconds, unix-ns, unix-seconds, sleep-ns, sleep-seconds, and ns-per-second and its two smaller siblings -filesfile-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 +filesfile-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 systemgetenv, which answers an (Option [u8]) viewing the process environment randomrand-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 macrosform-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 macrosform-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 restpause, 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,