= compares bools, a match over a bool takes true and false, and a match over a dyn takes keyword arms

This commit is contained in:
Joseph Ferano 2026-09-25 22:00:51 +07:00
commit af6e00bc2a
15 changed files with 361 additions and 110 deletions

View File

@ -308,10 +308,6 @@ keyword resolves against the expected type and against nothing else, so two enum
could always share a member spelling. What the prefix buys is the call site read
on its own.
** NEXT Keyword arms over a dyn, and = on bool
Decided 2026-09-25: a match over a dyn takes keyword arms, meaning (= d :k); = and !=
compare bools, and a match over a bool takes true/false arms, exhaustive without _.
** NEXT The .fln printer writes a flat let where the scope does not matter
Decided 2026-09-25: a let whose name no later statement of its block mentions prints
flat, not as a nested block; a one-argument and/or prints as its argument.
@ -326,7 +322,7 @@ guards, or-patterns, literals at any depth, exhaustiveness over the nesting.
** DONE match over numbers and strings
CLOSED: [2026-09-25]
Rules out a literal the scrutinee's type cannot hold (refused, not widened as =(=)=
would), keyword arms over a dyn, and a bare-name catch-all: a bare name is a nullary case.
would) and a bare-name catch-all: a bare name is a nullary case.
** DONE match over enums
CLOSED: [2026-09-25]

View File

