The parameter list, its tests, and the note about what it breaks
This commit is contained in:
parent
69646e534e
commit
eeac54a8e3
66
FIX.org
66
FIX.org
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user