Six dogfooding items: empty bodies, comment, inc/dec, () bodies, type limits, {.field}

This commit is contained in:
Joseph Ferano 2026-09-20 18:02:59 +07:00
parent dc39631db7
commit 6c017c2cf8
7 changed files with 526 additions and 14 deletions

View File

@ -348,9 +348,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 ...)")
@ -755,11 +761,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,
@ -770,8 +792,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
@ -1960,11 +2034,91 @@ let source = {flan|
;; 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.
;;
;; 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

@ -3426,6 +3426,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
@ -4240,6 +4263,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;
@ -4256,7 +4317,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

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