Skip to main content

larvae_worm/
node.rs

1//! The guest side of the node API, so a rule reads an ordinary type and not offsets
2//!
3//! A rule runs in the wasm form, where larvae hands each host function below to
4//! the module as it instantiates it. [`Node`] and every method of it stay on
5//! each target all the same, so a worm reads one API whichever form it ships
6//! as, and the `rules!` macro expands wherever it is written.
7
8use crate::abi;
9
10// The host functions, which larvae supplies to a wasm module. rustdoc writes
11// nothing for an extern block, so this is a plain comment.
12#[cfg(target_arch = "wasm32")]
13#[link(wasm_import_module = "larvae")]
14unsafe extern "C" {
15    #[link_name = "node_kind"]
16    safe fn host_node_kind(epoch: u64, id: u32) -> i64;
17    #[link_name = "node_text"]
18    safe fn host_node_text(epoch: u64, id: u32) -> i64;
19    #[link_name = "node_span_start"]
20    safe fn host_span_start(epoch: u64, id: u32) -> i64;
21    #[link_name = "node_span_end"]
22    safe fn host_span_end(epoch: u64, id: u32) -> i64;
23    #[link_name = "node_parent"]
24    safe fn host_parent(epoch: u64, id: u32) -> i64;
25    #[link_name = "node_child_count"]
26    safe fn host_child_count(epoch: u64, id: u32) -> i64;
27    #[link_name = "node_child"]
28    safe fn host_child(epoch: u64, id: u32, index: u32) -> i64;
29    #[link_name = "take_str"]
30    safe fn host_take_str(ptr: u32, len: u32) -> i64;
31    #[link_name = "replace"]
32    safe fn host_replace(epoch: u64, id: u32, ptr: u32, len: u32) -> i64;
33    #[link_name = "remove"]
34    safe fn host_remove(epoch: u64, id: u32) -> i64;
35}
36
37/// The same names on a target that is not wasm, where no host answers them.
38///
39/// A worm that is not wasm holds no node: larvae gives a native worm one file
40/// at a time, and a Luau worm runs in the interpreter. So nothing here runs,
41/// and each one says so if it ever does.
42///
43/// The names have to exist all the same. An `extern` block outside wasm is a
44/// symbol that something must define, and `wasm_import_module` means nothing
45/// there. `link.exe` reads every object that it links and refuses a native
46/// worm over the ten names, while the linkers of linux and of macos drop the
47/// code of a rule first and refuse nothing, so two platforms of three hide the
48/// problem. One name hides even better: `remove` is a function of the C
49/// library, so a linker binds that import to the one that deletes a file.
50#[cfg(not(target_arch = "wasm32"))]
51mod outside_wasm {
52    pub fn host_node_kind(_epoch: u64, _id: u32) -> i64 {
53        absent()
54    }
55
56    pub fn host_node_text(_epoch: u64, _id: u32) -> i64 {
57        absent()
58    }
59
60    pub fn host_span_start(_epoch: u64, _id: u32) -> i64 {
61        absent()
62    }
63
64    pub fn host_span_end(_epoch: u64, _id: u32) -> i64 {
65        absent()
66    }
67
68    pub fn host_parent(_epoch: u64, _id: u32) -> i64 {
69        absent()
70    }
71
72    pub fn host_child_count(_epoch: u64, _id: u32) -> i64 {
73        absent()
74    }
75
76    pub fn host_child(_epoch: u64, _id: u32, _index: u32) -> i64 {
77        absent()
78    }
79
80    pub fn host_take_str(_ptr: u32, _len: u32) -> i64 {
81        absent()
82    }
83
84    pub fn host_replace(_epoch: u64, _id: u32, _ptr: u32, _len: u32) -> i64 {
85        absent()
86    }
87
88    pub fn host_remove(_epoch: u64, _id: u32) -> i64 {
89        absent()
90    }
91
92    fn absent() -> ! {
93        unreachable!("the node API belongs to the wasm form, and this worm is not wasm")
94    }
95}
96
97#[cfg(not(target_arch = "wasm32"))]
98use outside_wasm::*;
99
100/// A handle to one node of the larvae AST, valid only for the file it came from
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct Node {
103    epoch: u64,
104    id: u32,
105}
106
107impl Node {
108    /// Rebuild a handle that the host named. Only the generated exports call this.
109    #[doc(hidden)]
110    pub fn from_raw(epoch: u64, id: u32) -> Self {
111        Self { epoch, id }
112    }
113
114    /// The kind of this node, for example `"CallExpr"`
115    pub fn kind(&self) -> String {
116        pull(host_node_kind(self.epoch, self.id))
117    }
118
119    /// The source text that this node covers
120    pub fn text(&self) -> String {
121        pull(host_node_text(self.epoch, self.id))
122    }
123
124    /// Byte offsets into the original source, as a half open range
125    pub fn span(&self) -> (u32, u32) {
126        let start = host_span_start(self.epoch, self.id).max(0) as u32;
127        let end = host_span_end(self.epoch, self.id).max(0) as u32;
128
129        (start, end)
130    }
131
132    /// The node that contains this one. Only the root has none.
133    pub fn parent(&self) -> Option<Node> {
134        match host_parent(self.epoch, self.id) {
135            id if id < 0 => None,
136
137            id => Some(Node::from_raw(self.epoch, id as u32)),
138        }
139    }
140
141    /// The direct children, in source order
142    pub fn children(&self) -> Vec<Node> {
143        let count = host_child_count(self.epoch, self.id).max(0) as u32;
144
145        (0..count)
146            .filter_map(|i| match host_child(self.epoch, self.id, i) {
147                id if id < 0 => None,
148
149                id => Some(Node::from_raw(self.epoch, id as u32)),
150            })
151            .collect()
152    }
153
154    /// Queue a replacement of the bytes of this node
155    pub fn replace(&self, text: &str) -> bool {
156        host_replace(self.epoch, self.id, text.as_ptr() as u32, text.len() as u32) >= 0
157    }
158
159    /// Queue a removal. larvae keeps the newlines, so the line counts hold.
160    pub fn remove(&self) -> bool {
161        host_remove(self.epoch, self.id) >= 0
162    }
163}
164
165/*
166An accessor stages its text on the host side and returns a length, because a
167wasm function returns one number. The guest allocates that many bytes and asks
168for the copy. Thus the host needs no allocator on the guest side of the
169boundary.
170*/
171fn pull(len: i64) -> String {
172    if len <= 0 {
173        return String::new();
174    }
175
176    let len = len as u32;
177    let buf = abi::alloc(len);
178    let written = host_take_str(buf as u32, len);
179
180    if written < 0 {
181        // SAFETY: buf came from alloc with exactly len bytes, and no code uses it
182        unsafe { abi::dealloc(buf, len) };
183
184        return String::new();
185    }
186
187    // SAFETY: the host wrote `written` bytes of a &str that it held, so this is utf-8
188    unsafe {
189        let bytes = std::slice::from_raw_parts(buf, written as usize).to_vec();
190        abi::dealloc(buf, len);
191
192        String::from_utf8_unchecked(bytes)
193    }
194}