The parameter list, its tests, and the note about what it breaks

This commit is contained in:
Joseph Ferano 2026-09-20 18:21:54 +07:00
parent 69646e534e
commit eeac54a8e3
3 changed files with 112 additions and 15 deletions

66
FIX.org
View File

@ -1611,3 +1611,69 @@ because the code is the same.
Not fixed here, deliberately: the fix is to catch ~Closed~ in that poll and
read it as the program having ended, which is a claim about what those rows
mean and belongs to whoever owns them. Flagged rather than patched.
* A macro's parameter list, and the one breaking spelling, 2026-09-20
DISCUSS.org's "defmacro should support real parameter lists" is built.
=(defmacro do-grid [[r rows c cols] & body] ...)= — positional parameters, a
=[ ]= pattern wherever the argument is a vector, nesting, and =&= for the
tail. The list is read in lib/expand.ml (=params_of=, =check_call=), turned
into bindings by lib/parse.ml (=macro_body=) and checked against a call by
lib/macro.ml (=checked_call=) before anything is expanded.
** THE BREAKING CHANGE: [args] was the whole call, and is now the first argument
This is the one decision in the lane that changes what existing text means,
and it is here rather than in a commit message because it is the thing to
disagree with if it is wrong.
A macro's single parameter *was* the whole argument list, so =[args]= meant
"everything written at the call". Under a positional parameter list it cannot
keep meaning that: one named parameter has to be the first argument, the way
it is in every other language with parameter lists and the way Clojure has
it. So the whole list is now spelled =[& args]=.
The alternative was a legacy mode — one parameter with no =&= keeps the old
meaning — and it was refused. It makes =[a]= and =[a b]= mean unrelated
things, which is the kind of rule nobody can hold in their head, and it
would have left the corpus written in a grammar the documentation no longer
describes.
So every =defmacro= in the tree was migrated in the same commit. Seventeen
files, mechanical, bodies untouched:
- lib/prelude.ml — =clamp=, =unless=, =into=
- vendor/raylib/modes.flan — =with-drawing=, =with-mode-2d=, =with-mode-3d=,
=with-texture-mode=, =with-scissor-mode=
- vendor/edn/provide.flan — =defedn=; vendor/json/provide.flan — =defjson=
- test/programs/ — macros.flan (7), macro-cycle.flan (2), macro-spin.flan,
pkg-macro.flan, printers.flan, pkgs/mac (6), pkgs/macring (2),
pkgs/macspin (1)
- test/test_dev.ml, test_flan.ml, test_repl.ml, test_session.ml and
emacs/test-flan.el — the =defmacro= fixtures written as strings
Nothing was rewritten to *use* the new grammar as part of the migration —
=with-mode-2d= is still =[& args]= picking its camera out by hand, and its
hand-written arity guard still says what it said. That was deliberate: the
migration had to be a spelling change or it proves nothing. The new grammar
is shown off in test/programs/macro-params.flan, which is its own program
beside macros.flan.
The equivalence is asserted rather than assumed. pkg-macro.flan declares
=tenfold= (=[& args]=, =(at args 0)=) and =tenfold-listed= (=[n]=) with the
same body, and test_session expands both and requires the same text.
** Map destructuring in a macro's parameter list — deferred, refused by name
=dmap= (lib/parse.ml) is ={:keys [x y]}= over a *struct*: it reads field
names off a declared type. A macro's argument is a =Form=, whose =Map= case
is a flat run of alternating forms with no field names anywhere in it. So
the pattern cannot be translated — it would have to be given a new meaning
(match a keyword key in the literal map written at the call? bind by
position?), and none of those is obviously the one somebody wants.
Vectors and =&= are the 95% case and are built. A map pattern in a macro's
parameter list is refused by name where it is written:
map destructuring is not implemented in a macro's parameter list — a
macro's argument is a Form, whose Map case is a flat run of alternating
forms with no fields to name. Take the form and pick it apart in the body
Pinned in test_flan.ml. Whoever wants it should decide what it means first.

View File