@ -8373,12 +8373,16 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
(* A number, a string or a dyn: the arms are literals, and each is the
test (= t lit) over one temporary — the enum's chain, with [=]'s own
two lowerings for the test, so a match over a dyn means what [=] over
it means. [is_equatable]'s set minus the enums, which are above. *)
it means. [is_equatable]'s set minus the enums, which are above, and
minus bool, below. *)
| (Types.Int _ | Types.Float _ | Types.String | Types.Dyn) as t -> `Lit t
(* A bool is a two-member enum spelled true and false: the same chain,
and exhaustive without a [_] once both are named. *)
| Types.Bool -> `Bool
| other ->
fail loc
"match works on an Option, a data type, an enum, a number, a string \
or a dyn, not on %s"
"match works on an Option, a data type, an enum, a bool, a number, a \
string or a dyn, not on %s"
(Types.to_string other)
in
(* A literal arm, spelled as it was written, for the refusals that name one. *)
@ -8398,6 +8402,8 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
| Ast.Byte b when b > 32 && b < 127 -> Printf.sprintf "\\%c" (Char.chr b)
| Ast.Byte b -> string_of_int b
| Ast.Str t -> Printf.sprintf "%S" t
| Ast.Kw k -> ":" ^ k
| Ast.Var b -> b
| _ -> "this literal"
in
let what_ty t = match t with Types.Dyn -> "a dyn" | t -> Types.to_string t in
@ -8411,11 +8417,106 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
(match t with
| Types.String -> "\"yes\" 1 _ 0"
| Types.Float _ -> "0.5 1 _ 0"
| Types.Dyn -> "5 1 :go 2 _ 0"
| _ -> "5 1 _ 0")
in
let bool_fix () =
let name = match scrutinee.Ast.e with Ast.Var n -> n | _ -> "b" in
Printf.sprintf "(match %s true 1 false 0)" name
in
(* The checked literal of each literal arm, by the key [resolve_pat] gave it. *)
let lits : (string, Tast.expr) Hashtbl.t = Hashtbl.create 8 in
let lit_values = ref [] in
(* A literal arm over [t]: its checked value under a fresh key in [lits],
refused if [t] cannot hold it or an earlier arm already equals it. *)
let lit_arm (a : Ast.arm) t (e : Ast.expr) =
let v =
(* [=]'s dyn pair checks its literal at dyn, which boxes it. *)
match trial ctx (fun () -> check ctx ~want:t e) with
| Ok v -> v
| Error _ ->
(* A literal that does not fit is refused, where [=] would widen
the pair and let the arm quietly never match. The literal's
own refusal is not repeated: its fixes are casts, and a cast
is not a pattern. *)
(match t with
| Types.Dyn ->
fail a.Ast.aloc
"this match is over a dyn, which holds a number as an i64 or \
an f64, and %s fits in neither. Change the arm to a value an \
i64 holds, or remove it" (spell e)
| _ -> ());
let tn = Types.to_string t in
let an =
match tn.[0] with
| 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn
| _ -> "a " ^ tn
in
let why =
match e.Ast.e, t with
| Ast.Str _, _ -> "is a string"
| _, Types.String -> "is a number"
| Ast.Float x, Types.Int _ when not (Float.is_integer x) ->
"is not a whole number"
| Ast.Float _, Types.Int _ -> "is a float"
| _ -> "does not fit in one"
in
fail a.Ast.aloc
"this match is over %s, so each arm has to be %s, and %s %s. \
Change the arm to a value %s holds, or remove it"
tn an (spell e) why an
in
(* The arm's value at the scrutinee's type, and a second arm [=] could
not tell from an earlier one is refused, since it can never be
reached: 97 and \a are one u8, 0.1 and 0.10000000001 are one f32,
and over a dyn 1 and 1.0 are equal. Compared pairwise rather than
hashed, because dyn = between an integer and a float goes through
the float and is not transitive past 2^53. *)
let value =
let f32 x = Int32.float_of_bits (Int32.bits_of_float x) in
let num x =
match t with Types.Float Types.F32 -> `F (f32 x) | _ -> `F x
in
match e.Ast.e, t with
| Ast.Str s, _ -> `S s
| (Ast.Int n | Ast.UInt (n, _)), Types.Float _ -> num (Int64.to_float n)
| Ast.Byte b, Types.Float _ -> num (float_of_int b)
| (Ast.Int n | Ast.UInt (n, _)), _ -> `I n
| Ast.Byte b, _ -> `I (Int64.of_int b)
| Ast.Float x, _ -> num x
| Ast.Kw k, _ -> `K k
| Ast.Var b, _ -> `B b
| _ -> assert false
in
let same x y =
match x, y with
| `I a, `I b -> Int64.equal a b
| `F a, `F b -> a = b
| `I a, `F b | `F b, `I a -> Int64.to_float a = b
| `S a, `S b | `K a, `K b | `B a, `B b -> String.equal a b
| _ -> false
in
(match List.find_opt (fun (w, _) -> same value w) !lit_values with
| Some (_, earlier) when earlier = spell e ->
fail a.Ast.aloc "this match has two %s arms" earlier
| Some (_, earlier) ->
fail a.Ast.aloc
"this match has two %s arms — %s equals it as %s, so this arm is \
never reached. Remove it"
earlier (spell e)
(match t with
| Types.Dyn -> "a dyn"
| t ->
let tn = Types.to_string t in
(match tn.[0] with
| 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn
| _ -> "a " ^ tn))
| None -> ());
lit_values := (value, spell e) :: !lit_values;
let key = string_of_int (Hashtbl.length lits) in
Hashtbl.replace lits key v;
Some key, []
in
(* Which case each arm names, and the type of each name it binds. This is the
whole of what differs between the two subjects; everything below it is
shared. *)
@ -8447,91 +8548,27 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
"this match is over the enum %s, and %s is not one of its members. An \
arm names a member as a keyword: %s" n c
(String.concat " " (List.map (fun (m, _) -> ":" ^ m) members))
| `Lit t, Ast.Plit e ->
let v =
(* [=]'s dyn pair checks its literal at dyn, which boxes it. *)
match trial ctx (fun () -> check ctx ~want:t e) with
| Ok v -> v
| Error _ ->
(* A literal that does not fit is refused, where [=] would widen
the pair and let the arm quietly never match. The literal's
own refusal is not repeated: its fixes are casts, and a cast
is not a pattern. *)
(match t with
| Types.Dyn ->
fail a.Ast.aloc
"this match is over a dyn, which holds a number as an i64 or \
an f64, and %s fits in neither. Change the arm to a value an \
i64 holds, or remove it" (spell e)
| _ -> ());
let tn = Types.to_string t in
let an =
match tn.[0] with
| 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn
| _ -> "a " ^ tn
in
let why =
match e.Ast.e, t with
| Ast.Str _, _ -> "is a string"
| _, Types.String -> "is a number"
| Ast.Float x, Types.Int _ when not (Float.is_integer x) ->
"is not a whole number"
| Ast.Float _, Types.Int _ -> "is a float"
| _ -> "does not fit in one"
in
fail a.Ast.aloc
"this match is over %s, so each arm has to be %s, and %s %s. \
Change the arm to a value %s holds, or remove it"
tn an (spell e) why an
in
(* The arm's value at the scrutinee's type, and a second arm [=] could
not tell from an earlier one is refused, since it can never be
reached: 97 and \a are one u8, 0.1 and 0.10000000001 are one f32,
and over a dyn 1 and 1.0 are equal. Compared pairwise rather than
hashed, because dyn = between an integer and a float goes through
the float and is not transitive past 2^53. *)
let value =
let f32 x = Int32.float_of_bits (Int32.bits_of_float x) in
let num x =
match t with Types.Float Types.F32 -> `F (f32 x) | _ -> `F x
in
match e.Ast.e, t with
| Ast.Str s, _ -> `S s
| (Ast.Int n | Ast.UInt (n, _)), Types.Float _ -> num (Int64.to_float n)
| Ast.Byte b, Types.Float _ -> num (float_of_int b)
| (Ast.Int n | Ast.UInt (n, _)), _ -> `I n
| Ast.Byte b, _ -> `I (Int64.of_int b)
| Ast.Float x, _ -> num x
| _ -> assert false
in
let same x y =
match x, y with
| `I a, `I b -> Int64.equal a b
| `F a, `F b -> a = b
| `I a, `F b | `F b, `I a -> Int64.to_float a = b
| `S a, `S b -> String.equal a b
| _ -> false
in
(match List.find_opt (fun (w, _) -> same value w) !lit_values with
| Some (_, earlier) when earlier = spell e ->
fail a.Ast.aloc "this match has two %s arms" earlier
| Some (_, earlier) ->
fail a.Ast.aloc
"this match has two %s arms — %s equals it as %s, so this arm is \
never reached. Remove it"
earlier (spell e)
(match t with
| Types.Dyn -> "a dyn"
| t ->
let tn = Types.to_string t in
(match tn.[0] with
| 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn
| _ -> "a " ^ tn))
| None -> ());
lit_values := (value, spell e) :: !lit_values;
let key = string_of_int (Hashtbl.length lits) in
Hashtbl.replace lits key v;
Some key, []
| `Lit t, Ast.Plit e -> lit_arm a t e
(* Over a dyn a keyword is a value like any other, so :north is the arm
(= d :north), and true and false are the arms (= d true) and
(= d false). *)
| `Lit Types.Dyn, Ast.Pkw k ->
lit_arm a Types.Dyn { Ast.e = Ast.Kw k; loc = a.Ast.aloc }
| `Lit Types.Dyn, Ast.Pctor (("true" | "false") as b, []) ->
lit_arm a Types.Dyn { Ast.e = Ast.Var b; loc = a.Ast.aloc }
| `Bool, Ast.Pctor (("true" | "false") as b, []) -> Some b, []
| `Bool, Ast.Pctor (c, _) ->
fail a.Ast.aloc
"%s names a case, and this match is over a bool, whose arms are true \
and false, as in %s" c (bool_fix ())
| `Bool, Ast.Pkw k ->
fail a.Ast.aloc
":%s is a keyword, and this match is over a bool, whose arms are true \
and false, as in %s" k (bool_fix ())
| `Bool, Ast.Plit e ->
fail a.Ast.aloc
"%s is a literal, and this match is over a bool, whose arms are true \
and false, as in %s" (spell e) (bool_fix ())
| `Lit t, Ast.Pkw k ->
fail a.Ast.aloc
":%s is an enum member, and this match is over %s, whose arms are \
@ -8720,6 +8757,8 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
List.filter_map
(fun (m, _) -> if Hashtbl.mem seen m then None else Some (":" ^ m))
members
| `Bool ->
List.filter (fun c -> not (Hashtbl.mem seen c)) [ "true"; "false" ]
| `Lit _ -> []
in
(* No list of literals covers a number, a string or a dyn, so a literal
@ -8741,7 +8780,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
~notes:(match subject with
| `Data u -> declared_note ctx.env u.Tast.dname
| `Enum (n, _) -> declared_note ctx.env n
| `Option _ | `Lit _ -> [])
| `Option _ | `Bool | `Lit _ -> [])
"this match is not exhaustive — %s %s no arm. Add %s, or a _ arm for \
the rest"
(String.concat ", " missing)
@ -8750,7 +8789,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
let ty = match !want with Some t -> t | None -> Types.Never in
match subject with
| `Option _ | `Data _ -> mk loc ty (Tast.Match (s, arms))
| `Enum _ | `Lit _ ->
| `Enum _ | `Bool | `Lit _ ->
(* The scrutinee once, into a temporary, and then an [if] per arm in the
order written. A [_] arm ends the chain, and so does the last arm of a
match with none: it is exhaustive by the check above, so the last
@ -8773,6 +8812,8 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
mk loc s.Tast.ty (Tast.Int (List.assoc m members, Types.I32))
in
mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; v ]))
| `Bool when String.equal m "true" -> local
| `Bool -> mk loc Types.Bool (Tast.Prim (Tast.Not, [ local ]))
| `Lit Types.Dyn -> dyn_eq loc local (Hashtbl.find lits m)
| _ ->
mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; Hashtbl.find lits m ]))
@ -8827,7 +8868,7 @@ and unknown_name : 'a. ?setting:bool -> ctx -> Loc.t -> string -> 'a =
and [x/2] are one name each. When the parts either side of an operator
character are a value in scope and a number or another value, that is
almost certainly the arithmetic, and the sentence says how to spell it. *)
(if Filename.check_suffix loc.Loc.file ".fln" then begin
(if Source.indented_at loc then begin
let known s =
s <> ""
&& (String.for_all (fun c -> (c >= '0' && c <= '9') || c = '.') s
@ -10180,7 +10221,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
ordering attached — plan.org, Types calls out an ordering as a
collation the language has not picked. Backend codegen (emit.ml,
x86.ml) has a [Types.String] case in the [Eq]/[Ne] arm and nowhere
else. *)
else. A bool is the third: two values and no order between them. *)
let ok =
match name with
| "=" | "!=" -> Types.is_equatable a.Tast.ty
@ -10193,8 +10234,8 @@ and named_call ?(qualified = false) ctx ~want loc name args =
(match name with
| "=" | "!=" ->
fail loc
"%s compares machine numbers, enums and strings, and %s is none of \
those" name (Types.to_string a.Tast.ty)
"%s compares numbers, enums, strings and bools, and %s is none \
of those" name (Types.to_string a.Tast.ty)
| _ ->
fail loc
"%s orders machine numbers and enums, and %s is neither" name

View File

@ -3800,6 +3800,10 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
location. Signed, because a member may be declared negative. *)
| Types.Enum _ ->
ins f "%s = icmp %s %s %s, %s" t (icmp_op true p) (ll x.Tast.ty) a b
(* A bool is an i1 here, and only [=]/[!=] reach it: [<] on a bool is
refused in check.ml. *)
| Types.Bool ->
ins f "%s = icmp %s i1 %s, %s" t (icmp_op false p) a b
(* A string is ptr+len at this boundary, not a machine word, so there is
no [icmp] to reach for — the comparison itself is [flan_str_eq]
(runtime/flan_rt.c), bytewise with a length and a same-pointer fast

View File

@ -1209,6 +1209,22 @@ and header (s : st) w : Form.t =
let wt = advance p in
let rec preds acc =
let e, _ = expr p in
(* [and] is how a condition joins tests, so it is what gets written
for several predicates; the clause separates them with commas. *)
(match e.Form.v with
| Form.List ({ Form.v = Form.Sym "and"; _ } :: (_ :: _ as ps)) ->
let spell (q : Form.t) =
match q.Form.v with
| Form.List [ { Form.v = Form.Sym n; _ };
{ Form.v = Form.Sym v; _ } ] ->
Printf.sprintf "%s(%s)" n v
| _ -> Form.to_string q
in
failk "where-and" e.Form.loc
"a where clause separates its predicates with commas, not and \
— write where %s"
(String.concat ", " (List.map spell ps))
| _ -> ());
match (peek p).tok with
| COMMA -> ignore (advance p); preds (e :: acc)
| _ -> List.rev (e :: acc)

View File

@ -373,6 +373,14 @@ let constraints (body : Form.t list) : Ast.pred list * Form.t list =
ignore vloc;
{ Ast.pname = name; pvar = String.sub v 1 (String.length v - 1);
ploc = p.Form.loc }
(* [and] is how a condition joins tests, so it is what gets written
for several predicates. The .fln reader refuses its own spelling of
this with its own fix; what reaches here is paren code. *)
| Form.List ({ Form.v = Form.Sym "and"; _ } :: (_ :: _ as ps)) ->
Loc.fail p.Form.loc
"a where clause puts several predicates in a vector, not in an and \
— write {:where [%s]}"
(String.concat " " (List.map Form.to_string ps))
| _ ->
Loc.fail p.Form.loc
"a where predicate is (name? $t), one predicate about one type \

View File

@ -41,13 +41,24 @@ let syntax_of_field = function
| Some ("indented" | "fln") -> Indented
| _ -> Paren
let in_request = ref false
let with_code ?indent ~syntax ~at f =
let s = !code_syntax and a = !code_at and i = !code_indent in
let s = !code_syntax and a = !code_at and i = !code_indent
and r = !in_request in
code_syntax := syntax;
code_at := at;
code_indent := indent;
in_request := true;
Fun.protect
~finally:(fun () -> code_syntax := s; code_at := a; code_indent := i) f
~finally:(fun () ->
code_syntax := s; code_at := a; code_indent := i; in_request := r) f
(* Was the code at [loc] written in the indented syntax? Inside an editor
request the request says; outside one a file's name does, as
[read_file] decides. *)
let indented_at (loc : Loc.t) =
if !in_request then !code_syntax = Indented else is_indented loc.Loc.file
(* The paren reader started at a line and column: [Reader.read_all] always
starts at 1:1. *)

View File

@ -305,8 +305,9 @@ let is_comparable = function Enum _ -> true | t -> is_numeric t
(* Equality admits one type ordering does not: a string, grown in by the M2
queue's item 5 — bytewise, by content and not by address, so two
separately built strings with the same bytes are equal. *)
let is_equatable = function String -> true | t -> is_comparable t
separately built strings with the same bytes are equal. A bool is the
other: true and false are two values with no order between them. *)
let is_equatable = function String | Bool -> true | t -> is_comparable t
(* [Never] is the type of an expression that does not produce a value: return,
an early-returning `some`, exit. It fits anywhere, and that is the only

View File

@ -3244,6 +3244,11 @@ and call_native ?at f ~sym ?(chan = false) ~(args : Tast.expr list) ~rty dst =
unsupported
"%s returns %s by value, which needs SysV return classification this \
backend does not have" sym (Types.to_string rty);
(* A C bool is 0 or 1 only by the callee's good faith: [(declare f [..]
bool "abs")] hands back whatever byte abs left. LLVM declares the
return [i1] and reads its low bit, so this does too, and every bool
past this point is 0 or 1 — which [=], [not] and [match] assume. *)
if Types.equal rty Types.Bool then and_imm f.b ~dst:rax 1;
store_loc f ~reg:(if is_float rty then xmm0 else rax) dst rty
end

