diff --git a/lib/load.ml b/lib/load.ml index 17b5e1d..f02584d 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -645,7 +645,36 @@ let rec import ~seen ~open_ ~loc alias dir = let files = if one_file then [ dir ] else entries dir ".flan" in if files = [] then fail loc "the package at %s has no .flan file" dir; let ds = - List.concat_map (fun f -> Parse.program (Reader.read_file f)) files + List.concat_map + (fun f -> + let forms = Reader.read_file f in + (* A defmacro in a package is refused by name, and here is the only + place that can see one: by the time [Parse] is finished, a + defmacro is an ordinary [Ast.Defn] and the word is gone. + + It is a real gap and not an oversight. The expander collects + macros from the prelude and from the file being compiled; to + collect them from a package it would have to resolve that + package's own imports first, at the Form level, before this + function -- which is a second import resolver. The refusal says + that rather than letting the call arrive at the checker as an + unknown name. *) + List.iter + (fun (form : Form.t) -> + match form.Form.v with + | Form.List ({ Form.v = Form.Sym "defmacro"; _ } + :: { Form.v = Form.Sym n; _ } :: _) -> + Loc.fail form.Form.loc + "%s is a macro, and macros are not imported yet. A \ + defmacro has to be compiled before the call it expands, \ + and the expander collects them from the prelude and from \ + the file being compiled -- not from a package, whose own \ + imports would have to be resolved first. Move it into \ + the file that calls it" n + | _ -> ()) + forms; + Parse.program forms) + files in (* [main] is the importer's, always. A package that called its own would get the importer's instead — silently, since the name still resolves — diff --git a/test/dune b/test/dune index 4d7a689..8240929 100644 --- a/test/dune +++ b/test/dune @@ -41,6 +41,8 @@ (glob_files programs/pkgs/ring-a/*) (glob_files programs/pkgs/ring-b/*) (glob_files programs/pkgs/ring-c/*) + ; The package that declares a macro, which a package may not do yet. + (glob_files programs/pkgs/mac/*) ; The synthetic C header the importer's table reads. Committed rather than ; reached for on the machine: the raylib case needs raylib installed, at the ; right version, with a variable set, so it skips everywhere and covers diff --git a/test/programs/pkg-macro.flan b/test/programs/pkg-macro.flan new file mode 100644 index 0000000..5dd6cfb --- /dev/null +++ b/test/programs/pkg-macro.flan @@ -0,0 +1,14 @@ +;;;; A macro in an imported package. +;;;; +;;;; The expander collects defmacros from the prelude and from the file being +;;;; compiled. Collecting them from a package would mean resolving that +;;;; package's own imports at the Form level, before Load runs -- a second +;;;; import resolver -- so it does not, and says so. Left alone the call would +;;;; arrive at the checker as an unknown name, which is the failure shape this +;;;; codebase refuses to ship. Never built: the refusal is the test. + +(import mac "pkgs/mac") + +(defn main [] i32 + (print (mac/double 4)) + 0) diff --git a/test/programs/pkgs/mac/mac.flan b/test/programs/pkgs/mac/mac.flan new file mode 100644 index 0000000..9bcd330 --- /dev/null +++ b/test/programs/pkgs/mac/mac.flan @@ -0,0 +1,7 @@ +;;;; A package that declares a macro, which is a thing a package may not do +;;;; yet. The refusal is the test; this is never built. + +(defmacro twice [args] + `(+ ~(at args 0) ~(at args 0))) + +(defn double [n i32] i32 (* n 2)) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index c19d2f3..37db25f 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -1129,6 +1129,12 @@ let () = refusal that says only "there is a cycle" leaves them to find it. The ring is a -> b -> c -> a, and the message closes it by repeating the package it came back to. *) + (* A package may not declare a macro yet, and the reason is the ordering: + collecting one would mean resolving that package's own imports over + Forms, before Load runs. Refused where the defmacro is written rather + than where it is called, because that is where the fix goes. *) + refuses "a macro in an imported package" "programs/pkg-macro.flan" + "macros are not imported yet"; refuses "an import ring" "programs/pkg-cycle.flan" "round a ring: a -> b -> c -> a"; refuses "two mains in one program" "programs/pkg-two-mains.flan"