The delimiter half of the backtick fix was observed by nothing

Every sigil in the corpus test sat in leading position, where read_form
handles it before is_delimiter is ever consulted — so reverting the
is_delimiter line alone left every case green. a`b now has the case a~b
already had, and the corpus carries both, which is what makes the class
guard cover the delimiter change rather than only the read branches.

The handoff note hedged on the one thing it exists to decide: expansion
runs over Form before Parse, not over Ast. There is no Ast.Defmacro, so
an Ast pass would have nothing to read. Says milestone 6 for union
values because that is the number check.ml itself gives.
This commit is contained in:
Joseph Ferano 2026-09-11 20:20:24 +07:00
parent f590436ed3
commit a311664a08
2 changed files with 11 additions and 9 deletions

17
NEXT.md
View File

@ -1621,14 +1621,15 @@ The shape it wants:
accessors. That is the real work, and it is bigger than the expander itself:
the compiler and the compiled macro have to agree on the *layout* of a
`Form`, not merely its shape, so whatever the checker does for unions has to
be exact here. Until unions are values this cannot start; that is milestone 6
work landing before milestone 5's.
2. **Expansion is a pass between `Parse` and `Check`**, over `Ast`, not over
`Form` — or, better, over `Form` before `Parse` ever runs, which is why
`Parse` refusing `defmacro` rather than storing it is not a dead end: the
expander runs first and `Parse` never sees a macro call at all. That is the
Clojure ordering and the one to prefer, because a macro expanding to a
special form is then ordinary rather than a special case.
be exact here. Until unions are values this cannot start — `check.ml` puts
union values and `match` on a union at **milestone 6**, so that is milestone
6 work landing before milestone 5's.
2. **Expansion runs over `Form`, before `Parse`.** Not a pass over `Ast`:
there is no `Ast.Defmacro` and `Parse` refuses `defmacro` outright, so an
`Ast`-level pass would have nothing to work with. That refusal is not a dead
end, it is the ordering — the expander runs first and `Parse` never sees a
macro call at all. It is also the Clojure ordering, and the reason a macro
expanding to a special form is ordinary rather than a special case.
3. **Order matters and files do not have one.** Top-level names in a package
are order-independent everywhere else (`declared_types`, the constant
fixpoint in `check.ml`). Macros cannot be: a macro must be compiled and

View File

@ -113,6 +113,7 @@ let () =
reads "quote in quasi" "`(a 'b)" "(quasiquote (a (quote b)))";
(* The delimiter half of the fix: without it ~x is one symbol named "~x". *)
reads "tilde ends a name" "(f a~b)" "(f a (unquote b))";
reads "backtick ends a name" "(f a`b)" "(f a (quasiquote b))";
reads "backtick in vec" "[`a ~b]" "[(quasiquote a) (unquote b)]";
(* The whole class: no reader-significant character may end up inside a name. *)
@ -127,7 +128,7 @@ let () =
in
let corpus =
"(invoke-restart 'skip-form) (a 'b [c 'd] {:e 'f}) '(g 'h) \
`(i ~j ~@k) `(l `(m ~n)) [`o ~p] {:q `r}"
`(i ~j ~@k) `(l `(m ~n)) [`o ~p] {:q `r} (f a~b x`y)"
in
check "no sigils leak into names"
(bad_names (Form.make (Form.List (Reader.read_all ~file:"<test>" corpus))