Skip to main content

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}