Pin the () body guards, the match-arm rule, and correct two comments

This commit is contained in:
Joseph Ferano 2026-09-20 18:18:20 +07:00
parent fa2b56ba5a
commit 1702a62308
6 changed files with 89 additions and 10 deletions

26
FIX.org
View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -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)))")