larvae_worm/lib.rs
1/*!
2The guest side of the larvae worm ABI.
3
4A worm is a `wasm32` module that larvae loads and calls. wasm has no strings.
5Thus all data crosses as an offset and a length into the linear memory of the
6module. This crate owns that protocol, so a worm author does not write it:
7
8```ignore
9larvae_worm::frontend!(|source: &str, config: &str| -> anyhow::Result<String> {
10 luaux::compile_configured(source, Backend::Vide, &Config::parse(config)?)
11});
12```
13
14The macro is not the only entry point. [`abi`] is public and documented. Thus
15a worm with an unusual design can export the raw functions itself, without a
16copy of the macro.
17
18# The ABI
19
20A worm exports `memory`, plus:
21
22| export | signature |
23|---|---|
24| `larvae_alloc` | `(len: u32) -> ptr` |
25| `larvae_dealloc` | `(ptr, len: u32)` |
26| `larvae_transform` | `(src_ptr, src_len, cfg_ptr, cfg_len) -> *header` |
27| `larvae_init` | `(cfg_ptr, cfg_len, rules_ptr, rules_len)` |
28| `larvae_visit` | `(rule, epoch, node_id)` |
29| `larvae_format` | `(src_ptr, src_len) -> *header` |
30| `larvae_lint` | `(src_ptr, src_len) -> *header` |
31| `larvae_settings` | `(fmt_ptr, fmt_len, lint_ptr, lint_len)` |
32
33`larvae_transform` returns a pointer to a three word header,
34`[out_ptr, out_len, ok]`. `ok` is 1 when the bytes are output and 0 when they
35are an error message. The header lives in a static, so the host does not free
36it. The host calls `larvae_dealloc(out_ptr, out_len)` when it has read the
37payload out.
38
39`larvae_format` and `larvae_lint` return the same header. Their ok payload is
40the JSON of a [`wire::Format`] or [`wire::Lint`] reply. The
41[`formatter!`], [`linter!`], and [`settings!`] macros write these three
42exports, behind the `wire` feature.
43*/
44
45#![deny(missing_docs)]
46
47/// The ABI revision this crate implements. It must match `api` in `worm.toml`.
48pub const ABI_VERSION: u32 = 1;
49
50pub mod abi;
51#[cfg(feature = "native")]
52pub mod native;
53pub mod node;
54#[cfg(feature = "wire")]
55pub mod wasm_ops;
56#[cfg(feature = "wire")]
57pub mod wire;
58
59pub use node::Node;
60
61/**
62Define a front-end worm. It takes source text and returns transformed source.
63
64The closure takes the contents of the file and the `[config.<name>]` table of
65the worm, serialized again as TOML. It returns the transformed source. Each
66error type that implements [`Display`](core::fmt::Display) works, so
67`anyhow::Result<String>` is valid.
68
69```ignore
70larvae_worm::frontend!(|source: &str, _config: &str| -> Result<String, String> {
71 Ok(source.replace("<>", "{}"))
72});
73```
74
75The macro expands to the three exports in the module docs. Use it once per worm.
76*/
77#[macro_export]
78macro_rules! frontend {
79 ($handler:expr) => {
80 /// Allocate `len` bytes for the host to write into
81 #[unsafe(no_mangle)]
82 pub extern "C" fn larvae_alloc(len: u32) -> *mut u8 {
83 $crate::abi::alloc(len)
84 }
85
86 /// Release a buffer that the host does not need anymore
87 #[unsafe(no_mangle)]
88 pub extern "C" fn larvae_dealloc(ptr: *mut u8, len: u32) {
89 // SAFETY: the host passes back only pointers that larvae_alloc
90 // returned, with the length of the allocation
91 unsafe { $crate::abi::dealloc(ptr, len) }
92 }
93
94 /// Transform `src` under `cfg` and return a pointer to the result header
95 #[unsafe(no_mangle)]
96 pub extern "C" fn larvae_transform(
97 src_ptr: *const u8,
98 src_len: u32,
99 cfg_ptr: *const u8,
100 cfg_len: u32,
101 ) -> *const u32 {
102 // SAFETY: larvae_alloc allocated both spans, and the host wrote
103 // them and knows their lengths
104 unsafe { $crate::abi::dispatch(src_ptr, src_len, cfg_ptr, cfg_len, $handler) }
105 }
106 };
107}
108
109/**
110Define the rule half of a worm.
111
112Each rule is a name and a handler. larvae calls a rule only on the nodes that
113match the `filter` you declared in `worm.toml`. Thus undeclared kinds do not
114cross the boundary.
115
116```ignore
117larvae_worm::rules! {
118 "strip_debug" => |node: larvae_worm::Node| {
119 if node.kind() == "CallExpr" && node.text().starts_with("dprint") {
120 node.remove();
121 }
122 },
123}
124```
125
126Combine this macro with [`frontend!`](crate::frontend) when a worm holds both roles.
127*/
128#[macro_export]
129macro_rules! rules {
130 ($($name:literal => $handler:expr),+ $(,)?) => {
131 /// Rule ids are indexes into the order that is declared here
132 #[unsafe(no_mangle)]
133 pub extern "C" fn larvae_visit(rule: u32, epoch: u64, id: u32) {
134 let node = $crate::Node::from_raw(epoch, id);
135 let mut which = 0u32;
136
137 $(
138 if rule == which {
139 let _ = $name;
140 let handler = $handler;
141 handler(node);
142 return;
143 }
144
145 which += 1;
146 )+
147
148 let _ = which;
149 }
150 };
151}
152
153/**
154Define the format half of a worm. It needs the `wire` feature.
155
156The closure takes the contents of a claimed file and returns the layout as a
157[`wire::Format`]. larvae renders the layout with the width and indentation of
158the project, so no worm reimplements the printer. Set `fmt = true` under
159`[frontend]` in your `worm.toml`, because larvae only calls the export that
160the manifest promises.
161
162The macro writes only the `larvae_format` export. Combine it with
163[`frontend!`], which writes the allocator exports that every worm needs.
164
165```ignore
166larvae_worm::formatter!(|source: &str| -> Result<larvae_worm::wire::Format, String> {
167 Ok(larvae_worm::wire::Format::spans(find_luau_regions(source)))
168});
169```
170*/
171#[cfg(feature = "wire")]
172#[macro_export]
173macro_rules! formatter {
174 ($handler:expr) => {
175 /// Lay out `src` and return a pointer to the result header
176 #[unsafe(no_mangle)]
177 pub extern "C" fn larvae_format(src_ptr: *const u8, src_len: u32) -> *const u32 {
178 // SAFETY: larvae_alloc allocated the span, and the host wrote it
179 // and knows its length
180 unsafe { $crate::wasm_ops::dispatch_format(src_ptr, src_len, $handler) }
181 }
182 };
183}
184
185/**
186Define the lint half of a worm. It needs the `wire` feature.
187
188The closure takes the contents of a claimed file and returns the problems as
189a [`wire::Lint`]. The findings carry no severity, because the host stamps the
190levels from `[lint.rules]` and owns the exit codes. Declare each lint name
191under `[lints]` in your `worm.toml`.
192
193The macro writes only the `larvae_lint` export. Combine it with
194[`frontend!`], which writes the allocator exports that every worm needs.
195
196```ignore
197larvae_worm::linter!(|source: &str| -> Result<larvae_worm::wire::Lint, String> {
198 Ok(larvae_worm::wire::Lint::default())
199});
200```
201*/
202#[cfg(feature = "wire")]
203#[macro_export]
204macro_rules! linter {
205 ($handler:expr) => {
206 /// Report the problems of `src` and return a pointer to the result header
207 #[unsafe(no_mangle)]
208 pub extern "C" fn larvae_lint(src_ptr: *const u8, src_len: u32) -> *const u32 {
209 // SAFETY: larvae_alloc allocated the span, and the host wrote it
210 // and knows its length
211 unsafe { $crate::wasm_ops::dispatch_lint(src_ptr, src_len, $handler) }
212 }
213 };
214}
215
216/**
217Receive the settings of the project. It needs the `wire` feature.
218
219The macro writes the `larvae_settings` export. The host calls the export once,
220directly after init, with the resolved `[fmt]` table and the lint levels of
221the project, both as JSON text. Read them back at any later point with
222[`wasm_ops::settings`]. Thus the user states a width one time, and not a
223second time under `[worms.<name>.config]`.
224
225```ignore
226larvae_worm::settings!();
227
228fn width() -> Option<u64> {
229 let (fmt, _lint) = larvae_worm::wasm_ops::settings();
230
231 serde_json::from_str::<serde_json::Value>(&fmt).ok()?["column_width"].as_u64()
232}
233```
234*/
235#[cfg(feature = "wire")]
236#[macro_export]
237macro_rules! settings {
238 () => {
239 /// Store the settings of the project for `wasm_ops::settings` to return
240 #[unsafe(no_mangle)]
241 pub extern "C" fn larvae_settings(
242 fmt_ptr: *const u8,
243 fmt_len: u32,
244 lint_ptr: *const u8,
245 lint_len: u32,
246 ) {
247 // SAFETY: larvae_alloc allocated both spans, and the host wrote
248 // them and knows their lengths
249 unsafe { $crate::wasm_ops::store_settings(fmt_ptr, fmt_len, lint_ptr, lint_len) }
250 }
251 };
252}