diff --git a/TODO.org b/TODO.org index 45c6a7bc..2d5643fc 100644 --- a/TODO.org +++ b/TODO.org @@ -300,9 +300,10 @@ on its own. Held 2026-09-25 as a future direction, like the JS backend: nested destructuring, guards, or-patterns, literals at any depth, exhaustiveness over the nesting. -** NEXT match over numbers and strings -Decided 2026-09-25: a match arm's pattern can be an integer, a float, a char or a -string literal, compared as =(= t lit)=; a match over such a type needs a =_= arm. +** 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. ** DONE match over enums CLOSED: [2026-09-25] diff --git a/lib/ast.ml b/lib/ast.ml index 824d743d..7ab6b16e 100644 --- a/lib/ast.ml +++ b/lib/ast.ml @@ -222,6 +222,10 @@ and arm = { pat : pattern; body : expr list; aloc : Loc.t } and pattern = | Pctor of string * string list (* (Some e) (Rect w h) None *) | Pkw of string (* :north — an enum member *) + (* 5 -2.5 \a "go" — an Int, UInt, Float, Byte or Str expr, compared as + (= t lit). An expr and not a literal type of its own, so the checker + types it against the scrutinee as any literal is typed against its site. *) + | Plit of expr | Pwild (* _ :else *) (* ── Declarations ──────────────────────────────────────────────────── *) diff --git a/lib/check.ml b/lib/check.ml index 3aa48fab..6b340ded 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3801,6 +3801,11 @@ let box_option ctx loc (t : Types.t) (got : Tast.expr) : Tast.expr = (Tast.Let ([ (s, got) ], [ mk loc Types.Dyn (Tast.If (is_some, some_dyn, none_dyn)) ])) +(* [=] over a dyn pair, answering a bool. Shared by the [=] builtin and a + literal [match] over a dyn, which is (= t lit) by definition. *) +let dyn_eq loc u v = + unbox loc Types.Bool (rt loc Types.Dyn "flan_dyn_eq" [ box loc u; box loc v ]) + let unbox_option ctx loc (t : Types.t) (got : Tast.expr) : Tast.expr = let oty = Types.Option t in match t with @@ -8089,10 +8094,52 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = "%s is a union, and nothing in one records which member was written, \ so there is nothing to match on. Read the member you mean with \ (.member u), or use a defdata" n + (* 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. *) + | (Types.Int _ | Types.Float _ | Types.String | Types.Dyn) as t -> `Lit t | other -> - fail loc "match works on an Option, a data type or an enum, not on %s" + fail loc + "match works on an Option, a data type, an enum, 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. *) + let spell (e : Ast.expr) = + match e.Ast.e with + | Ast.Int n -> Int64.to_string n + | Ast.UInt (_, t) -> t + | Ast.Float x when Float.is_integer x && Float.abs x < 1e15 -> + Printf.sprintf "%.1f" x + | Ast.Float x -> + (* The shortest spelling that reads back as the same float. *) + let rec go p = + let t = Printf.sprintf "%.*g" p x in + if p >= 17 || float_of_string t = x then t else go (p + 1) + in + go 1 + | 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 + | _ -> "this literal" + in + let what_ty t = match t with Types.Dyn -> "a dyn" | t -> Types.to_string t in + (* A literal match that compiles, over the scrutinee's own name where it + has one, for the refusals that need to show the shape. *) + let lit_arms_fix t = + let name = + match scrutinee.Ast.e with Ast.Var n -> n | _ -> "t" + in + Printf.sprintf "(match %s %s)" name + (match t with + | Types.String -> "\"yes\" 1 _ 0" + | Types.Float _ -> "0.5 1 _ 0" + | _ -> "5 1 _ 0") + 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 (* 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. *) @@ -8124,6 +8171,114 @@ 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.Pkw k -> + fail a.Ast.aloc + ":%s is an enum member, and this match is over %s, whose arms are \ + literals, as in %s" k (what_ty t) (lit_arms_fix t) + | `Lit t, Ast.Pctor (c, _) -> + fail a.Ast.aloc + "%s names a case, and this match is over %s, whose arms are \ + literals, as in %s" c (what_ty t) (lit_arms_fix t) + | `Option _, Ast.Plit e -> + fail a.Ast.aloc + "%s is a literal, and this match is over an Option, whose arms are \ + (Some x) and None" (spell e) + | `Enum (n, members), Ast.Plit e -> + fail a.Ast.aloc + "%s is a literal, and this match is over the enum %s, whose arms \ + name its members as keywords: %s" (spell e) n + (String.concat " " (List.map (fun (m, _) -> ":" ^ m) members)) + | `Data u, Ast.Plit e -> + fail a.Ast.aloc + "%s is a literal, and this match is over the data type %s, whose \ + arms name its cases: %s" (spell e) u.Tast.dname + (String.concat ", " + (List.map (fun (v : Tast.variant) -> v.Tast.vname) u.Tast.cases)) | `Option _, Ast.Pkw k -> fail a.Ast.aloc ":%s is an enum member, and this match is over an Option, whose arms \ @@ -8184,7 +8339,10 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = | Some c -> if Hashtbl.mem seen c then fail a.Ast.aloc "this match has two %s arms" - (match subject with `Enum _ -> ":" ^ c | _ -> c); + (match subject, a.Ast.pat with + | `Enum _, _ -> ":" ^ c + | _, Ast.Plit e -> spell e + | _ -> c); Hashtbl.add seen c ()); (a, ctor, binds)) arms @@ -8286,7 +8444,19 @@ 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 + | `Lit _ -> [] in + (* No list of literals covers a number, a string or a dyn, so a literal + match always needs its [_]. Refused here, before the chain below, which + would otherwise run a lone last arm untested as the enum's does. *) + (match subject with + | `Lit t when not !saw_wild -> + Loc.failk "check/non-exhaustive-match" loc + "this match is not exhaustive — its arms are literals, and no list of \ + them covers every %s. Add a _ arm for the rest, as in %s" + (match t with Types.Dyn -> "dyn value" | t -> Types.to_string t) + (lit_arms_fix t) + | _ -> ()); if not !saw_wild && missing <> [] then (* The data type's declaration, because that is where the case list this match failed to cover actually lives, and because adding a case there is what @@ -8295,7 +8465,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 _ -> []) + | `Option _ | `Lit _ -> []) "this match is not exhaustive — %s %s no arm. Add %s, or a _ arm for \ the rest" (String.concat ", " missing) @@ -8304,7 +8474,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 (_, members) -> + | `Enum _ | `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 @@ -8320,10 +8490,18 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms = | ({ Tast.acase = None; _ } as a) :: _ -> body a | [ a ] -> body a | ({ Tast.acase = Some m; _ } as a) :: rest -> - let v = mk loc s.Tast.ty (Tast.Int (List.assoc m members, Types.I32)) in - mk loc ty - (Tast.If (mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; v ])), - body a, chain rest)) + let test = + match subject with + | `Enum (_, members) -> + let v = + mk loc s.Tast.ty (Tast.Int (List.assoc m members, Types.I32)) + in + mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; v ])) + | `Lit Types.Dyn -> dyn_eq loc local (Hashtbl.find lits m) + | _ -> + mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; Hashtbl.find lits m ])) + in + mk loc ty (Tast.If (test, body a, chain rest)) in mk loc ty (Tast.Let ([ (slot, s) ], [ chain arms ])) @@ -9698,7 +9876,8 @@ and named_call ?(qualified = false) ctx ~want loc name args = and the negation is an [i1] flip the backend folds away. *) let link u v = let cmp = - unbox loc Types.Bool (rt loc Types.Dyn sym ([ u; v ] @ site)) + if String.equal sym "flan_dyn_eq" then dyn_eq loc u v + else unbox loc Types.Bool (rt loc Types.Dyn sym ([ u; v ] @ site)) in if String.equal name "!=" then mk loc Types.Bool (Tast.Prim (Tast.Not, [ cmp ])) diff --git a/lib/load.ml b/lib/load.ml index 9b5688b5..930fdda8 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -274,7 +274,7 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr = let bound = match a.Ast.pat with | Ast.Pctor (_, ns) -> ns @ bound - | Ast.Pkw _ | Ast.Pwild -> bound + | Ast.Pkw _ | Ast.Plit _ | Ast.Pwild -> bound in { a with Ast.body = List.map (rename_expr owned alias bound) a.Ast.body }) arms) diff --git a/lib/parse.ml b/lib/parse.ml index a9aa6e0c..b325e03a 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -1402,6 +1402,7 @@ and pattern (f : Form.t) : Ast.pattern = (* An enum member. Which enum is the scrutinee's type, so [Check] resolves it, as it resolves a keyword anywhere an enum is expected. *) | Kw member -> Ast.Pkw member + | Int _ | UInt _ | Float _ | Byte _ | Str _ -> Ast.Plit (expr f) | List ({ v = Sym ctor; _ } :: binds) -> List.iter no_pattern binds; Ast.Pctor (ctor, List.map dname binds) diff --git a/spec-syntax.md b/spec-syntax.md index c43bebb5..2d075b65 100644 --- a/spec-syntax.md +++ b/spec-syntax.md @@ -202,9 +202,17 @@ Each item: the proposal, then the reason in one line. Rect(w, h) -> w * h :north -> 0 _ -> 0 + + match code + 404 -> "missing" + -1 -> "none" + "ok" -> "fine" + \a -> "a" + _ -> "other" ``` An arm's body can be an indented block, which reads as `(do …)`. **Built** (a - one-line block reads as that line). + one-line block reads as that line). A number, char or string pattern is the + literal as written, compared as `(= t lit)`. - **Conditions**, clauses at the header's column: ``` diff --git a/test/programs/match-literal.flan b/test/programs/match-literal.flan new file mode 100644 index 00000000..9253b7aa --- /dev/null +++ b/test/programs/match-literal.flan @@ -0,0 +1,71 @@ +;;;; match over numbers, chars, strings and dyn values: each arm is (= t lit) +;;;; over one temporary, and a _ arm is the rest. + +(defn small [n i16] string + (match n + 5 "five" + -3 "minus three" + _ "other")) + +(defn half [x f32] i32 + (match x + 0.5 1 + 2 2 + _ 0)) + +(defn letter [c u8] i32 + (match c + \a 1 + 98 2 + _ 0)) + +(defn command [s string] i32 + (match s + "go" 1 + "stop" 2 + "" 3 + _ 0)) + +(defn big [n u64] i32 + (match n + 18446744073709551615 1 + _ 0)) + +;; Over a dyn the test is dyn =, so 1 matches 1.0 and "go" matches only a +;; string. +(defn kind [d dyn] string + (match d + 1 "one" + 2.5 "two and a half" + "go" "go" + _ "other")) + +(defn calls [] i32 + (print "(called) ") + 7) + +;; recur from inside an arm: the arm is the loop's tail. +(defn count-down [from i32] i32 + (loop [n from steps 0] + (match n + 0 steps + _ (recur (- n 1) (+ steps 1))))) + +(defn main [] i32 + (println (small 5)) + (println (small -3)) + (println (small 4)) + (print (half 0.5)) (print (half 2.0)) (print (half 3.0)) (println "") + (print (letter 97)) (print (letter 98)) (print (letter 99)) (println "") + (print (command "go")) (print (command "stop")) (print (command "")) + (print (command "gone")) (println "") + (print (big 18446744073709551615)) (print (big 1)) (println "") + (println (kind 1)) + (println (kind 1.0)) + (println (kind 2.5)) + (println (kind "go")) + (println (kind "1")) + ;; The scrutinee is evaluated once, however many arms test it. + (println (match (calls) 1 "a" 2 "b" 7 "seven" _ "c")) + (print (count-down 4)) (println "") + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 29f9a0b3..1c703e47 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -432,6 +432,19 @@ let () = match_enum_out; outputs ~dev:true "match over an enum, dev" "programs/match-enum.flan" match_enum_out; + (* A literal match is the same chain with (= t lit) as each test, so the + dyn rows (1 and 1.0 both "one") are dyn ='s answer. *) + let match_lit_out = + "five\nminus three\nother\n120\n120\n1230\n10\none\none\n\ + two and a half\ngo\nother\n(called) seven\n4\n" + in + outputs "match over literals" "programs/match-literal.flan" match_lit_out; + outputs ~opt:"-O0" "match over literals, -O0" "programs/match-literal.flan" + match_lit_out; + outputs ~x86:true "match over literals, --x86" "programs/match-literal.flan" + match_lit_out; + outputs ~dev:true "match over literals, dev" "programs/match-literal.flan" + match_lit_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 diff --git a/test/test_flan.ml b/test/test_flan.ml index 1e151779..5a85d2b4 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2690,7 +2690,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 i32] i32 (match x _ 0))" ~needle:"match works on an Option"; + "(defn f [x bool] i32 (match x _ 0))" ~needle:"match works on an Option"; (* ── Names, order-independence, entry point ────────────────────── *) accepts "mutually recursive, no forward declaration" @@ -4582,8 +4582,85 @@ 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 i32] i32 (match n _ 2))" - ~needle:"match works on an Option, a data type or an enum, not on i32"; + "(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"; + + (* ── match over literals ───────────────────────────────────────── *) + + (* Each arm is (= t lit) with the literal built at the scrutinee's type, so + a literal that type cannot hold is refused rather than widened into an + arm that never matches. *) + accepts "match over an i16, a literal arm built at i16" + "(defn f [n i16] i32 (match n 5 1 -3 2 _ 0))"; + accepts "match over a string" "(defn f [s string] i32 (match s \"go\" 1 _ 0))"; + accepts "match over a dyn, arms of several kinds" + "(defn f [d dyn] i32 (match d 1 1 2.5 2 \"go\" 3 \\a 4 _ 0))"; + accepts "match over a number, :else for the rest" + "(defn f [n i32] i32 (match n 5 1 :else 0))"; + rejects_check "a literal arm the scrutinee cannot hold" + "(defn f [n i8] i32 (match n 300 1 _ 0))" + ~needle:"this match is over i8, so each arm has to be an i8, and 300 does \ + not fit in one. Change the arm to a value an i8 holds, or remove it"; + rejects_check "a float arm over an integer" + "(defn f [n i32] i32 (match n 1.5 1 _ 0))" + ~needle:"and 1.5 is not a whole number"; + rejects_check "a string arm over a number" + "(defn f [n i32] i32 (match n \"a\" 1 _ 0))" + ~needle:"and \"a\" is a string"; + rejects_check "a number arm over a string" + "(defn f [s string] i32 (match s 5 1 _ 0))" + ~needle:"so each arm has to be a string, and 5 is a number"; + rejects_check "a literal match with no _ arm" + "(defn f [n i32] i32 (match n 5 1 6 2))" + ~needle:"this match is not exhaustive — its arms are literals, and no list \ + of them covers every i32. Add a _ arm for the rest, as in (match \ + n 5 1 _ 0)"; + rejects_check "a literal match over a dyn with no _ arm" + "(defn f [d dyn] i32 (match d 5 1))" + ~needle:"covers every dyn value"; + rejects_check "a literal named twice" + "(defn f [n i32] i32 (match n 5 1 5 2 _ 0))" + ~needle:"this match has two 5 arms"; + rejects_check "a char and a number that are one u8" + "(defn f [c u8] i32 (match c \\a 1 97 2 _ 0))" + ~needle:"this match has two \\a arms — 97 equals it as a u8"; + rejects_check "1 and 1.0 are one arm over a dyn, as dyn = says" + "(defn f [d dyn] i32 (match d 1 1 1.0 2 _ 0))" + ~needle:"this match has two 1 arms — 1.0 equals it as a dyn"; + rejects_check "two literals that round to one f32" + "(defn f [x f32] i32 (match x 0.1 1 0.10000000001 2 _ 0))" + ~needle:"this match has two 0.1 arms — 0.10000000001 equals it as an f32"; + rejects_check "two integers that round to one f32" + "(defn f [x f32] i32 (match x 16777216 1 16777217 2 _ 0))" + ~needle:"two 16777216 arms — 16777217 equals it as an f32"; + rejects_check "an integer and a float that are one f64" + "(defn f [x f64] i32 \ + (match x 4611686018427387904 1 4611686018427387904.0 2 _ 0))" + ~needle:"equals it as an f64"; + accepts "two f64 literals that differ" + "(defn f [x f64] i32 (match x 0.1 1 0.10000000001 2 _ 0))"; + rejects_check "a literal no dyn holds" + "(defn f [x dyn] i32 (match x 18446744073709551615 1 _ 0))" + ~needle:"this match is over a dyn, which holds a number as an i64 or an \ + f64, and 18446744073709551615 fits in neither. Change the arm to \ + a value an i64 holds, or remove it"; + rejects_check "a keyword arm among literal arms" + "(defn f [n i32] i32 (match n 5 1 :lo 2 _ 0))" + ~needle:":lo is an enum member, and this match is over i32, whose arms are \ + literals, as in (match n 5 1 _ 0)"; + rejects_check "a case arm among literal arms" + "(defn f [n i32] i32 (match n 5 1 (Some x) 2 _ 0))" + ~needle:"Some names a case, and this match is over i32"; + rejects_check "a literal arm among keyword arms" + (k ^ "(defn f [k K] i32 (match k :lo 1 5 2 _ 0))") + ~needle:"5 is a literal, and this match is over the enum K"; + rejects_check "a literal arm over an Option" + "(defn f [o (Option i32)] i32 (match o 5 1 _ 0))" + ~needle:"5 is a literal, and this match is over an Option"; + 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]"; (* 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" diff --git a/test/test_syntax.ml b/test/test_syntax.ml index ab4fb6a3..e1a17e11 100644 --- a/test/test_syntax.ml +++ b/test/test_syntax.ml @@ -362,6 +362,8 @@ let () = "(restart-case (f) (continue [] (do)))"; reads "match" "match s\n Circle(r) -> r\n _ ->\n a()\n b()" "(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 "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" diff --git a/web/index.html b/web/index.html index ea18a0de..a8a8a6e8 100644 --- a/web/index.html +++ b/web/index.html @@ -1096,8 +1096,11 @@ as first-even does above.

Option, match and some

(Option T) is how absence is spelled: a lookup miss, an empty -collection, the end of a stream. match works on an Option and -on a defdata, and on nothing else. some unwraps +collection, the end of a stream. match works on an Option, a +defdata and an enum, whose arms name cases; and on a number, a string or a +dyn, whose arms are literals — (match n 0 "zero" -1 "none" _ "some") +— each compared with =, with a _ arm required for the rest. +some unwraps Some and early-returns None from the enclosing function.

(defconst nums [4 i32] [4 8 15 16])