The dogfood batch: empty forms, comment, inc and dec, guards, limits, shorthand

# Conflicts:
#	FIX.org
This commit is contained in:
Joseph Ferano 2026-09-20 18:34:13 +07:00
commit e807986622
12 changed files with 962 additions and 21 deletions

232
FIX.org
View File

@ -1746,3 +1746,235 @@ by hand at all three settings and its output diffed across them before the
acceptance rows were written. Not added to ~test_sanitize.ml~: the program
allocates one small dyn map and nothing else, so there is nothing for ASan to
find that ~dyn-map.flan~ does not already exercise.
* Six dogfooding items off DISCUSS.org, 2026-09-20
Each of these is an author note from a session of writing Flan rather than a
report from a test. They are small and they are unrelated to each other, which
is why they went in one lane: every one of them is a place the language said no
for no reason, or did not have a name it should have had.
** (when test) with no body, and the family it turned out to belong to
[lib/parse.ml]'s ~when~ required ~body <> []~ and failed "when is (when test
body ...)". The guard is gone and an empty body is the ~Do []~ that ~(do)~
already means. A ~(when)~ with no test at all is still refused, because there
is nothing to branch on.
~unless~ is a prelude macro now, not a special form, and carried the same
restriction as ~(< (len args) 2)~. It is ~(< (len args) 1)~, and its
unknown-name report narrowed with it: ~unless-takes-a-test~ rather than
~unless-takes-a-test-and-a-body~, since the body is no longer part of the
claim.
The author then added the third member of the family, and it turned out to be
two different questions:
- =(defn foo [bar i32] ())= — a declared return type of () and no body — was
*already legal*, and the refusal for the other case was already the right
one: [Check] says "foo returns i32 but has no body" at the declaration. No
change; both are pinned now, which they were not.
- =(fn [])= was refused by the parser, by the same ~body <> []~ guard ~when~
had. Dropped. An fn declares no return type, so "legal exactly when the
return type is ()" has to be decided somewhere else, and the position it is
written in is the only thing that knows: check_fn now refuses an empty body
at a non-unit want — "an fn with no body answers (), and this one is in a
position that wants i32". *That refusal is new and it was needed:* without
it the empty body fell straight through check_fn's ~List.rev fbody~ match,
the fn compiled, and the call read a return value nothing had written. So
relaxing the parser here opened a hole that had to be closed in the checker,
which is not true of ~when~ or of ~defn~.
** () as a unit value in expression position — considered and dropped
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, 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.
** (rl/with-drawing ()) — a body that was not written, spelled the second way
[vendor/raylib/modes.flan]'s guards caught zero arguments and not one argument
that was itself ~()~, so the latter was spliced into the expansion verbatim and
the report came out of the middle of the expanded ~do~ saying ~()~ is not an
expression — several forms from anything anyone wrote. All five ~with-*~ macros
now treat a lone ~()~ where the body goes as no body, answering the same
unknown-name they already answered for the missing one.
Only a lone ~()~, and only in the body position. ~()~ as a camera or a render
target is left to fail on its own: nothing the macro could say about it would
be truer than what the compiler says.
The macros needed a predicate they did not have. ~form-items~ cannot tell ~()~
from a symbol — it answers the empty slice for both — so [lib/prelude.ml] grew
~form-empty-list?~, which matches ~Form.List~ and asks its length.
** (comment ...), built in
A prelude ~defmacro~ answering ~(do)~ and reading none of its arguments, which
is the whole feature: a macro's arguments are raw Form and are never checked as
expressions, so what is inside never has to be a program. The pinned test puts
an unknown function, a wrong arity, ~(+ 1 "two")~ and a field that does not
exist inside one and compiles it.
The one rule it does obey is the reader's — balanced delimiters, legal tokens —
because reading happens before any macro runs. ~#_~ is the other spelling and
they are not rivals: ~#_~ is the reader's and discards the one form after it,
so it works in argument position; this is a form of its own and takes any
number, which is what a parked block wants.
** inc/dec, ++/--
The four the note spells out, in the prelude rather than per project. A word
for the pure pair, C's punctuation for the mutating pair, so ~(inc i)~ in an
argument and ~(++ i)~ as a statement cannot be confused the way C's ~i++~ and
~i+1~ can.
Generic for free, and verified rather than assumed: the pinned program runs
~inc~ over i8, i16, i32, i64, u8, u16, u32, u64, f32, f64 and a dyn, and prints
the answers. Nothing in the four macros mentions a type, because ~+~ and ~-~
already work at all of them and a macro has no type to get in the way.
*The accepted tradeoff, documented at the definition:* ~(++ PLACE)~ expands to
~(set PLACE (+ PLACE 1))~, so the place is read once and written once and is
therefore *evaluated twice*. Free for a variable, a field or a deref. Not free
for ~(at arr (next-index))~: ~next-index~ runs twice and the read and the write
land on different elements. Not fixable here — macros are non-hygienic by
decision, and a macro cannot bind a temporary for a *place* without a reference
type the language does not have. rl/with-drawing and rl/with-mode-2d already
take the same trade on their arguments.
The note's four macros have no arity guard, and they needed one: ~(inc)~ would
have indexed past the end of its own argument slice and failed inside the
compiler rather than saying anything about the program. Each guards on
~(!= (len args) 1)~ — both too few and too many — and each is pinned.
** Type-limit constants
[lib/prelude.ml] gained i8/i16/i32/i64 and u8/u16/u32/u64 max and min, and
f32/f64 max, min-positive and epsilon. Kebab and the type's own name, following
~ns-per-second~: ~i32-max~, not ~INT_MAX~. Each carries its type, so ~i32-max~
where a u8 is wanted is a type error rather than a silent 255.
The u*-min constants are all zero and are all there. A family with a hole in it
is worse than four lines that say nothing surprising.
There is no ~f32-min~, and the absence is the design. A float's least value is
the negation of its greatest and needs no constant; what a caller reaching for
"min" actually wants is the smallest positive one, which is a different number
entirely. Naming either of them ~f32-min~ would put the collision at the worst
possible place, so the name says which it is: ~f32-min-positive~, the smallest
*normal* value, as Rust's MIN_POSITIVE does.
*u64-max is written in hex and has to be.* The reader parses a decimal integer
through ~Int64.of_string~, and 18446744073709551615 does not fit one;
~0xFFFFFFFFFFFFFFFF~ is read as the 64-bit pattern it names, which is what a
u64 literal is here — [Check.in_range] accepts any pattern at 64 bits unsigned
for exactly this reason. i64-min's decimal *does* fit, being i64's own least
value, so it is written the ordinary way.
*Every value is pinned against an independent derivation, not against itself.*
A wrong constant compiles — that is the whole hazard — so
[test/programs/limits.flan] does not compare any constant to the way the
prelude spells it. The integers are printed, and the expected text in
test_acceptance is the decimal spelling written out from the definition of each
type; an integer's decimal rendering is exact, so that comparison is the whole
value. The floats cannot be pinned that way, because printing one is snprintf
"%g" and 3.40282e+38 is equally true of f32-max and of a neighbourhood around
it — so each is *derived* by exact power-of-two arithmetic and compared for
equality. Every step of those derivations is exact in IEEE-754, and the two
that are not powers of two have representable operands and a representable
product:
| constant | derivation | bit pattern |
|------------------+-------------------------------+--------------------|
| f32-epsilon | 2^-23 | 0x34000000 |
| f64-epsilon | 2^-52 | 0x3CB0000000000000 |
| f32-min-positive | 2^-126 | 0x00800000 |
| f64-min-positive | 2^-1022 | 0x0010000000000000 |
| f32-max | (2 - 2^-23) * 2^127 | 0x7F7FFFFF |
| f64-max | (2 - 2^-52) * 2^1023 | 0x7FEFFFFFFFFFFFFF |
and each epsilon additionally against the property its name promises — adding
it to 1.0 moves, adding half of it does not — and each max against there being
nothing finite above it, since doubling one overflows to an infinity.
The program runs on *both backends*, and that is not ceremony: materialising a
full-width u64 immediate and an f64 bit pattern is a different job in LLVM and
in the hand-written x86 backend, and a lowering that truncated one would print
a number this row catches and nothing else in the suite does. Both print
identical text.
*No infinity or NaN constant, and none is possible to write down.* The reader
has no literal for either. ~(/ 1.0 0.0)~ is the only route to an infinity
today, and under the defconst-is-const rule decided the same day it is not one
a defconst can take: the folding pass is integers only, so a float division is
a computed initialiser and refused by name. So an ~f64-infinity~ defconst is
not available without either a reader literal or a second folder, and neither
is this lane's. Recorded, not added. The *runtime* test for one is in the
prelude already and limits.flan reuses it: an infinity is the value that equals
its own double and is not zero.
** {.row .col} — and the collision the note said was not there
DISCUSS.org: "No obvious grammar collision — nothing currently matches a bare
.field symbol on its own." *That is false*, and it was worth checking before
relying on it. [dmap]'s pair arm takes any pattern in head position, and
[destructure]'s first arm accepts any ~Sym~ as a name — a dotted one included.
So before this change:
- =(let [{.x .y} p] ...)= parsed, as "bind a local called ~.x~ to field ~y~",
and the program failed later with "unknown name x" pointing at the *use*.
Verified against the compiler, not reasoned about.
- an odd number of bare fields hit the ~[odd]~ arm and was refused, which is
where test_flan's =rejects_check "a field name with no pattern before it"=
came from.
So the dot in head position did have a meaning; it was just never a useful one.
The new arm is checked *before* the pair arm and takes both readings away. The
~[odd]~ arm survives for the case it was actually written for — a plain name
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 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
listing the fields there are.
** Pinned
- test_flan.ml: ~(when c)~ parses to if + empty do; ~(when)~ still refused;
~(fn [])~ parses with an empty body; ~(fn)~ still refused; a defn returning
() with no body accepted and one returning i32 refused; an fn with no body
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; 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.
- test/programs/limits.flan, plain, -O0 and --x86: every constant, as above.
- test/programs/destructure.flan gained ~shorthand~ and ~shorthand-mixed~
rows, so the shorthand is in the program that is the destructuring test.
- test_acceptance.ml: the arity guard of every new macro, by the name it
answers, plus ~unless~'s narrowed one.
** Note for the concurrent lanes
The diagnostics lane owns check.ml's message strings. This lane added *one* new
message at a *new* site — check_fn's empty-body refusal — and rewrote none. The
parse.ml edits are structural: a dropped guard in ~when~, a dropped guard in
~fn~, a new arm at the top of ~dmap~. Expect a rebase, not a conflict of
intent.