View File

@ -209,10 +209,16 @@ Each item: the proposal, then the reason in one line.
"ok" -> "fine"
\a -> "a"
_ -> "other"
match ready
true -> go()
false -> wait()
```
An arm's body can be an indented block, which reads as `(do …)`. **Built** (a
one-line block reads as that line). A number, char or string pattern is the
literal as written, compared as `(= t lit)`.
literal as written, compared as `(= t lit)`; over a dyn a keyword or
`true`/`false` is too. A bool's arms are `true` and `false`, and naming both
needs no `_`.
- **Conditions**, clauses at the header's column:
```

View File

@ -0,0 +1,24 @@
;;;; A bool from C that is not 0 or 1 reads as its low bit on both backends:
;;;; abs(3) is 3, which is true, and equal to abs(1).
(declare two [n i32] bool "abs")
(defstruct Flag [on bool])
(defn main [] i32
(let [a (two 3)
b (two 1)
c (two 0)
f (Flag {.on a})
v [a b]]
(print (= a b)) (print " ")
(print (!= a b)) (print " ")
(print (= a true)) (print " ")
(print (= (.on f) b)) (print " ")
(print (= (at v 0) (at v 1))) (print " ")
(print (match a true "T" false "F")) (print " ")
(print (if a "t" "f")) (print " ")
(print (not a)) (print " ")
(print (= c false))
(println "")
0))

