I’m currently exploring the capabilities of pps in a personal project and was wondering if it is possible to use a PPX to modify the AST of a different module within the same library. Specifically, I’d like to achieve something along these lines:
In one file, I define a type, for example:
type foo = Foo
Then, using a PPX on the previous definition, I want to add a constructor to a different type (let’s call it Bar) in another module within my library. For example:
type bar = Bar1 | Bar2
After running the PPX, I want the bar type to be automatically updated to include a new constructor FooBar, resulting in:
type bar = Bar1 | Bar2 | Foo
I’m interested in understanding whether it’s feasible to implement such behavior using a PPX, and if so, what the general approach would be. Specifically:
Is it possible for a PPX to modify the AST of a different module?
What would be the main challenges or limitations associated with this approach? I know extensible variant types are a thing for example, but I would like to refrain from using them.
Are there any existing PPX examples or projects that achieve something similar?
Any guidance or examples would be greatly appreciated!
Thank you in advance for your help.
The only somewhat similar thing I can think of is ppx_import, but I don’t think it has what you need out of the box.
AFAIK ppx_import is quite special as to how it works, but maybe you could borrow from it to do some cross-module preprocessing.
Besides extensible variant types, there are also polymorphic variants which easily allow one type to be included in another, which maybe is what you need?
If I understand correctly, using polymorphic variants would require me to change the definition of the type in the 2nd module every time I want to add a new type in the 1st module, right? If so that defeats the purpose of what I’m trying to do.
Thanks for the info on ppx_import though! Maybe the easy way for now is to use an extensible variant type…
I’ve been meaning to reply to you but I never remember to do so when I’m at my laptop (only at my phone). So this is late. What you’re trying to do might not be very hard, actually.
I don’t know how PPXlib works, but in principle, a PPX rewriter could easily add information to a “global context” which could be passed from one PPX rewriter to the next, and could affect the subsequent PPX rewriter. Again, I don’t know how PPXlib does it, but in the pa_ppx family of PPX rewriters (based on Camlp5, so not compatible with PPXlib), the ppx_deriving rewriter works as two passes.
If you recall, in ppx_deriving, there’s a rule about how names are scoped, so that if two derivers (let’s say d1 and d2) both want to use an attribute (like [@name ...], hence a name-clash) they can arrange so that to use @name via @d1.name and @d2.name). It’s been a long time since I wrote the code, so I don’t remember how exactly the rules worked, but I remember that I wrote the ppx_deriving rewriter thus:
scan the entire module to which ppx_deriving is being applied, looking for instances of derivers. From all those derivers, get the list of attributes that can be applied to types, and find the attributes that name-clash and name a list of those.
in a second pass, scan thru the module, applying each deriver, and for each, using the information from step #1, you can decide whether (e.g.) @name is an attribute for deriver d1, or you need to see @d1.name.
I fear this isn’t so clear, but my point is, the first pass is a PPX rewriter that accumulates a context, that it passes to the second pass PPX rewriter, that uses it to rewrite code in a manner -driven- by that context.