@ -3075,16 +3075,49 @@ it and loading it into the compiler's own process.** There is nothing to interpr
be, so `Emit.redefinition``Build.shared``dlopen`, the reload primitive the dev loop already runs, is pointed at
the compiler instead of at a running program.
`(defmacro name [args] body ...)` is one function, `[Form] -> Form`. One parameter, the slice of forms written at the
call site, which is where variadics come from in a language with no `&rest`: `(len args)` is how many were written.
`(defmacro name [param ...] body ...)` is one function, `[Form] -> Form`. The *declared* parameter is one and always
has been — the slice of forms written at the call site — but the author writes a real parameter list against it:
positional names, a `[ ]` pattern wherever the argument is a vector, and `&` for the tail. `(defmacro do-grid
[[r rows c cols] & body] ...)` is Clojure's shape and it binds six names out of one slice.
### `[args]` binds the first argument; `[& args]` binds the whole call
This is the one breaking change the feature carried and it is worth stating twice. Before parameter lists a macro's
single parameter *was* the whole argument list, so `[args]` meant "everything". Under the list it is positional like
every other name, so `[args]` means "the first argument" and the whole list is spelled `[& args]`. Every `defmacro` in
the tree — the prelude's three, raylib's five, edn's and json's providers, every test fixture — was migrated to
`[& args]` when this landed. There is one grammar and no legacy mode: `[& args]` is the trivial case of the list.
### Arity and destructuring are refused at the call, before expansion
`Expand.params_of` reads the list and `Expand.check_call` measures a call against it. `Macro.checked_call` runs that
check before `Expand.call`, on all four ways into an expansion — the walk, `settle`'s re-expansion, and the editor's
`expand_step` and `expand_all` — so C-c C-m refuses what a build refuses, in the same words.
Before expansion is the whole point. Every node a macro returns is stamped with the call site's `Loc.t` (see `Form`
below), which is a documented limitation waiting on the structured-error rewrite; a refusal raised *before* the macro
runs has the call's own location and its own column, with no stamping involved. The four shapes are: too few
arguments, too many (when there is no `&`), a `[ ]` pattern meeting a form that is not a vector, and a vector of the
wrong length for its pattern. `test/programs/macro-arity.flan` and the three beside it pin each one.
Map destructuring is **not** in a macro's parameter list, and it is a deferral rather than an oversight: `dmap` is
`{:keys [x y]}` over a *struct*, and a macro's argument is a `Form` whose `Map` case is a flat run of alternating forms
with no field names in it at all. The pattern would have to mean something new. Refused by name, written down in
FIX.org.
### A defmacro is a defn, and there is no Ast.Defmacro
`Parse` turns `(defmacro m [args] body)` into `(defn m [args [Form]] Form body)` and nothing below the parser knows
`Parse` turns `(defmacro m [a & rest] body)` into `(defn m [macro~args [Form]] Form (let [a (at macro~args 0) rest
(form-rest macro~args 1)] body))` and nothing below the parser knows
the word exists. The checker checks it like any function, the backend emits it like any function, `Reach.link` drops
it from a program that does not call it like any function. The only thing that makes it a macro is that `Macro` calls
it at compile time instead of the program calling it at run time.
The parameter list goes the same way: it is bindings over that one slice and nothing downstream learns a pattern
existed, exactly as nothing downstream learns a `let` had one. `macro~args` is the compiler's own name for the slice,
with a `~` in it for `gensym`'s reason — the reader cannot put that character in a symbol, so no name an author writes
collides with it. The generated extraction is unchecked, because `check_call` already counted the call.
This is also why there is no macro table. Storage was the question the front half deliberately left open, and the
answer is that there is none: the macro set is recomputed by scanning the top level for the word `defmacro`, which is
the only place it survives, and the compiled artefact is a `.so` keyed by a digest. The top level scanned is the

View File

@ -3355,14 +3355,8 @@ level "1"
Three opt levels for the reason macros.flan has them, and the dev row
because the dev path is this project's priority. *)
let macro_params_out =
"000102101112
42
123 nested
4 and more
alone!
with body!
true true false
"
"000102101112\n42\n123 nested\n4 and more\nalone!\nwith body!\n\
true true false\n"
in
outputs "a macro's parameter list" "programs/macro-params.flan"
macro_params_out;
@ -3502,16 +3496,20 @@ with body!
that stamping is a documented limitation waiting on the structured-error
rewrite, and these four are the part of it that does not have to wait. *)
refuses "a macro call with too few arguments" "programs/macro-arity.flan"
"do-grid takes at least 1 argument and this call gives 0 — its parameter list is [[r rows c cols] & body], where &body is the rest";
"do-grid takes at least 1 argument and this call gives 0 — its \
parameter list is [[r rows c cols] & body], where &body is the rest";
refuses "a macro call with too many arguments"
"programs/macro-arity-extra.flan"
"pair takes 2 arguments and this call gives 3 — its parameter list is [a b]";
"pair takes 2 arguments and this call gives 3 — its parameter list is \
[a b]";
refuses "a destructuring parameter meeting a form that is not a vector"
"programs/macro-destructure.flan"
"do-grid destructures this argument with [r rows c cols], so a [ ] belongs here and 7 was written";
"do-grid destructures this argument with [r rows c cols], so a [ ] \
belongs here and 7 was written";
refuses "a destructuring parameter meeting a vector of the wrong length"
"programs/macro-destructure-arity.flan"
"do-grid destructures this argument with [r rows c cols], which takes 4, and 3 are written here";
"do-grid destructures this argument with [r rows c cols], which takes 4, \
and 3 are written here";
refuses "a macro that does not settle" "programs/macro-spin.flan"
"did not settle after";