View File

@ -0,0 +1,58 @@
;;;; = and != on bools, a match over a bool, and keyword arms over a dyn.
(defstruct Flag [on bool])
(defn truth [n i32] bool (> n 0))
(defn same? [a bool b bool] bool (= a b))
;; Exhaustive without a _ arm: true and false are every bool.
(defn word [b bool] string
(match b
true "yes"
false "no"))
(defn flipped [b bool] i32
(match b
false 0
true 1))
(defn only-true [b bool] i32
(match b
true 1
_ 0))
;; Over a dyn :north is the arm (= d :north), beside numbers, strings and
;; bools, which are dyn values too.
(defn heading [d dyn] string
(match d
:north "up"
:south "down"
1 "one"
"west" "left"
true "true"
_ "other"))
(defn main [] i32
(let [f (Flag {.on true})
g (Flag {.on false})
flags [true false true]]
(print (same? true true)) (print " ")
(print (same? true false)) (print " ")
(print (!= true false)) (print " ")
(print (= (.on f) (truth 3))) (print " ")
(print (= (.on g) (truth 3))) (print " ")
(print (!= (.on g) (at flags 1))) (print " ")
(print (= true (at flags 0) (at flags 2))) (println "")
(println (word true))
(println (word (truth -1)))
(print (flipped true)) (print (flipped false))
(print (only-true true)) (print (only-true false)) (println "")
(println (heading :north))
(println (heading :south))
(println (heading :east))
(println (heading 1.0))
(println (heading "west"))
(println (heading true))
(println (heading false))
0))