View File

@ -2939,6 +2939,17 @@ and check_fn ctx ~want loc (params : string list) body =
answer, and it has to be the declared return type. *)
let fbody =
match List.rev fbody with
(* An fn with no body answers unit, the same as a defn whose declared
return type is () and whose body is empty. Unlike a defn it declares no
return type of its own, so there is nothing here to contradict but
the *position* names one, and a position wanting a value is the case
[Check] has to refuse. Without this the empty body would simply fall
through and the call would read a return value nothing ever wrote. *)
| [] when not (Types.equal ret Types.Unit) ->
fail loc
"an fn with no body answers (), and this one is in a position that \
wants %s write the value it should answer"
(Types.to_string ret)
| [] -> fbody
| last :: rest ->
List.rev (expect fctx last.Tast.loc ~want:(Some ret) last :: rest)

View File

@ -355,9 +355,15 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
| _ -> fail f "if is (if test then) or (if test then else)")
(* Sugar, desugared here: special forms until macros land at milestone 5. *)
(* An empty body is allowed, and becomes the same [Ast.Do []] that [(do)]
already means. There was never a reason for the restriction: [(when test)]
is a guard whose consequent has not been written yet, which is a state a
program passes through while it is being written, and refusing it buys
nothing. A [(when)] with no test at all is still refused, because there is
no expression to test. *)
| Sym "when" ->
(match args with
| c :: body when body <> [] ->
| c :: body ->
mk (Ast.If (expr c, { Ast.e = Ast.Do (body_of body); loc = f.loc }, None))
| _ -> fail f "when is (when test body ...)")
@ -439,9 +445,16 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
(* ── binding and control: never a call ─────────────────────────── *)
(* A form that binds a name or alters control flow cannot fall through to
Call it would parse cleanly and mean the wrong thing, silently. *)
(* An empty body is allowed here too, and means the same as it does in
[when] and in a [defn]: the body is [Do []] and the function answers
unit. A [defn] can only have one when its declared return type is (),
because there is a type written down to contradict [Check] refuses
"returns i32 but has no body". An [fn] declares nothing, so an empty body
is not in conflict with anything: it makes the answer type unit rather
than failing to produce a value of some other one. *)
| Sym "fn" ->
(match args with
| { v = Vec ps; _ } :: body when body <> [] ->
| { v = Vec ps; _ } :: body ->
List.iter no_pattern ps;
mk (Ast.Fn (List.map sym ps, body_of body))
| _ -> fail f "fn is (fn [param ...] body ...)")
@ -762,11 +775,27 @@ and destructure (p : Form.t) (v : Ast.expr) : Ast.binding list =
{:keys [x y]} over a struct or [a b] over a fixed array"
(Form.to_string p)
(* {:keys [x y]} and {inner .field}, over a struct. Clojure's map destructuring
with Flan's structs standing in for its maps: [:keys] is the common case and
the pair form is what nests, since a [:keys] entry is a name and never a
pattern. Everything else Clojure puts in this position [:as], [:or],
[:strs], [:syms] is refused by name where it is written.
(* {:keys [x y]}, {.x .y} and {inner .field}, over a struct. Clojure's map
destructuring with Flan's structs standing in for its maps: [:keys] is the
common case and the pair form is what nests, since a [:keys] entry is a name
and never a pattern. Everything else Clojure puts in this position [:as],
[:or], [:strs], [:syms] is refused by name where it is written.
[{.x .y}] is [:keys]'s other spelling and the shortest one: a lone [.field]
with no pattern before it binds a local of the field's own name. It is what
[:keys] would have been if the language had only ever had structs a
struct's fields are typed and known, so naming one is naming the binding
and it puts the field syntax in the place the rest of the language spells a
field. [:keys] stays, because a dyn map's keys are not field names and that
is the form they will keep.
This arm comes before the pair arm and has to: a lone [.x] is a [Sym], and
[destructure] takes any [Sym] as a name, so before this existed [{.x .y}]
parsed as the pair "bind a local called [.x] to field [y]" and the program
failed later with "unknown name x" a mis-parse rather than a refusal. An
odd number of them hit the [has no .field] arm instead. So the dot in head
position did have a meaning here, and this replaces it with the one that was
wanted.
[:keys] keeps its colon while [.field] takes the dot, and the split is the
point rather than an inconsistency: [.field] names a field of the struct,
@ -777,8 +806,16 @@ and destructure (p : Form.t) (v : Ast.expr) : Ast.binding list =
and dmap (p : Form.t) (t : Ast.expr) (items : Form.t list) : Ast.binding list =
let ex loc e : Ast.expr = { Ast.e; loc } in
let field loc name = ex loc (Ast.Field (t, name)) in
let dotted s = String.length s > 1 && s.[0] = '.' in
let rec go = function
| [] -> []
(* The shorthand. Checked first, so a [.field] in head position is never
read as a name to bind. *)
| ({ v = Sym s; _ } as fform) :: rest when dotted s ->
let name = String.sub s 1 (String.length s - 1) in
{ Ast.bname = name; bty = None; bval = field fform.loc name;
bloc = fform.loc }
:: go rest
| { v = Kw "keys"; _ } :: names :: rest ->
let ns =
match names.v with

View File

@ -553,6 +553,80 @@ let source = {flan|
(set i (+ i 1)))
(if neg (Some (- 0 n)) (Some n))))
;; The limits of each numeric type
;;
;; What C spells INT_MAX and FLT_MAX, and what nothing here could reach for:
;; cimport pulls in declared functions, structs and typedefs, never a #define,
;; so limits.h and float.h have no way in. These are written out instead, once,
;; where every program already sees them.
;;
;; Kebab-case and the type's own name, like ns-per-second above: i32-max, not
;; I32_MAX and not INT_MAX. The type prefix is the type as the language spells
;; it, so the constant for a u8 is u8-max and there is nothing to translate.
;;
;; Each carries its type, which is the point of them i32-max is an i32 and
;; putting it where a u8 is wanted is a type error rather than a silent 255.
;; That also means the pair for a type is the pair the *language* has, so
;; u8-min is here beside u16-min and u32-min and u64-min, all of them zero: a
;; family with a hole in it is worse than four lines that say nothing
;; surprising, and code generated over a list of type names needs the hole
;; filled.
;;
;; u64-max is written in hex and it has to be. The reader parses a decimal
;; integer into an i64, and 18446744073709551615 does not fit one; the hex
;; spelling is read as the 64-bit pattern it names, which is what a u64
;; literal is here (see [Check.in_range], which accepts any pattern at 64 bits
;; unsigned for exactly this reason). i64-min's decimal spelling *does* fit,
;; since it is i64's own least value, so it is written the ordinary way.
(defconst i8-max i8 127)
(defconst i8-min i8 -128)
(defconst i16-max i16 32767)
(defconst i16-min i16 -32768)
(defconst i32-max i32 2147483647)
(defconst i32-min i32 -2147483648)
(defconst i64-max i64 9223372036854775807)
(defconst i64-min i64 -9223372036854775808)
(defconst u8-max u8 255)
(defconst u8-min u8 0)
(defconst u16-max u16 65535)
(defconst u16-min u16 0)
(defconst u32-max u32 4294967295)
(defconst u32-min u32 0)
(defconst u64-max u64 0xFFFFFFFFFFFFFFFF)
(defconst u64-min u64 0)
;; The floats are three questions and not two, which is why there is no
;; f32-min here to sit beside f32-max.
;;
;; A float's least value is just the negation of its greatest (- 0.0 f32-max)
;; so a constant for it would say nothing the language cannot. What a caller
;; actually reaches for under the name "min" is the smallest positive one, and
;; that is a different number entirely. Naming it f32-min would make the two
;; readings collide at the worst possible place, so the name says which it is:
;; f32-min-positive, the smallest *normal* positive value, as Rust's
;; MIN_POSITIVE does. Below it the subnormals run further down still, trading
;; mantissa bits for exponent range; nothing here names one, because a program
;; that wants the last subnormal wants to say so.
;;
;; The epsilons are the gap from 1.0 to the next representable value above it
;; 2^-23 and 2^-52, the mantissa widths and not "the smallest number you can
;; add to anything". That distinction is the whole reason a comparison written
;; (< (abs (- a b)) f64-epsilon) is wrong for any a and b of interesting size,
;; and the reason this is named epsilon and not tolerance.
;;
;; Every decimal below is the shortest one that round-trips to the exact value
;; intended, and each is pinned against an independent derivation in
;; test/programs/limits.flan rather than trusted. There is no infinity or NaN
;; constant, and there cannot be one written down: the reader has no literal
;; for either. (/ 1.0 0.0) is the only way to reach an infinity today.
(defconst f32-max f32 3.4028234663852886e38)
(defconst f64-max f64 1.7976931348623157e308)
(defconst f32-min-positive f32 1.1754943508222875e-38)
(defconst f64-min-positive f64 2.2250738585072014e-308)
(defconst f32-epsilon f32 1.1920928955078125e-07)
(defconst f64-epsilon f64 2.220446049250313e-16)
;; Numbers
;;
;; Only the ones that encode a decision. abs is (max x (- 0 x)); a wrapper over
@ -1957,14 +2031,94 @@ 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
;; state a program passes through on the way to being finished, and refusing it
;; bought nothing. `when` in lib/parse.ml is the same change; the two are
;; halves of one form and only a restriction they both carried would be worth
;; keeping. A test is still required, because there is nothing to negate
;; without one.
(defmacro unless [args]
(if (< (len args) 2)
`(unless-takes-a-test-and-a-body)
(if (< (len args) 1)
`(unless-takes-a-test)
`(if (not ~(at args 0)) (do ~@(form-rest args 1)))))
;; comment
;;
;; (comment (whatever you like)) is nothing at all, and the "whatever you like"
;; is the whole feature. A macro's arguments arrive as raw Form and are never
;; checked as expressions, so what is inside can name functions that do not
;; exist, call them at the wrong arity, or add a string to a number: none of it
;; is ever looked at, because this answers (do) without reading a single
;; argument. That is Clojure's (comment ...) exactly, and it is what ;; cannot
;; do a commented-out block stops being a form, so an editor can no longer
;; move over it, indent it or send it to the REPL, and a discarded one still
;; can.
;;
;; The one thing it does require is that the contents READ: balanced
;; delimiters and legal tokens, since the reader runs before any macro does.
;; An unterminated string inside a (comment ...) is still an unterminated
;; string.
;;
;; #_ is the other spelling and they are not rivals: #_ discards the one form
;; after it and is the reader's, so it works in any position including inside
;; another form's arguments; this is a form of its own and takes any number,
;; which is what a block of parked code wants. Built in rather than left to
;; every project, because a name this standard should mean the same thing in
;; all of them.
(defmacro comment [args]
`(do))
;; inc/dec and ++/--
;;
;; Two pairs, and the split between them is the whole design. inc and dec
;; answer a number and change nothing; ++ and -- change a place and answer
;; whatever `set` answers. The spelling says which: a word for the pure one, a
;; punctuation pair borrowed from C for the one with the effect, so
;; (inc i) in an argument and (++ i) as a statement never get confused for one
;; another the way C's i++ and i+1 do.
;;
;; Generic for free, all four of them, because + and - already are: (inc x) is
;; (+ x 1) with the literal taking whichever numeric type x has i8 through
;; i64, u8 through u64, f32, f64, and a dyn and none of that is this macro's
;; business. There is no per-type family here and there is no `where` clause,
;; because a macro does not have a type at all; the expansion is checked at the
;; call site as if it had been written there.
;;
;; **++ and -- read the place twice, and that is an accepted cost.** The
;; expansion is (set PLACE (+ PLACE 1)), so PLACE is evaluated once to read
;; and once to write. For a variable, a field or a deref that is free and
;; means nothing. For (at arr (next-index)) an index with a side effect
;; it means next-index runs twice and the read and the write land on different
;; elements. That is not a bug to be fixed here: macros are non-hygienic by
;; decision (plan.org, open decision 2), a macro cannot bind a temporary for
;; the *place* without a reference type it does not have, and
;; rl/with-drawing and rl/with-mode-2d already take the same trade on their
;; arguments. Write the index out first if it does anything.
(defmacro inc [args]
(if (!= (len args) 1)
`(inc-takes-one-number)
`(+ ~(at args 0) 1)))
(defmacro dec [args]
(if (!= (len args) 1)
`(dec-takes-one-number)
`(- ~(at args 0) 1)))
(defmacro ++ [args]
(if (!= (len args) 1)
`(++-takes-one-place)
`(set ~(at args 0) (+ ~(at args 0) 1))))
(defmacro -- [args]
(if (!= (len args) 1)
`(---takes-one-place)
`(set ~(at args 0) (- ~(at args 0) 1))))
;; into: a fused transformation, and not a transducer
;;
;; (into xs (vec-new i32) (map double) (filter even?))
@ -2049,6 +2203,16 @@ let source = {flan|
(Form.Sym s) (bytes=? (bytes s) (bytes name))
_ false))
;; Whether a form is the empty list, (). [form-items] cannot answer this: it
;; returns the empty slice for a non-list too, so "no items" and "not a list"
;; arrive the same. A macro that has to tell `()` from a name needs the
;; difference see vendor/raylib/modes.flan, where a lone () argument is a
;; body that was not written rather than a body of one form.
(defn form-empty-list? [f Form] bool
(match f
(Form.List xs) (= (len xs) 0)
_ false))
(defn form-is-sym? [f Form] bool
(match f
(Form.Sym s) true

View File

@ -40,6 +40,16 @@
(let [{a .x b .y} (Point {.x 10 .y 20})]
(show2 "pairs" a b))
;; The shorthand: a lone .field with no name before it binds a local of the
;; field's own name, which is what :keys does and in the spelling the rest of
;; the language uses for a field. It mixes with the pair form in one brace,
;; because the two are read one item at a time and a dot in head position is
;; the only thing that tells them apart.
(let [{.x .y} (Point {.x 30 .y 40})]
(show2 "shorthand" x y))
(let [{.x b .y} (Point {.x 50 .y 60})]
(show2 "shorthand-mixed" x b))
(let [l (Line {.a (Point {.x 5 .y 6}) .b (Point {.x 7 .y 8})})]
(let [{{:keys [x y]} .b} l]
(show2 "nested" x y))

121
test/programs/limits.flan Normal file
View File

@ -0,0 +1,121 @@
;;;; The prelude's type limits, checked against something other than themselves.
;;;;
;;;; A wrong constant here would compile. That is the whole reason this program
;;;; exists: i32-max off by one, or f64-max one ulp low, is a number the
;;;; compiler has no opinion about, and it would sit in the prelude being
;;;; subtly wrong in every program that read it. So nothing below asserts a
;;;; constant against the way it is spelled in the prelude.
;;;;
;;;; The integers are checked by printing them. The expected output beside this
;;;; program in test_acceptance is the decimal spelling of each limit, written
;;;; out independently, and an integer's decimal rendering is exact — so the
;;;; comparison is the whole value and not an approximation of it. u64-max is
;;;; the one that matters most: it is written in hex in the prelude, because
;;;; the reader cannot take its decimal, and this is where that hex is read
;;;; back as the number it is supposed to name.
;;;;
;;;; The floats cannot be checked that way, because printing one is snprintf
;;;; "%g" and that is six significant digits — 3.40282e+38 is equally true of
;;;; f32-max and of a dozen values around it. So each is *derived* here by
;;;; exact power-of-two arithmetic and compared for equality. Every step of
;;;; that derivation is exact in IEEE-754: doubling and halving a float only
;;;; moves the exponent, and the one multiplication that is not a power of two
;;;; has both operands representable and a representable product. The
;;;; derivations are therefore a second, independent construction of the same
;;;; bit pattern, which is what a pin needs to be.
;; 2^n, built by repeated doubling from 1.0 and reciprocated for a negative n.
;; Exact for every n this program asks for: the largest is 2^1023, which is
;; half of f64-max and so is nowhere near overflowing, and the smallest is
;; 2^-1022, whose reciprocal partner 2^1022 is a normal value — so no step
;; passes through a subnormal, where the halving would start losing bits.
(defn p2-f64 [n i32] f64
(let [m (if (< n 0) (- 0 n) n)
x 1.0]
(dotimes [i m]
(set x (* x 2.0)))
(if (< n 0) (/ 1.0 x) x)))
;; The same, at f32's width and with f32's exponent range. 2^127 is the
;; largest normal power of two an f32 holds and 2^-126 the smallest, and both
;; are exactly the ends this file asks for.
(defn p2-f32 [n i32] f32
(let [m (if (< n 0) (- 0 n) n)
x (f32 1.0)]
(dotimes [i m]
(set x (* x (f32 2.0))))
(if (< n 0) (/ (f32 1.0) x) x)))
;; An infinity is a value that equals its own double and is not zero — the
;; same test format-f64 in the prelude uses, and the only one available with
;; no infinity literal to compare against.
(defn inf-f64? [x f64] bool
(and (= x (* x 2.0)) (!= x 0.0)))
(defn inf-f32? [x f32] bool
(and (= x (* x (f32 2.0))) (!= x (f32 0.0))))
(defn say [name string ok bool] ()
(print name)
(print " ")
(println (if ok "ok" "WRONG")))
(defn main [] i32
;; The integers, each printed as the exact decimal the expected output pins.
(println i8-max)
(println i8-min)
(println i16-max)
(println i16-min)
(println i32-max)
(println i32-min)
(println i64-max)
(println i64-min)
(println u8-max)
(println u8-min)
(println u16-max)
(println u16-min)
(println u32-max)
(println u32-min)
(println u64-max)
(println u64-min)
;; The floats, each against its derivation.
;;
;; An epsilon is the gap from 1.0 to the next value above it, which is
;; 2^-(mantissa bits): 23 for an f32, 52 for an f64. Derived that way here,
;; and then confirmed by the property the name actually promises — adding it
;; to 1.0 moves, adding half of it does not.
(say "f32-epsilon" (= f32-epsilon (p2-f32 -23)))
(say "f64-epsilon" (= f64-epsilon (p2-f64 -52)))
(say "f32-epsilon is the step above 1.0"
(and (!= (+ (f32 1.0) f32-epsilon) (f32 1.0))
(= (+ (f32 1.0) (/ f32-epsilon (f32 2.0))) (f32 1.0))))
(say "f64-epsilon is the step above 1.0"
(and (!= (+ 1.0 f64-epsilon) 1.0)
(= (+ 1.0 (/ f64-epsilon 2.0)) 1.0)))
;; The smallest positive *normal* value is 2^(1-bias): 2^-126 and 2^-1022.
;; Halving one leaves the normals, so the value below it is not simply half
;; — that is the property that says this is the boundary and not some value
;; near it.
(say "f32-min-positive" (= f32-min-positive (p2-f32 -126)))
(say "f64-min-positive" (= f64-min-positive (p2-f64 -1022)))
;; The greatest finite value is (2 - 2^-mantissa) * 2^maxexp. Both factors
;; are exactly representable and so is the product, which is why this
;; derivation is an equality and not a near-miss. Doubling it overflows to
;; an infinity, which is the other end of the same claim: there is nothing
;; finite above it.
(say "f32-max" (= f32-max (* (p2-f32 127) (- (f32 2.0) (p2-f32 -23)))))
(say "f64-max" (= f64-max (* (p2-f64 1023) (- 2.0 (p2-f64 -52)))))
(say "f32-max is the last finite f32" (inf-f32? (* f32-max (f32 2.0))))
(say "f64-max is the last finite f64" (inf-f64? (* f64-max 2.0)))
;; And the two the language does not need a constant for, said once so that
;; the absence is recorded rather than merely unmentioned: a float's least
;; value is the negation of its greatest, and there is nothing to derive.
(say "f32's least value negates its greatest"
(< (- (f32 0.0) f32-max) (- (f32 0.0) f32-min-positive)))
(say "f64's least value negates its greatest"
(< (- 0.0 f64-max) (- 0.0 f64-min-positive)))
0)

View File

@ -0,0 +1,103 @@
;;;; comment, inc/dec, ++/--, and an empty body — the prelude's small macros,
;;;; asserted through a compiler that has to run them.
;;;;
;;;; These cannot be asserted in test_flan the way a special form can: a macro
;;;; is compiled into a shared object and dlopened into the compiler before
;;;; the first line below is parsed, so the only honest test of one is a
;;;; program that was built. macro-unless.flan beside this file is the same
;;;; argument for the same reason.
(defstruct Counter [hits i32 misses i32])
(defvar dyn-count dyn 5)
(defn show [label string n i32] ()
(print label)
(print " ")
(println n))
;; (comment ...) never reads its arguments, so nothing inside one has to be a
;; program. Everything in this function's comment would be a refusal written
;; anywhere else: a name nothing defines, a call at an arity it does not have,
;; a string added to a number, a field of a struct that has no such field.
;; The one rule it does obey is the reader's — delimiters balance and every
;; token is legal — because reading happens before any macro runs.
(defn commented [] i32
(comment
(no-such-function 1 2 3)
(show "too" "few")
(+ 1 "two")
(.nonexistent (Counter {}))
(defn this is not even a definition))
7)
(defn main [] i32
(println (commented))
;; inc and dec answer a number and change nothing.
(let [n 10]
(show "inc" (inc n))
(show "dec" (dec n))
(show "n unchanged" n))
;; Generic for free at every numeric type, because + and - already are.
;; Nothing below names a type twice and there is no per-type family.
(let [a (i8 1)
b (i16 1)
c 1
d (i64 1)
e (u8 1)
f (u16 1)
g (u32 1)
h (u64 1)]
(print (inc a)) (print " ")
(print (inc b)) (print " ")
(print (inc c)) (print " ")
(print (inc d)) (print " ")
(print (inc e)) (print " ")
(print (inc f)) (print " ")
(print (inc g)) (print " ")
(println (inc h)))
(let [x (f32 1.5)
y 1.5]
(print (inc x)) (print " ")
(println (dec y)))
(println (inc dyn-count))
;; ++ and -- change a place. Every place `set` takes is one: a local, a
;; field, an element, a deref.
(let [n 0]
(++ n)
(++ n)
(-- n)
(show "local" n))
(let [c (Counter {.hits 0 .misses 9})]
(++ (.hits c))
(++ (.hits c))
(-- (.misses c))
(show "field hits" (.hits c))
(show "field misses" (.misses c)))
(let [xs [10 20 30]]
(++ (at xs 1))
(-- (at xs 2))
(show "element 1" (at xs 1))
(show "element 2" (at xs 2)))
(let [n 100
p (addr n)]
(++ (deref p))
(show "through a pointer" n))
;; A body that was not written. (when test) and (unless test) are the guard
;; a program passes through while it is being written, and both expand to
;; the (do) they always would have — no branch taken, nothing printed, and
;; the form's value is () either way.
(let [n 0]
(when (= n 0))
(unless (= n 0))
(when (= n 0) (++ n))
(unless (= n 1) (++ n))
(show "after empty and written bodies" n))
0)

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
@ -3426,6 +3439,29 @@ level "1"
outputs ~opt:"-O0" "unless, now a prelude macro, -O0"
"programs/macro-unless.flan" unless_out;
(* The rest of the prelude's macros, and for the same reason: a macro is
compiled into a shared object and dlopened into the compiler before the
program that calls it is parsed, so the only honest assertion about one
is a program that was built.
The generic row -- eight integer widths, two float widths and a dyn,
all printing 2 or the obvious successor -- is the claim that inc and
dec needed no type machinery of their own: + already works at every one
of those, and a macro has no type to get in the way. A per-type family
would have had to be written and this row would look the same, which is
why it prints the answers rather than merely compiling. *)
let prelude_macros_out =
"7\ninc 11\ndec 9\nn unchanged 10\n\
2 2 2 2 2 2 2 2\n2.5 0.5\n6\n\
local 1\nfield hits 2\nfield misses 8\n\
element 1 21\nelement 2 29\nthrough a pointer 101\n\
after empty and written bodies 1\n"
in
outputs "comment, inc/dec, ++/-- and an empty body"
"programs/prelude-macros.flan" prelude_macros_out;
outputs ~opt:"-O0" "comment, inc/dec, ++/-- and an empty body, -O0"
"programs/prelude-macros.flan" prelude_macros_out;
(* An error on code a macro produced says which macro, and it has to be
asserted through a real expansion: the tag is put on by [Macro] and
defaulted into the diagnostic by [Loc], and a unit test on either half
@ -3461,6 +3497,42 @@ level "1"
print_endline "FAIL the report does not say which macro"
end);
(* Every prelude macro's arity guard, said the only way a macro can say
anything: a call to a name nothing defines, reported at the call site.
The point of pinning these is that without a guard the macro would
index past the end of its own argument slice, and the failure would be
a bounds trap inside the compiler rather than a message about the
program. Each row therefore asserts the *name*, which is the sentence
the author actually reads. *)
let macro_arity name src needle =
match
Check.program (Parse.program (Reader.read_all ~file:"<arity>" src))
with
| _ ->
incr failures;
Printf.printf "FAIL %s\n it was accepted\n" name
| exception Loc.Error { Loc.dmsg = m; _ } ->
if not (contains m needle) then begin
incr failures;
Printf.printf "FAIL %s\n said: %S\n wanted: %S in it\n"
name m needle
end
in
macro_arity "inc with no argument"
"(defn main [] i32 (inc))" "inc-takes-one-number";
macro_arity "inc with two arguments"
"(defn main [] i32 (inc 1 2))" "inc-takes-one-number";
macro_arity "dec with no argument"
"(defn main [] i32 (dec))" "dec-takes-one-number";
macro_arity "++ with no argument"
"(defn main [] i32 (++) 0)" "++-takes-one-place";
macro_arity "-- with two arguments"
"(defn main [] i32 (let [a 1 b 2] (-- a b)) 0)" "---takes-one-place";
(* unless keeps a guard, and it is now the narrower one: a body may be
missing, a test may not. *)
macro_arity "unless with no test at all"
"(defn main [] i32 (unless) 0)" "unless-takes-a-test";
(* The two ways expansion does not terminate, and they are different
failures. A ring is a compile-order problem -- each body calls the other
while the other is being compiled -- and there is no order, so it is
@ -4259,6 +4331,44 @@ level "1"
incr failures;
Printf.printf "FAIL %s\n refused: %S\n" name m);
(* ── The prelude's type limits ──────────────────────────────── *)
(* A wrong constant compiles, which is the only reason this row is worth
its seconds: nothing in the compiler has an opinion about whether
i32-max is 2147483647 or one less, and a prelude constant that is
subtly wrong is wrong in every program that reads it.
The expected text below is written out from the definitions of the
types and not copied from the prelude, so the two spellings of each
integer limit have to agree. The floats cannot be pinned this way
printing one is "%g", six digits, true of a whole neighbourhood of
values so limits.flan derives each by exact power-of-two arithmetic
and prints whether the derivation matched; a WRONG in that half fails
this row on the text.
--x86 as well, and that is not ceremony: a limit is a constant the
backend has to materialise, and the two backends build an f64 bit
pattern and a full-width u64 immediate by entirely different routes.
An x86 lowering that truncated one would print a number this row
would catch and nothing else in the suite would. *)
let limits_out =
"127\n-128\n32767\n-32768\n2147483647\n-2147483648\n\
9223372036854775807\n-9223372036854775808\n\
255\n0\n65535\n0\n4294967295\n0\n18446744073709551615\n0\n\
f32-epsilon ok\nf64-epsilon ok\n\
f32-epsilon is the step above 1.0 ok\n\
f64-epsilon is the step above 1.0 ok\n\
f32-min-positive ok\nf64-min-positive ok\n\
f32-max ok\nf64-max ok\n\
f32-max is the last finite f32 ok\n\
f64-max is the last finite f64 ok\n\
f32's least value negates its greatest ok\n\
f64's least value negates its greatest ok\n"
in
outputs "type limits" "programs/limits.flan" limits_out;
outputs ~opt:"-O0" "type limits, -O0" "programs/limits.flan" limits_out;
outputs ~x86:true "type limits, --x86" "programs/limits.flan" limits_out;
let signed_out = "-4\n-1\nbig is not small\nbig is large\n1\n" in
outputs "signedness" "programs/signedness.flan" signed_out;
outputs ~opt:"-O0" "signedness, -O0" "programs/signedness.flan" signed_out;
@ -4275,7 +4385,8 @@ level "1"
tail slice is an address into a local array, and mem2reg launders a
sloppy one. *)
let destructure_out =
"keys 1 2\npairs 10 20\nnested 7 8\nshadow 5 6\nsequential 100 200\n\
"keys 1 2\npairs 10 20\nshorthand 30 40\nshorthand-mixed 50 60\n\
nested 7 8\nshadow 5 6\nsequential 100 200\n\
array 11 22 33\nrest 1 4 2 5\nempty-tail 17 0\nnested-in-array 1 4\n\
struct-tail 1 2 4 5\ncalls 2 14\n"
in

View File

@ -314,6 +314,34 @@ let () =
| If (_, { e = Do [ _; _ ]; _ }, None) -> ()
| _ -> check "when -> if+do" false);
(* An empty body is the same [Do []] that [(do)] already is, and not a
refusal. (when test) is a guard whose consequent has not been written yet
-- a state a program passes through while it is being written -- and
refusing it bought nothing that the empty [do] does not already allow.
[unless] in the prelude took the same change; it is a macro now, so it is
asserted in programs/prelude-macros.flan instead of here. *)
(match (parse1 "(when c)").e with
| If (_, { e = Do []; _ }, None) -> ()
| _ -> check "(when test) with no body -> if+(do)" false);
(* The test is still required, because there is nothing to branch on
without one. *)
parse_rejects "when with no test at all" "(defn f [] () (when))"
~needle:"when is (when test body ...)";
(* The same rule for a function with nothing in it. A [defn] whose declared
return type is () and whose body is empty has always been legal -- there
is a unit to answer and no forms needed to reach it -- and [Check] refuses
the case where the declaration disagrees, "returns i32 but has no body".
An [fn] now parses the same way; it declares no return type, so the
position it sits in is what decides, and the two rows below check.ml's
arms are in the checker section further down. *)
(match (parse1 "(fn [])").e with
| Fn ([], []) -> ()
| _ -> check "(fn []) parses with an empty body" false);
parse_rejects "fn with no parameter vector" "(defn f [] () (fn))"
~needle:"fn is (fn [param ...] body ...)";
(* unless was here, and is not any more: it is a defmacro in the prelude,
and the parser has nothing to say about it. What it expands to is the
same if-over-(not) this used to assert, and it is asserted where it can
@ -2655,6 +2683,28 @@ let () =
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [] 2)]))")
~needle:"a handler-case clause is (Type [name] body ...)";
(* ── A function with nothing in it ─────────────────────────────── *)
(* The empty body, the other half of (when test) with no body: a function
that does nothing is a function, and the only question is whether it has
a value to answer. A declared () says it does not and the body may be
empty; a declared anything else says it does, and an empty body cannot
provide one. *)
accepts "a defn returning () with no body"
"(defn nothing [n i32] ())\n(defn f [] i32 (nothing 1) 0)";
rejects_check "a defn returning a value with no body"
"(defn nothing [n i32] i32)"
~needle:"returns i32 but has no body";
(* An fn declares no return type, so the position decides instead. Both arms
are asserted, because the refusal is the one that would otherwise let a
call read a return value nothing wrote. *)
accepts "an fn with no body where a (Fn [] ()) is wanted"
"(defn call [f (Fn [] ())] () (f))\n(defn f [] i32 (call (fn [])) 0)";
rejects_check "an fn with no body where a value is wanted"
"(defn call [f (Fn [] i32)] i32 (f))\n(defn f [] i32 (call (fn [])))"
~needle:"an fn with no body answers ()";
(* ── Destructuring ─────────────────────────────────────────────── *)
(* A pattern is desugared in [Parse] into the bindings and field accesses that
@ -2680,6 +2730,47 @@ let () =
(pt ^ "(defn mk [] Point (Point {.x 1 .y 2}))\n\
(defn f [] i32 (let [{:keys [x y]} (mk)] (+ x y)))");
(* The shorthand: a lone .field binds a local of the field's own name. It is
:keys said in the spelling the rest of the language uses for a field, and
it mixes with the pair form in one brace because the two are told apart
one item at a time -- a dot in head position is the shorthand, anything
else is a pattern expecting its .field next. *)
accepts "struct pattern with the .field shorthand"
(pt ^ "(defn f [p Point] i32 (let [{.x .y} p] (+ x y)))");
accepts "the shorthand mixed with a pair in one brace"
(pt ^ "(defn f [p Point] i32 (let [{.x b .y} p] (+ x b)))");
accepts "the shorthand inside a nested pattern"
(line ^ "(defn f [l Line] i32 (let [{{.x .y} .a} l] (+ x y)))");
(* The shorthand names a field, so an unknown one is the same refusal the
named form gets -- it is the same field access underneath. *)
rejects_check "the shorthand naming a field the struct does not have"
(pt ^ "(defn f [p Point] i32 (let [{.z} p] 0))")
~needle:"Point has no field z";
(* This used to be "{.x} has no .field": a lone dotted symbol was read as a
name to bind and the brace then wanted a field after it. An even number
of them was worse than a refusal -- {.x .y} parsed as "bind a local
called .x to field y" and the program failed later with "unknown name x",
several lines from the mistake. Both readings are gone. *)
accepts "a lone .field is the shorthand and not a missing pair"
(pt ^ "(defn f [p Point] i32 (let [{.x} p] x))");
(* And the arm that refusal came from is still there for the case it was
written for: a plain name with nothing after it. *)
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)))")
~needle:"Point has no field z";
@ -2692,9 +2783,6 @@ let () =
rejects_check "an empty struct pattern"
(pt ^ "(defn f [p Point] i32 (let [{} p] 0))")
~needle:"an empty struct pattern {} binds nothing";
rejects_check "a field name with no pattern before it"
(pt ^ "(defn f [p Point] i32 (let [{.x} p] 0))")
~needle:"has no .field";
rejects_check "a pattern with no field name after it"
(pt ^ "(defn f [p Point] i32 (let [{a b} p] 0))")
~needle:"expected .field after a";

