From 1702a62308c07c51885d35ec57ee4c0422c84204 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 18:18:20 +0700 Subject: [PATCH] Pin the () body guards, the match-arm rule, and correct two comments --- FIX.org | 26 +++++++++++++++++++------- lib/prelude.ml | 6 +++--- test/programs/rl-with-empty-arg.flan | 21 +++++++++++++++++++++ test/programs/rl-with-empty.flan | 21 +++++++++++++++++++++ test/test_acceptance.ml | 13 +++++++++++++ test/test_flan.ml | 12 ++++++++++++ 6 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 test/programs/rl-with-empty-arg.flan create mode 100644 test/programs/rl-with-empty.flan diff --git a/FIX.org b/FIX.org index ed74150..012f3fb 100644 --- a/FIX.org +++ b/FIX.org @@ -1640,8 +1640,9 @@ The author, 2026-09-20, closing the question DISCUSS.org left open beside =(rl/with-drawing ())=: making bare ~()~ a unit value in expression position was considered and is not wanted. Empty forms doing the right thing — the three above — covers the need that made it look attractive, and ~()~ stays -unspoken-for in value position on purpose, in case the language ever grows -lists: "we might want lists at some point." +unspoken-for in value position on purpose, against the possibility that the +language grows lists later and wants the spelling. (Paraphrased from the +author's note, not quoted.) So ~()~ remains the type-position spelling of Unit and nothing else, and the guard below is written against that rather than around it. @@ -1786,10 +1787,14 @@ with nothing after it, ~{a}~ — and that test is now two: the old one inverted to an ~accepts~, and a new one on ~{a}~. Where it works: ~let~, and nowhere else, which is where ~{name .field}~ works -today. Destructuring binds in ~let~ only — a defn parameter, an fn parameter, a -dotimes counter and *a match arm's binds* all take a plain name and are refused -by [no_pattern] — so "let and match positions" from the brief has only one half -to satisfy. The shorthand inherits that rule rather than changing it. +today. Destructuring binds in ~let~ only. A match arm is *not* a second +position the shorthand had to reach: a struct pattern has never worked in one, +and the refusal there is the match grammar's own — "expected a pattern, found +{a .x}" — not [no_pattern], which is what a defn parameter, an fn parameter +and a dotimes counter get. Checked against the compiler rather than read off +parse.ml's comment: ~{.x .y}~ and ~{a .x}~ in a match arm produce the same +refusal as each other, which is the claim that matters — the shorthand +inherited the existing rule rather than changing it. An unknown field gets the named form's refusal unchanged, because it is the same field access underneath: "Point has no field z" with the declared_note @@ -1802,7 +1807,14 @@ listing the fields there are. accepted at a ~(Fn [] ())~ want and refused at a ~(Fn [] i32)~ one; the ~{.x .y}~ shorthand accepted plain, mixed with a pair, and nested; ~{.z}~ refused by field name; ~{.x}~ inverted from a refusal to an ~accepts~; ~{a}~ - refused. + refused; and both ~{.x .y}~ and ~{a .x}~ refused identically in a match arm, + which is the "wherever the named form works" half of the claim. +- test/programs/rl-with-empty.flan and rl-with-empty-arg.flan, through + test_acceptance's ~refuses~: the two guard shapes, a body starting at + argument zero and a body starting after a camera, each given a bare ~()~ + and each answering the name the zero-argument case already answered. Never + built, which is how rl-with-reject.flan beside them works and is why these + need no raylib on the machine. - test/programs/prelude-macros.flan, plain and -O0: ~comment~ with four different kinds of garbage in it; inc/dec over eleven types; ++/-- over a local, a field, an element and a deref; empty ~when~ and ~unless~ bodies. diff --git a/lib/prelude.ml b/lib/prelude.ml index c9610c9..454684c 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -2031,9 +2031,9 @@ let source = {flan| ;; The one thing the compiler could say and this cannot is a reason. A macro ;; has no error facility: it runs inside the compiler and anything it signals ;; aborts the compile with no location. So a malformed (unless) answers a name -;; nothing defines, and the report is "unknown name unless-takes-a-test-and-a- -;; body" at the call site, which is the right place and the wrong sentence. -;; That is the next thing a macro needs and it is written down in NEXT.md. +;; nothing defines, and the report is "unknown name unless-takes-a-test" at the +;; call site, which is the right place and the wrong sentence. That is the next +;; thing a macro needs and it is written down in NEXT.md. ;; ;; An empty body is allowed, and expands to the (do) it always would have: ;; (unless test) is a guard whose body has not been written yet, which is a diff --git a/test/programs/rl-with-empty-arg.flan b/test/programs/rl-with-empty-arg.flan new file mode 100644 index 0000000..800920f --- /dev/null +++ b/test/programs/rl-with-empty-arg.flan @@ -0,0 +1,21 @@ +;;;; The same bare-() body, on a macro that takes an argument before it. +;;;; +;;;; Two guard shapes, so two programs. with-drawing's body starts at argument +;;;; zero; with-mode-2d's starts at argument one, after the camera, so the +;;;; check is "exactly the minimum arguments, and the last of them is ()" +;;;; rather than "one argument and it is ()". A guard written only for the +;;;; first shape would leave the four siblings that take an argument exactly +;;;; where with-drawing was. +;;;; +;;;; The camera is real here on purpose: the refusal has to be about the body +;;;; and not about the camera, and a () in the camera position is deliberately +;;;; not this guard's business. +;;;; +;;;; Being refused is the whole test; this is never built. + +(import rl "vendor:raylib") + +(defn main [] i32 + (let [c (rl/Camera2D {})] + (rl/with-mode-2d c ())) + 0) diff --git a/test/programs/rl-with-empty.flan b/test/programs/rl-with-empty.flan new file mode 100644 index 0000000..24cc110 --- /dev/null +++ b/test/programs/rl-with-empty.flan @@ -0,0 +1,21 @@ +;;;; with-drawing given a body that is a bare (). +;;;; +;;;; The other way a body can be missing, and the one that used to get through. +;;;; rl-with-reject.flan beside this file is the zero-argument case, which the +;;;; guard always caught; one argument that happens to be () is one argument, +;;;; so it was spliced into the expansion verbatim and the refusal came out of +;;;; the middle of the expanded (do) — "() is not an expression", several forms +;;;; from anything anyone wrote. +;;;; +;;;; () has no value-position meaning in the language at all, so a lone one +;;;; where a body goes is never a body, and the guard answers the same name the +;;;; missing-body case already answered. (do) is what to write for a body that +;;;; is meant to be empty. +;;;; +;;;; Being refused is the whole test; this is never built. + +(import rl "vendor:raylib") + +(defn main [] i32 + (rl/with-drawing ()) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 078ff6a..6524357 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2515,6 +2515,19 @@ let () = "programs/rl-with-reject.flan" "with-mode-2d-takes-a-camera-and-a-body"; + (* The other spelling of a body that was not written, and the reason these + are two programs rather than one: the guard has two shapes. A body that + starts at argument zero is "one argument and it is ()"; a body that + starts after a camera is "exactly the minimum arguments, and the last + of them is ()". Both answer the same name the zero-argument case above + answers, because it is the same mistake. *) + refuses "with-drawing given a bare () for a body" + "programs/rl-with-empty.flan" + "with-drawing-takes-a-body"; + refuses "with-mode-2d given a bare () for a body" + "programs/rl-with-empty-arg.flan" + "with-mode-2d-takes-a-camera-and-a-body"; + (* Visibility: main is not a name a package offers, and saying so is the point — "unknown name sand/main" would be true and useless. *) (* Generics, at the definition rather than at a call site. Both of these diff --git a/test/test_flan.ml b/test/test_flan.ml index 93719b6..541f6c9 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -2652,6 +2652,18 @@ let () = rejects_check "a name with no field after it" (pt ^ "(defn f [p Point] i32 (let [{a} p] 0))") ~needle:"has no .field"; + (* And where the shorthand does *not* reach, which is not a limitation it + introduced: a struct pattern has never worked in a match arm, and the two + spellings are refused identically there. Pinned as a pair, because "the + shorthand works wherever {name .field} works" is the claim, and a row on + only one of them would not be saying it. *) + List.iter + (fun pat -> + rejects_check ("a struct pattern in a match arm: " ^ pat) + (pt ^ Printf.sprintf + "(defn f [p Point] i32 (match p %s 0))" pat) + ~needle:"expected a pattern, found") + [ "{.x .y}"; "{a .x}" ]; rejects_check "a field the struct does not have" (pt ^ "(defn f [p Point] i32 (let [{:keys [x z]} p] (+ x z)))")