View File

@ -445,6 +445,26 @@ let () =
match_lit_out;
outputs ~dev:true "match over literals, dev" "programs/match-literal.flan"
match_lit_out;
(* heading 1.0 is "one": a keyword arm sits in the same dyn = chain as
the literal arms. *)
let match_bool_out =
"true false true true false false true\nyes\nno\n1010\nup\ndown\n\
other\none\nleft\ntrue\nother\n"
in
outputs "match over a bool and keywords" "programs/match-bool.flan"
match_bool_out;
outputs ~opt:"-O0" "match over a bool and keywords, -O0"
"programs/match-bool.flan" match_bool_out;
outputs ~x86:true "match over a bool and keywords, --x86"
"programs/match-bool.flan" match_bool_out;
outputs ~dev:true "match over a bool and keywords, dev"
"programs/match-bool.flan" match_bool_out;
let ffi_bool_out = "true false true true true T t false true\n" in
outputs "a bool from C" "programs/ffi-bool.flan" ffi_bool_out;
outputs ~opt:"-O0" "a bool from C, -O0" "programs/ffi-bool.flan"
ffi_bool_out;
outputs ~x86:true "a bool from C, --x86" "programs/ffi-bool.flan"
ffi_bool_out;
(* update, ++ and -- evaluate their place's subexpressions once: the
counts are the number of calls an index or a key function got. *)
let update_out = "3\n11 20 90\n1 1 3\n16\n2\n7 1\n32\n2 50\n" in

