Skip to content

[RFC] AST compatibility of plugins #2929

kdy1 started this conversation in Ideas
[RFC] AST compatibility of plugins #2929
Dec 1, 2021 · 5 comments · 7 replies

kdy1
Dec 1, 2021
Maintainer

Terms

normal ast: AST used by core transforms.
plugin ast: AST representation which can be passed to the dynamic library directly.

Requirements

These are what we want to achieve

  • We need to pass AST to plugins and construct AST back from the output of plugins.
  • We want to prevent adding fields/variants to ast being a breaking change.
  • If possible, we want plugins to be fast.

Triage: serde

This is a good old technique for implementing plugin systems.

Previously serde_json is used to serialize/deserialize AST. This approach is easy to implement, but serialization and deserialization are costly enough to take more time than the plugin itself.

But adding fields or variants to AST nodes is breaking change. If a new plugin ignores fields, it will be simply removed from the output of the plugin.

Triage: Bridge-ast

This is the current design. This is conceptually very similar to serde-based one. Instead of representing AST in serialized form, we convert normal ast to plugin ast, which can be passed to dynamic libraries directly. Then dynamic library converts it back to normal ast.

But the ast compatibility issue is not solved. We can detect ast compatibility issue on load time, but we can't automatically fix it. All plugins will be broken when a field or variant is added.

Triage: Plugin-only ast (X)

This is based on prefix type of abi_stable. It didn't work, but idea is that we can make every type an opaque pointer. Accessing fields is done by calling methods on the pointer and it does not depend on the exact layout of ast types.

Problem is that abi_stable currently does not support mutating fields in prefix types.

Also, this requires lots of more work. We need to reimplement the visitor generator and we can't use exising utilities for swc_ecma_ast.

Finally, this makes all plugins slower. rustc optimize it well because there will be a lot of indirection. I expect it to be at least 2x slower. It's based on https://nullderef.com/blog/plugin-abi-stable/ and I expect much more slowdown like 5x to 20x, because we have lots of opaque types.

Ideas

Some ideas about this issue.

Idea: Add Unknown variant to enums and extra to structs

In this way, plugin will ignore extra fields and variants if they don't know the way to handle it.

The problems of this approach are that it will make the core slower, and target syntax in newly-added AST nodes cannot be handled.

e.g. If you use some feature in static blocks and if plugin is too old so it does not know static blocks, all ast nodes in static blocks will be simply ignored.

Idea: Just bump major version on breaking changes

This is too bad solution IMO, but may work.

Version history: https://crates.io/crates/swc_ecma_ast/versions

Replies

5 comments
·
7 replies

What about the performance of Bridge-ast way?

Does this RFC show how to implement babel AST compatibility?

1 reply
@kdy1

kdy1 Dec 1, 2021
Maintainer Author

Bridge AST is much fastter than serde_json.
You can see benchmark result at #2635 (comment)

I can accept last idea if I just need to update my Cargo.toml and compile/publish it.

1 reply
@kdy1

kdy1 Dec 1, 2021
Maintainer Author

For most of ast changes, it will be enough.
But the problem is plugins managed by other authors.

Have other serialization formats than json been considered? For example msgpack should be somewhat faster but is it enough to make a meaningful difference?

1 reply
@kdy1

kdy1 Dec 1, 2021
Maintainer Author

I tried some other formats, but I failed because many binary serializers did not support deserialize_any.

Who are the target of this plugin system? Why the plugin cannot be implemented directly in Rust?

1 reply
@kdy1

kdy1 Dec 1, 2021
Maintainer Author

Plugins will be written in rust. I'm not sure why you said

Why the plugin cannot be implemented directly in Rust?

Problem is that abi_stable currently does not support mutating fields in prefix types.

Do you need mutation? Could the plugin API be like Fold where you return a new AST node? Then you could have a constructor function. When you add a field, I guess you'd need a new constructor that also accepts that new field while keeping the old one around. A bit messy but it might work.

Another wild idea: say you decide supporting all this ABI stable plugin system stuff is too much work, too slow, or causes too much perf regression in core. Have you thought of inverting the model? Rather than core loading a config, finding plugins, and calling them, the config could just be a small Rust program with a normal crate dependency on both SWC core's defaults and whatever plugins it wanted. You basically already have this, but with a much simpler API for the defaults and a standard API for plugins it might be simple enough for most people to understand even if they don't know Rust. And this avoids the ABI stability and plugin loading problems entirely. If you wanted, you could even have a config format in another language like JSON that you compile to a Rust program to make it even simpler.

3 replies
@kdy1

kdy1 Dec 1, 2021
Maintainer Author

Do you need mutation? Could the plugin API be like Fold where you return a new AST node

To be exact, abi_stable does not support storing non-copy types. So we can't pass plugin ast back from plugin to core.

And about the wild idea, the problem is compile-time IMO. Compilation of node-swc takes 20 min ~ 40 min, so I'm not sure it's feasible. But seems like a great idea, thanks!

@devongovett

Ah seems like you'd need to make your own wrapper type then...

Compilation only takes that long if you are compiling for all platforms though. When building a custom plugin config, you'd only need to compile for the system you're running on which is much less. Perhaps you could also distribute precompiled libraries as well to speed that up so that users don't need to recompile all of SWC core... hmm that might have the same rustc versioning problems 😕

@kdy1

kdy1 Dec 1, 2021
Maintainer Author

Actually 20 min ~ 40 min is per-platform time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Ideas
Labels
None yet
6 participants