A package may not declare a macro, and says so
The expander collects defmacros from the prelude and from the file being
compiled. Not from an imported package, and the reason is an ordering one:
Load learns a package's imports by parsing it, so reaching a package's macros
would mean resolving that package's own imports over Forms, before Load runs.
That is a second import resolver, and it is a bigger thing than this lane.
Refused by name, which is the rule that caught the two misparse bugs. Left
alone the call arrives at the checker as an unknown name -- true, and no help.
Refused where the defmacro is written rather than where it is called, because
that is where the fix goes.
The check has to sit in Load's read, because that 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.
Measured while here, since a prelude that grows a defmacro is a cost every
program pays or does not:
- A build of a program that names no macro: 50ms, the same as before. The
pass scans the top level, finds nothing, and no compiler runs.
- A program that calls one: 310ms the first time, 70ms after. The 240ms is
the clang driver building the macro module; it is cached under the object
cache, keyed by the prelude's source and the file's defmacros, so it is
paid once per change rather than once per build.
- A hello-world's binary carries exactly one symbol out of all of this:
flan.gensym-n, eight bytes. Reach.link drops unless, form-cons, form-nil,
form-append, form-rest and gensym, because nothing reachable calls them.
This commit is contained in:
parent
f3a0e435fd
commit
545ef6e0ea
31
lib/load.ml
31
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 —
|
||||
|
||||
@ -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
|
||||
|
||||
14
test/programs/pkg-macro.flan
Normal file
14
test/programs/pkg-macro.flan
Normal file
@ -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)
|
||||
7
test/programs/pkgs/mac/mac.flan
Normal file
7
test/programs/pkgs/mac/mac.flan
Normal file
@ -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))
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user