View File

@ -1888,6 +1888,11 @@ let () =
rejects_check ":where is reserved at a defn body's head, even alone"
"(defn f [] dyn {:where 1})\n(defn main [] i32 0)"
~needle:"a where predicate is";
rejects_check "several where predicates joined with and"
"(defn f [x $t y $u] i32 {:where (and (ordered? $t) (equal? $u))} 0)\n\
(defn main [] i32 (f 1 2))"
~needle:"a where clause puts several predicates in a vector, not in an \
and — write {:where [(ordered? $t) (equal? $u)]}";
(* Keywords: dyn where nothing else is asked, still an enum member where an
enum is, and refused where a concrete non-dyn type is wanted. *)
@ -2693,7 +2698,7 @@ let () =
accepts "a wildcard arm is exhaustive"
"(defn g [] (Option i32) None) (defn f [] i32 (match (g) (Some v) v _ 0))";
rejects_check "match on a non-Option"
"(defn f [x bool] i32 (match x _ 0))" ~needle:"match works on an Option";
"(defn f [x [u8]] i32 (match x _ 0))" ~needle:"match works on an Option";
(* ── Names, order-independence, entry point ────────────────────── *)
accepts "mutually recursive, no forward declaration"
@ -4683,9 +4688,9 @@ let () =
(k ^ "(defn f [k K] i32 (match k :lo 1 _ \"x\"))")
~needle:"expected i32, found string";
rejects_check "match over something that is none of them"
"(defn f [n bool] i32 (match n _ 2))"
~needle:"match works on an Option, a data type, an enum, a number, a \
string or a dyn, not on bool";
"(defn f [n [u8]] i32 (match n _ 2))"
~needle:"match works on an Option, a data type, an enum, a bool, a \
number, a string or a dyn, not on [u8]";
(* ── match over literals ───────────────────────────────────────── *)
@ -4762,6 +4767,51 @@ let () =
rejects_check "a literal match over a byte slice, which = does not compare"
"(defn f [b [u8]] i32 (match b \"a\" 1 _ 0))"
~needle:"not on [u8]";
(* ── match over a bool, keyword arms over a dyn ────────────────── *)
accepts "a match over a bool naming both is exhaustive"
"(defn f [b bool] i32 (match b true 1 false 0))";
accepts "a match over a bool, one arm and _"
"(defn f [b bool] i32 (match b false 1 _ 0))";
rejects_check "a match over a bool naming one of them"
"(defn f [b bool] i32 (match b true 1))"
~needle:"this match is not exhaustive — false has no arm. Add it, or a _ \
arm for the rest";
rejects_check "true named twice"
"(defn f [b bool] i32 (match b true 1 true 2 _ 0))"
~needle:"this match has two true arms";
rejects_check "a number arm over a bool"
"(defn f [b bool] i32 (match b 1 1 _ 0))"
~needle:"1 is a literal, and this match is over a bool, whose arms are \
true and false, as in (match b true 1 false 0)";
rejects_check "a keyword arm over a bool"
"(defn f [b bool] i32 (match b :yes 1 _ 0))"
~needle:":yes is a keyword, and this match is over a bool";
accepts "keyword arms over a dyn, beside numbers, strings and bools"
"(defn f [d dyn] i32 (match d :north 1 :south 2 5 3 \"w\" 4 true 5 _ 0))";
rejects_check "keyword arms over a dyn need a _ arm"
"(defn f [d dyn] i32 (match d :north 1 :south 2))"
~needle:"covers every dyn value. Add a _ arm for the rest, as in (match d \
5 1 :go 2 _ 0)";
rejects_check "a keyword named twice over a dyn"
"(defn f [d dyn] i32 (match d :north 1 :north 2 _ 0))"
~needle:"this match has two :north arms";
rejects_check "true named twice over a dyn"
"(defn f [d dyn] i32 (match d true 1 true 2 _ 0))"
~needle:"this match has two true arms";
accepts "a keyword arm over an enum still names a member"
(k ^ "(defn f [k K] i32 (match k :lo 1 _ 0))");
(* = and != compare bools; ordering them stays refused. *)
accepts "= on bools" "(defn f [a bool b bool] bool (= a b))";
accepts "!= on bools, chained" "(defn f [a bool b bool] bool (!= a b true))";
rejects_check "< on bools"
"(defn f [a bool b bool] bool (< a b))"
~needle:"< orders machine numbers and enums, and bool is neither";
rejects_check "= on a struct names what it compares"
"(defstruct P [x i32])\n(defn f [a P b P] bool (= a b))"
~needle:"= compares numbers, enums, strings and bools, and P is none of \
those";
(* A destructuring pattern in an arm's binds is a name position like any
other. *)
rejects_check "a pattern inside a match arm's binds"

