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
|
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
|
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.
|
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
|
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.
|
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
|
`(defmacro name [param ...] body ...)` is one function, `[Form] -> Form`. The *declared* parameter is one and always
|
||||||
call site, which is where variadics come from in a language with no `&rest`: `(len args)` is how many were written.
|
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
|
### 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
|
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 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.
|
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
|
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
|
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
|
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
|
Three opt levels for the reason macros.flan has them, and the dev row
|
||||||
because the dev path is this project's priority. *)
|
because the dev path is this project's priority. *)
|
||||||
let macro_params_out =
|
let macro_params_out =
|
||||||
"000102101112
|
"000102101112\n42\n123 nested\n4 and more\nalone!\nwith body!\n\
|
||||||
42
|
true true false\n"
|
||||||
123 nested
|
|
||||||
4 and more
|
|
||||||
alone!
|
|
||||||
with body!
|
|
||||||
true true false
|
|
||||||
"
|
|
||||||
in
|
in
|
||||||
outputs "a macro's parameter list" "programs/macro-params.flan"
|
outputs "a macro's parameter list" "programs/macro-params.flan"
|
||||||
macro_params_out;
|
macro_params_out;
|
||||||
@ -3502,16 +3496,20 @@ with body!
|
|||||||
that stamping is a documented limitation waiting on the structured-error
|
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. *)
|
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"
|
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"
|
refuses "a macro call with too many arguments"
|
||||||
"programs/macro-arity-extra.flan"
|
"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"
|
refuses "a destructuring parameter meeting a form that is not a vector"
|
||||||
"programs/macro-destructure.flan"
|
"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"
|
refuses "a destructuring parameter meeting a vector of the wrong length"
|
||||||
"programs/macro-destructure-arity.flan"
|
"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"
|
refuses "a macro that does not settle" "programs/macro-spin.flan"
|
||||||
"did not settle after";
|
"did not settle after";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user