View File

@ -74,10 +74,28 @@
;;;; Each macro answers the value of its `End*` call, which is (). A pair was
;;;; never an expression worth reading anyway.
;; Each guard below asks the same question twice over, and the second half is
;; the one worth explaining. A body that was not written can arrive two ways:
;; as no argument at all — (with-drawing) — and as a single bare () —
;; (with-drawing ()). The second used to slip past, because one argument is one
;; argument however empty it is: the () was spliced into the expansion
;; verbatim, and the report came out of the middle of the expanded (do) saying
;; that () is not an expression, several forms away from the line anyone wrote.
;; () has no value-position meaning in the language at all, so a lone one here
;; is never a body and can be answered with the same message the missing-body
;; case gets. (do) is what to write for a body that really is meant to be
;; empty, and it is an ordinary expression that needs none of this.
;;
;; Only a *lone* () is caught, and only where the body goes. () anywhere else —
;; as a camera, as a render target — is left to fail on its own, because
;; nothing here could say anything truer about it than the compiler already
;; does.
;; The frame. Everything drawn lands on the back buffer; end-drawing swaps it
;; and waits out the frame time set by set-target-fps.
(defmacro with-drawing [args]
(if (< (len args) 1)
(if (or (< (len args) 1)
(and (= (len args) 1) (form-empty-list? (at args 0))))
`(with-drawing-takes-a-body)
`(do (begin-drawing)
~@args
@ -87,7 +105,8 @@
;; always was. Remember that a fresh (Camera2D {}) has zoom 0.0 and is not
;; usable as an identity — raylib.flan says so beside the struct.
(defmacro with-mode-2d [args]
(if (< (len args) 2)
(if (or (< (len args) 2)
(and (= (len args) 2) (form-empty-list? (at args 1))))
`(with-mode-2d-takes-a-camera-and-a-body)
`(do (begin-mode-2d ~(at args 0))
~@(form-rest args 1)
@ -97,7 +116,8 @@
;; more here than anywhere: ending a 3D mode with end-mode-2d type-checks
;; fine and leaves the projection matrix wrong for everything after it.
(defmacro with-mode-3d [args]
(if (< (len args) 2)
(if (or (< (len args) 2)
(and (= (len args) 2) (form-empty-list? (at args 1))))
`(with-mode-3d-takes-a-camera-and-a-body)
`(do (begin-mode-3d ~(at args 0))
~@(form-rest args 1)
@ -108,7 +128,8 @@
;; that correction is the caller's and is deliberately not hidden here, since
;; it belongs with the draw and not with the mode.
(defmacro with-texture-mode [args]
(if (< (len args) 2)
(if (or (< (len args) 2)
(and (= (len args) 2) (form-empty-list? (at args 1))))
`(with-texture-mode-takes-a-target-and-a-body)
`(do (begin-texture-mode ~(at args 0))
~@(form-rest args 1)
@ -118,7 +139,8 @@
;; scalars rather than a Rectangle, because that is what BeginScissorMode
;; takes and this file is not the place to invent a second spelling.
(defmacro with-scissor-mode [args]
(if (< (len args) 5)
(if (or (< (len args) 5)
(and (= (len args) 5) (form-empty-list? (at args 4))))
`(with-scissor-mode-takes-x-y-width-height-and-a-body)
`(do (begin-scissor-mode ~(at args 0) ~(at args 1) ~(at args 2) ~(at args 3))
~@(form-rest args 4)