This is one for the macrology experts—I'm hoping to cement my understanding of this behavior beyond "it works and I don't need to touch it" because I may indeed need to modify it some day!
PS On "macro*-generated" in the title: That's a Kleene star in the sense that one macro expands to another, which expands to a use of define-runtime-path
that I want to appear as if the original source. I'm pretty sure all my problems are from using helper macros here. Would plain old functions work better here? What are the tradeoffs[1]? (Am I even right about the nesting of helpers being the issue?)
TL;DR Did I get this fix right?
Recently, I removed some extraneous synthesized identifiers in macros, which required tweaking a macro that used those for lexical and source information. The original use of those IDs is from commit 4b5848b (include original location in ability card, 2023-03-20), which I don't completely understand.
Did I diagnose the problem correctly wrt capturing the correct source/context for define-runtime-path
by needing something from the original inputs? Is there a simpler fix?
Reconstructing the original reason for synthesizing define-runtime-path
this way
I was able to track down the original conversation that led to this code; here's a summary.
I started with code like this:
(define-syntax-parse-rule (make-dbs ({~literal provide} info-db ability-db)
;; stuff
;; ...
;; ...
)
;; stuff
#:with runtime-path-lib (datum->syntax #'info-db 'racket/runtime-path #'info-db)
#:with here (datum->syntax #'info-db 'here #'info-db)
#:with runtime-path-define (datum->syntax #'info-db (list 'define-runtime-path #'here ".") #'info-db)
(begin
(provide info-db ability-db)
(require ;;stuff ...
runtime-path-lib)
runtime-path-define
(define-values (original-info-db original-ability-db)
#;(use of "here" in here somewhere))
;; more stuff
;; ...
))
where info-db
is an identifier synthesized from the source/context of the original macro (a #%module-begin
). The user #'(shu hung)
helped me, suggesting manipulating only the here
context. For me, that appears to have produced runtime-paths based on the macro implementation or module expander, not the input file.
We eventually got to the version with (syntax-e #'(define-runtime-path here "."))
, where #'(shu hung)
notes:
The lexical information has to be attached to the entire syntax of
(define-runtime-path ...)
. […]Here
here
is [sic] anddefine-runtime-path
all have the lexical scope of the macro definitionso it is intended that
here
is visible tomake-db
-generated code and thatdefine-runtime-path
is referring to(require racket/runtime-path
in the module definingmake-db
In sum, it seems that the original needs to make sure
define-runtime-path
has themake-dbs
macro scopes in order to access the correct binding fromracket/runtime-path
(and not need to embed thatrequire
in the expansion);here
also has themake-dbs
scope (why? I think because it used in the macro?);- the whole
(define-runtime-path …)
form also needs the scopes from the module to which I want the runtime path to be relative—originally, that came from#'info-db
.
What broke?
I realized that a macro-generated (provide id)
(at least from a module-expander's #%module-begin
) needs no special manipulation to make the program (require mod) id
work, in part thanks to reading DSLs in Racket: You Want It How, Now?[2]. When making info-db
not synthesized via format-id
, I noticed that the runtime paths stopped being embedded correctly—Frosthaven Manager displays "AoE not found" or similar at runtime[3].
I don't recall all the experiments I tried, but I remember using this-syntax
in make-dbs
and #'(imports ...)
without success. Scrolling back through debug output in my terminal, I see runtime-paths from the application root (PWD of the application runtime) and from <root>/syntax
(which contains the make-dbs
implementation).
Finally, I decided to send the callers' this-syntax
through to give make-dbs
something with the correct scopes to attach to. This feels like a bit of a hack, and a simpler option would be appreciated.
I ended up writing:
However, as a result, the input #'info-db to make-dbs is no longer
original in the sense of coming from the original module's syntax: this
is a problem because it means that the define-runtime-path form gets the wrong context, causing the AoE modules to not be found at runtime.Perhaps there is a way to use imports, infos, or actions as appropriate
sources, but using #'(imports ...) as the context and source for
datum->syntax also failed. Instead, pass an original syntax that can be
used for this information (and document it). Note that this requires
quasisyntax in order to embed this-syntax from syntax-parse.
To return to the original questions:
- Did I diagnose the problem correctly?
- What is meant by "form" in "The enclosing path for a define-runtime-path is determined as follows from the define-runtime-path syntactic form:"? Is it the entire S-expression
(define-runtime-path …)
? - Why does
here
also needmake-dbs
scopes? I think because it is used in the macro body.
- What is meant by "form" in "The enclosing path for a define-runtime-path is determined as follows from the define-runtime-path syntactic form:"? Is it the entire S-expression
- Is there a simpler fix to make sure all the needs from earlier stay met?
- Should I have just been using the macro stepper all along?
Presumably, one tradeoff is having to return syntax containing
(require racket/runtime-path)
instead of relying on hygiene to make that work in the helper module. ↩︎It's code has some flaws, but it did demonstrate unadorned macro-generated provides, where I though synthesizing IDs was required. ↩︎
Evaluating AoE references at compile-time is ongoing work. ↩︎