flan/HANDOFF-macros.md
Joseph Ferano 1898a3157d Macros come from a package now, and the refusal's reason was wrong
Load.program takes forms: it reads the import forms, resolves them with the
one resolver it always had, and parses the file with the packages' macros in
front of it. The refusal said this needed a second import resolver at the Form
level. It did not notice that the file being compiled is parsed before Load
runs too, so no shape of the feature could have left import resolution where
it was.

Names arrive qualified, as a defn's do. (mac/twice 4) is a call and (twice 4)
is an unknown name.

Stopped mid-task: dune test was never run and the acceptance wiring is
unfinished. HANDOFF-macros.md has what is left.
2026-09-13 15:40:08 +07:00

4.2 KiB

Handoff: macros in an imported package

Stopped mid-task when the session ended. The feature works; the acceptance wiring is unfinished. Its author's last words were "Works. Now the acceptance wiring." This file was written by the coordinator from the diff and from running the program, not by the lane, so treat the "what remains" list as read off the tree rather than as the author's own account.

State

dune build succeeds. test/programs/pkg-macro.flan runs and prints 8 12 10 12 10 8 70 60, which is a package macro, a package macro that quasiquotes another, and a program macro, all coexisting. dune test was never run. That is the first thing to do.

The shape picked, and why the refusal's reasoning was wrong

The old refusal in lib/load.ml said collecting a package's macros would need that package's imports resolved at the Form level, before Load runs — a second import resolver — and refused rather than build one.

The lane found the premise false, and this is the finding worth keeping:

the file being compiled is parsed before Load runs too, so no shape of the feature could have left import resolution where it was.

So the phases moved instead of being duplicated. Load.program takes forms now: it reads the import forms, resolves them with the one resolver it always had, and parses the file with the packages' macros already in front of it. There is no second resolver.

The acyclic-import guarantee is what makes this sound, and it was already there for this reason — load.ml's own comment says a definite package order is what the macro expander needs.

The rule

Package macros arrive qualified, exactly as a defn does. A program importing the directory as mac writes (mac/twice 4); (twice 4) is an unknown name. Nothing becomes globally visible by importing a package. Inside the package the names are unqualified, which is the rule every other declaration there follows.

Files touched

bin/main.ml, lib/load.ml, lib/macro.ml, lib/parse.ml, lib/session.ml, test/programs/pkg-macro.flan, test/programs/pkgs/mac/mac.flan, test/test_acceptance.ml, test/test_reload.ml, test/test_sanitize.ml, test/test_session.ml, test/test_valgrind.ml, test/test_web.ml. 447 insertions, 117 deletions.

The test files are mostly signature churn from Load.program taking forms — that call is made in many places and each had to move.

What remains

  1. Run dune test --root . and make it green. Nothing has run it. Load.program's signature changed and it is called from most test binaries, so expect breakage that is mechanical rather than deep.
  2. Finish the acceptance wiring. test_acceptance.ml:1423-1427 was the refusal case (refuses "a macro in an imported package"). The diff touches it; confirm it now asserts the working program and that a sibling refusal pins the qualified-name rule — that (twice 4) unqualified is an unknown name. pkg-macro.flan's header says that refusal exists; verify it does.
  3. Check the non-termination refusals still fire when the macros come from a package: a ring of macros is named, a macro that does not settle is bounded. BUILT.md records both. Neither was confirmed under the new path.
  4. Check the dev loop. A macro imported by the file being edited must still be available on a C-c C-c. lib/session.ml is in the diff, so this was at least considered, but nothing proves it. test/test_session.ml and test/test_dev.ml show how sessions are driven.
  5. Measure the build cost against the recorded baseline in BUILT.md: 50ms for a build naming no macro, 310ms cold and 70ms warm for one calling a macro. Whether reading a package's forms earlier moved those numbers is unknown.
  6. vendor/raylib is the customer. with-drawing and with-mode-2d over raylib's BeginDrawing/EndDrawing and BeginMode2D/EndMode2D, so an unbalanced pair stops being possible. A raylib lane tried and could not; that is what prompted this work. Not started here and deliberately out of scope — that file was held by another lane at the time.

Not verified

Everything above item 1. The program runs and the build is clean; that is the whole of what is known to work.