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 =(rl/with-drawing ())=: making bare ~()~ a unit value in expression position
was considered and is not wanted. Empty forms doing the right thing — the 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 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 unspoken-for in value position on purpose, against the possibility that the
lists: "we might want lists at some point." 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 So ~()~ remains the type-position spelling of Unit and nothing else, and the
guard below is written against that rather than around it. 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}~. to an ~accepts~, and a new one on ~{a}~.
Where it works: ~let~, and nowhere else, which is where ~{name .field}~ works 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 today. Destructuring binds in ~let~ only. A match arm is *not* a second
dotimes counter and *a match arm's binds* all take a plain name and are refused position the shorthand had to reach: a struct pattern has never worked in one,
by [no_pattern] — so "let and match positions" from the brief has only one half and the refusal there is the match grammar's own — "expected a pattern, found
to satisfy. The shorthand inherits that rule rather than changing it. {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 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 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 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}~ ~{.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 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 - test/programs/prelude-macros.flan, plain and -O0: ~comment~ with four
different kinds of garbage in it; inc/dec over eleven types; ++/-- over a 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. 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 ;; 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 ;; 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 ;; 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- ;; nothing defines, and the report is "unknown name unless-takes-a-test" at the
;; body" at the call site, which is the right place and the wrong sentence. ;; call site, which is the right place and the wrong sentence. That is the next
;; That is the next thing a macro needs and it is written down in NEXT.md. ;; 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: ;; 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 ;; (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" "programs/rl-with-reject.flan"
"with-mode-2d-takes-a-camera-and-a-body"; "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 (* Visibility: main is not a name a package offers, and saying so is the
point "unknown name sand/main" would be true and useless. *) point "unknown name sand/main" would be true and useless. *)
(* Generics, at the definition rather than at a call site. Both of these (* 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" rejects_check "a name with no field after it"
(pt ^ "(defn f [p Point] i32 (let [{a} p] 0))") (pt ^ "(defn f [p Point] i32 (let [{a} p] 0))")
~needle:"has no .field"; ~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" rejects_check "a field the struct does not have"
(pt ^ "(defn f [p Point] i32 (let [{:keys [x z]} p] (+ x z)))") (pt ^ "(defn f [p Point] i32 (let [{:keys [x z]} p] (+ x z)))")