View File

@ -287,6 +287,11 @@ let () =
"(defn g [h (Fn [i32 i32] bool)] () (h 1 2))";
reads "untyped parameter is dyn" "fn id(x) -> dyn = x" "(defn id [x dyn] dyn x)";
refuses "no return type" "fn f(x)\n x" "indent/return-type" "-> i32";
(* The fix is .fln's commas whatever the file is called: [read] names it
<syntax>. *)
refuses "where predicates joined with and"
"fn f(x: $t, y: $u) -> i32 where ordered?($t) and equal?($u)\n 0"
"indent/where-and" "commas, not and — write where ordered?($t), equal?($u)";
(* Characters, lexed before brackets and separators. *)
reads "character literals" "x = [\\( \\, \\space \\)]" "(set x [\\( \\, \\space \\)])";
reads "character arguments" "f(\\,, \\))" "(f \\, \\))";
@ -364,6 +369,10 @@ let () =
"(match s (Circle r) r _ (do (a) (b)))";
reads "match over literals" "match n\n 5 -> a\n -2.5 -> b\n \"go\" -> c\n \\a -> d\n _ -> e"
"(match n 5 a -2.5 b \"go\" c \\a d _ e)";
reads "match over a bool" "match b\n true -> a\n false -> b"
"(match b true a false b)";
reads "keyword arms over a dyn" "match d\n :north -> a\n 1 -> b\n _ -> c"
"(match d :north a 1 b _ c)";
reads "handler-bind moves the clauses" "handler-bind\n f()\non E(c)\n g(c)"
"(handler-bind [(E [c] (g c))] (f))";
reads "quote block"

View File

@ -1097,8 +1097,10 @@ as <code>first-even</code> does above.</p>
<p><code>(Option T)</code> is how absence is spelled: a lookup miss, an empty
collection, the end of a stream. <code>match</code> works on an <code>Option</code>, a
<code>defdata</code> and an enum, whose arms name cases; and on a number, a string or a
<code>dyn</code>, whose arms are literals &mdash; <code>(match n 0 "zero" -1 "none" _ "some")</code>
<code>defdata</code> and an enum, whose arms name cases; on a <code>bool</code>, whose arms
are <code>true</code> and <code>false</code> and need no <code>_</code>; and on a number, a string or a
<code>dyn</code>, whose arms are literals &mdash; <code>(match n 0 "zero" -1 "none" _ "some")</code>,
or over a <code>dyn</code> also keywords such as <code>:north</code>
&mdash; each compared with <code>=</code>, with a <code>_</code> arm required for the rest.
<code>some</code> unwraps
<code>Some</code> and early-returns <code>None</code> from the enclosing function.</p>