The dogfood batch: empty forms, comment, inc and dec, guards, limits, shorthand
# Conflicts: # FIX.org
This commit is contained in:
commit
e807986622
232
FIX.org
232
FIX.org
@ -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
|
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
|
allocates one small dyn map and nothing else, so there is nothing for ASan to
|
||||||
find that ~dyn-map.flan~ does not already exercise.
|
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.
|
||||||
|
|||||||
11
lib/check.ml
11
lib/check.ml
@ -2939,6 +2939,17 @@ and check_fn ctx ~want loc (params : string list) body =
|
|||||||
answer, and it has to be the declared return type. *)
|
answer, and it has to be the declared return type. *)
|
||||||
let fbody =
|
let fbody =
|
||||||
match List.rev fbody with
|
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
|
| [] -> fbody
|
||||||
| last :: rest ->
|
| last :: rest ->
|
||||||
List.rev (expect fctx last.Tast.loc ~want:(Some ret) last :: rest)
|
List.rev (expect fctx last.Tast.loc ~want:(Some ret) last :: rest)
|
||||||
|
|||||||
51
lib/parse.ml
51
lib/parse.ml
@ -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)")
|
| _ -> fail f "if is (if test then) or (if test then else)")
|
||||||
|
|
||||||
(* Sugar, desugared here: special forms until macros land at milestone 5. *)
|
(* 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" ->
|
| Sym "when" ->
|
||||||
(match args with
|
(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))
|
mk (Ast.If (expr c, { Ast.e = Ast.Do (body_of body); loc = f.loc }, None))
|
||||||
| _ -> fail f "when is (when test body ...)")
|
| _ -> 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 ─────────────────────────── *)
|
(* ── binding and control: never a call ─────────────────────────── *)
|
||||||
(* A form that binds a name or alters control flow cannot fall through to
|
(* 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. *)
|
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" ->
|
| Sym "fn" ->
|
||||||
(match args with
|
(match args with
|
||||||
| { v = Vec ps; _ } :: body when body <> [] ->
|
| { v = Vec ps; _ } :: body ->
|
||||||
List.iter no_pattern ps;
|
List.iter no_pattern ps;
|
||||||
mk (Ast.Fn (List.map sym ps, body_of body))
|
mk (Ast.Fn (List.map sym ps, body_of body))
|
||||||
| _ -> fail f "fn is (fn [param ...] 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"
|
{:keys [x y]} over a struct or [a b] over a fixed array"
|
||||||
(Form.to_string p)
|
(Form.to_string p)
|
||||||
|
|
||||||
(* {:keys [x y]} and {inner .field}, over a struct. Clojure's map destructuring
|
(* {:keys [x y]}, {.x .y} and {inner .field}, over a struct. Clojure's map
|
||||||
with Flan's structs standing in for its maps: [:keys] is the common case and
|
destructuring with Flan's structs standing in for its maps: [:keys] is the
|
||||||
the pair form is what nests, since a [:keys] entry is a name and never a
|
common case and the pair form is what nests, since a [:keys] entry is a name
|
||||||
pattern. Everything else Clojure puts in this position — [:as], [:or],
|
and never a pattern. Everything else Clojure puts in this position — [:as],
|
||||||
[:strs], [:syms] — is refused by name where it is written.
|
[: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
|
[: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,
|
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 =
|
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 ex loc e : Ast.expr = { Ast.e; loc } in
|
||||||
let field loc name = ex loc (Ast.Field (t, name)) 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
|
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 ->
|
| { v = Kw "keys"; _ } :: names :: rest ->
|
||||||
let ns =
|
let ns =
|
||||||
match names.v with
|
match names.v with
|
||||||
|
|||||||
174
lib/prelude.ml
174
lib/prelude.ml
@ -553,6 +553,80 @@ let source = {flan|
|
|||||||
(set i (+ i 1)))
|
(set i (+ i 1)))
|
||||||
(if neg (Some (- 0 n)) (Some n))))
|
(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 ───────────────────────────────────────────────────────────
|
;; ── Numbers ───────────────────────────────────────────────────────────
|
||||||
;;
|
;;
|
||||||
;; Only the ones that encode a decision. abs is (max x (- 0 x)); a wrapper over
|
;; 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
|
;; 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:
|
||||||
|
;; (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]
|
(defmacro unless [args]
|
||||||
(if (< (len args) 2)
|
(if (< (len args) 1)
|
||||||
`(unless-takes-a-test-and-a-body)
|
`(unless-takes-a-test)
|
||||||
`(if (not ~(at args 0)) (do ~@(form-rest args 1)))))
|
`(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: a fused transformation, and not a transducer ────────────────
|
||||||
;;
|
;;
|
||||||
;; (into xs (vec-new i32) (map double) (filter even?))
|
;; (into xs (vec-new i32) (map double) (filter even?))
|
||||||
@ -2049,6 +2203,16 @@ let source = {flan|
|
|||||||
(Form.Sym s) (bytes=? (bytes s) (bytes name))
|
(Form.Sym s) (bytes=? (bytes s) (bytes name))
|
||||||
_ false))
|
_ 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
|
(defn form-is-sym? [f Form] bool
|
||||||
(match f
|
(match f
|
||||||
(Form.Sym s) true
|
(Form.Sym s) true
|
||||||
|
|||||||
@ -40,6 +40,16 @@
|
|||||||
(let [{a .x b .y} (Point {.x 10 .y 20})]
|
(let [{a .x b .y} (Point {.x 10 .y 20})]
|
||||||
(show2 "pairs" a b))
|
(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 [l (Line {.a (Point {.x 5 .y 6}) .b (Point {.x 7 .y 8})})]
|
||||||
(let [{{:keys [x y]} .b} l]
|
(let [{{:keys [x y]} .b} l]
|
||||||
(show2 "nested" x y))
|
(show2 "nested" x y))
|
||||||
|
|||||||
121
test/programs/limits.flan
Normal file
121
test/programs/limits.flan
Normal 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)
|
||||||
103
test/programs/prelude-macros.flan
Normal file
103
test/programs/prelude-macros.flan
Normal 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)
|
||||||
21
test/programs/rl-with-empty-arg.flan
Normal file
21
test/programs/rl-with-empty-arg.flan
Normal 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)
|
||||||
21
test/programs/rl-with-empty.flan
Normal file
21
test/programs/rl-with-empty.flan
Normal 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)
|
||||||
@ -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
|
||||||
@ -3426,6 +3439,29 @@ level "1"
|
|||||||
outputs ~opt:"-O0" "unless, now a prelude macro, -O0"
|
outputs ~opt:"-O0" "unless, now a prelude macro, -O0"
|
||||||
"programs/macro-unless.flan" unless_out;
|
"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
|
(* 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
|
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
|
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"
|
print_endline "FAIL the report does not say which macro"
|
||||||
end);
|
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
|
(* The two ways expansion does not terminate, and they are different
|
||||||
failures. A ring is a compile-order problem -- each body calls the other
|
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
|
while the other is being compiled -- and there is no order, so it is
|
||||||
@ -4259,6 +4331,44 @@ level "1"
|
|||||||
incr failures;
|
incr failures;
|
||||||
Printf.printf "FAIL %s\n refused: %S\n" name m);
|
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
|
let signed_out = "-4\n-1\nbig is not small\nbig is large\n1\n" in
|
||||||
outputs "signedness" "programs/signedness.flan" signed_out;
|
outputs "signedness" "programs/signedness.flan" signed_out;
|
||||||
outputs ~opt:"-O0" "signedness, -O0" "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
|
tail slice is an address into a local array, and mem2reg launders a
|
||||||
sloppy one. *)
|
sloppy one. *)
|
||||||
let destructure_out =
|
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\
|
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"
|
struct-tail 1 2 4 5\ncalls 2 14\n"
|
||||||
in
|
in
|
||||||
|
|||||||
@ -314,6 +314,34 @@ let () =
|
|||||||
| If (_, { e = Do [ _; _ ]; _ }, None) -> ()
|
| If (_, { e = Do [ _; _ ]; _ }, None) -> ()
|
||||||
| _ -> check "when -> if+do" false);
|
| _ -> 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,
|
(* 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
|
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
|
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)]))")
|
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [] 2)]))")
|
||||||
~needle:"a handler-case clause is (Type [name] body ...)";
|
~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 ─────────────────────────────────────────────── *)
|
(* ── Destructuring ─────────────────────────────────────────────── *)
|
||||||
|
|
||||||
(* A pattern is desugared in [Parse] into the bindings and field accesses that
|
(* 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\
|
(pt ^ "(defn mk [] Point (Point {.x 1 .y 2}))\n\
|
||||||
(defn f [] i32 (let [{:keys [x y]} (mk)] (+ x y)))");
|
(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"
|
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)))")
|
||||||
~needle:"Point has no field z";
|
~needle:"Point has no field z";
|
||||||
@ -2692,9 +2783,6 @@ let () =
|
|||||||
rejects_check "an empty struct pattern"
|
rejects_check "an empty struct pattern"
|
||||||
(pt ^ "(defn f [p Point] i32 (let [{} p] 0))")
|
(pt ^ "(defn f [p Point] i32 (let [{} p] 0))")
|
||||||
~needle:"an empty struct pattern {} binds nothing";
|
~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"
|
rejects_check "a pattern with no field name after it"
|
||||||
(pt ^ "(defn f [p Point] i32 (let [{a b} p] 0))")
|
(pt ^ "(defn f [p Point] i32 (let [{a b} p] 0))")
|
||||||
~needle:"expected .field after a";
|
~needle:"expected .field after a";
|
||||||
|
|||||||
32
vendor/raylib/modes.flan
vendored
32
vendor/raylib/modes.flan
vendored
@ -74,10 +74,28 @@
|
|||||||
;;;; Each macro answers the value of its `End*` call, which is (). A pair was
|
;;;; Each macro answers the value of its `End*` call, which is (). A pair was
|
||||||
;;;; never an expression worth reading anyway.
|
;;;; 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
|
;; The frame. Everything drawn lands on the back buffer; end-drawing swaps it
|
||||||
;; and waits out the frame time set by set-target-fps.
|
;; and waits out the frame time set by set-target-fps.
|
||||||
(defmacro with-drawing [args]
|
(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)
|
`(with-drawing-takes-a-body)
|
||||||
`(do (begin-drawing)
|
`(do (begin-drawing)
|
||||||
~@args
|
~@args
|
||||||
@ -87,7 +105,8 @@
|
|||||||
;; always was. Remember that a fresh (Camera2D {}) has zoom 0.0 and is not
|
;; 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.
|
;; usable as an identity — raylib.flan says so beside the struct.
|
||||||
(defmacro with-mode-2d [args]
|
(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)
|
`(with-mode-2d-takes-a-camera-and-a-body)
|
||||||
`(do (begin-mode-2d ~(at args 0))
|
`(do (begin-mode-2d ~(at args 0))
|
||||||
~@(form-rest args 1)
|
~@(form-rest args 1)
|
||||||
@ -97,7 +116,8 @@
|
|||||||
;; more here than anywhere: ending a 3D mode with end-mode-2d type-checks
|
;; 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.
|
;; fine and leaves the projection matrix wrong for everything after it.
|
||||||
(defmacro with-mode-3d [args]
|
(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)
|
`(with-mode-3d-takes-a-camera-and-a-body)
|
||||||
`(do (begin-mode-3d ~(at args 0))
|
`(do (begin-mode-3d ~(at args 0))
|
||||||
~@(form-rest args 1)
|
~@(form-rest args 1)
|
||||||
@ -108,7 +128,8 @@
|
|||||||
;; that correction is the caller's and is deliberately not hidden here, since
|
;; that correction is the caller's and is deliberately not hidden here, since
|
||||||
;; it belongs with the draw and not with the mode.
|
;; it belongs with the draw and not with the mode.
|
||||||
(defmacro with-texture-mode [args]
|
(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)
|
`(with-texture-mode-takes-a-target-and-a-body)
|
||||||
`(do (begin-texture-mode ~(at args 0))
|
`(do (begin-texture-mode ~(at args 0))
|
||||||
~@(form-rest args 1)
|
~@(form-rest args 1)
|
||||||
@ -118,7 +139,8 @@
|
|||||||
;; scalars rather than a Rectangle, because that is what BeginScissorMode
|
;; scalars rather than a Rectangle, because that is what BeginScissorMode
|
||||||
;; takes and this file is not the place to invent a second spelling.
|
;; takes and this file is not the place to invent a second spelling.
|
||||||
(defmacro with-scissor-mode [args]
|
(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)
|
`(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))
|
`(do (begin-scissor-mode ~(at args 0) ~(at args 1) ~(at args 2) ~(at args 3))
|
||||||
~@(form-rest args 4)
|
~@(form-rest args 4)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user