diff --git a/FIX.org b/FIX.org index b1fbe41..35077d2 100644 --- a/FIX.org +++ b/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. diff --git a/docs/BUILT.md b/docs/BUILT.md index 3dad02a..c4685c1 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -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 diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 838014e..56b8dd4 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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";