Skip to main content

submilli_engine/stdlib/code/
install.rs

1//! Wasm registrations, argument decoding and effect boundaries for `submilli:code`.
2use super::{MODULE_NAME, budget::Budget, walk};
3use crate::runtime::StoreData;
4use crate::runtime::effects;
5use crate::runtime::host::{
6    abi_arg, invariant_trap, register_host_value_async, write_submilli_string_struct_units,
7};
8use crate::runtime::intrinsic_types::build_intrinsic_types;
9use crate::runtime::prelude::vtable::read_string_units;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use wasmtime::{Caller, FuncType, HeapType, Linker, RefType, Result, Val, ValType, bail};
13
14pub fn install(linker: &mut Linker<StoreData>) -> Result<()> {
15    let engine = linker.engine().clone();
16    let intr = build_intrinsic_types(&engine)?;
17    let string = ValType::Ref(RefType::new(false, HeapType::ConcreteStruct(intr.string)));
18    let object = ValType::Ref(RefType::new(
19        false,
20        HeapType::ConcreteStruct(intr.object_shape),
21    ));
22    let options = ValType::Ref(RefType::new(true, HeapType::ConcreteStruct(intr.object)));
23
24    register_host_value_async(
25        linker,
26        MODULE_NAME,
27        crate::mangle::package_symbol(MODULE_NAME, "read"),
28        FuncType::new(
29            &engine,
30            [string.clone(), ValType::F64, ValType::F64],
31            [object.clone()],
32        ),
33        |caller, params| {
34            Box::pin(async move {
35                let mut budget = Budget::new(caller);
36                let path = argument(caller, &mut budget, abi_arg(params, 0)?)?;
37                let request = ReadArguments {
38                    path: super::normalize(caller.data().vfs.cwd(), &path)?,
39                    start: integer(abi_arg(params, 1)?, "offset", 1)? - 1,
40                    limit: integer(abi_arg(params, 2)?, "limit", 1)?,
41                };
42                let result =
43                    effects::execute(caller, "code.read", request, move |caller, _, request| {
44                        Box::pin(async move {
45                            let value = super::read(
46                                caller,
47                                &mut budget,
48                                &request.path,
49                                request.start,
50                                request.limit,
51                            )?;
52                            Ok(CodeResult {
53                                value,
54                                _budget: Some(budget),
55                            })
56                        })
57                    })
58                    .await?;
59                encode_json(caller, &result.value)
60            })
61        },
62    )?;
63
64    register_host_value_async(
65        linker,
66        MODULE_NAME,
67        crate::mangle::package_symbol(MODULE_NAME, "search"),
68        FuncType::new(&engine, [string.clone(), options], [object.clone()]),
69        |caller, params| {
70            Box::pin(async move {
71                let mut budget = Budget::new(caller);
72                let request = SearchArguments {
73                    pattern: argument(caller, &mut budget, abi_arg(params, 0)?)?,
74                    options: walk::Options::read(caller, &mut budget, abi_arg(params, 1)?)?,
75                };
76                let result =
77                    effects::execute(caller, "code.search", request, move |caller, _, request| {
78                        Box::pin(async move {
79                            let value = super::search(
80                                caller,
81                                &mut budget,
82                                &request.pattern,
83                                &request.options,
84                            )?;
85                            Ok(CodeResult {
86                                value,
87                                _budget: Some(budget),
88                            })
89                        })
90                    })
91                    .await?;
92                encode_json(caller, &result.value)
93            })
94        },
95    )?;
96
97    register_host_value_async(
98        linker,
99        MODULE_NAME,
100        crate::mangle::package_symbol(MODULE_NAME, "glob"),
101        FuncType::new(&engine, [string.clone()], [object.clone()]),
102        |caller, params| {
103            Box::pin(async move {
104                let mut budget = Budget::new(caller);
105                let request = argument(caller, &mut budget, abi_arg(params, 0)?)?;
106                let result =
107                    effects::execute(caller, "code.glob", request, move |caller, _, request| {
108                        Box::pin(async move {
109                            let value = super::glob(caller, &mut budget, &request)?;
110                            Ok(CodeResult {
111                                value,
112                                _budget: Some(budget),
113                            })
114                        })
115                    })
116                    .await?;
117                encode_json(caller, &result.value)
118            })
119        },
120    )?;
121
122    register_host_value_async(
123        linker,
124        MODULE_NAME,
125        crate::mangle::package_symbol(MODULE_NAME, "tree"),
126        FuncType::new(&engine, [string.clone(), ValType::F64], [object.clone()]),
127        |caller, params| {
128            Box::pin(async move {
129                let mut budget = Budget::new(caller);
130                let path = argument(caller, &mut budget, abi_arg(params, 0)?)?;
131                let request = TreeArguments {
132                    path: super::normalize(caller.data().vfs.cwd(), &path)?,
133                    depth: integer(abi_arg(params, 1)?, "depth", 0)?,
134                };
135                let result =
136                    effects::execute(caller, "code.tree", request, move |caller, _, request| {
137                        Box::pin(async move {
138                            let value =
139                                super::tree(caller, &mut budget, &request.path, request.depth)?;
140                            Ok(CodeResult {
141                                value,
142                                _budget: Some(budget),
143                            })
144                        })
145                    })
146                    .await?;
147                encode_json(caller, &result.value)
148            })
149        },
150    )?;
151
152    register_host_value_async(
153        linker,
154        MODULE_NAME,
155        crate::mangle::package_symbol(MODULE_NAME, "edit"),
156        FuncType::new(
157            &engine,
158            [
159                string.clone(),
160                string.clone(),
161                string.clone(),
162                ValType::I32,
163                ValType::F64,
164            ],
165            [object.clone()],
166        ),
167        |caller, params| {
168            Box::pin(async move {
169                let mut budget = Budget::new(caller);
170                let path = argument(caller, &mut budget, abi_arg(params, 0)?)?;
171                let old = argument(caller, &mut budget, abi_arg(params, 1)?)?;
172                let new = argument(caller, &mut budget, abi_arg(params, 2)?)?;
173                let Val::I32(replace_all) = abi_arg(params, 3)? else {
174                    return Err(invariant_trap("code.edit: invalid boolean argument"));
175                };
176                let near_line = integer(abi_arg(params, 4)?, "nearLine", 0)?;
177                let request = EditArguments {
178                    path: super::normalize(caller.data().vfs.cwd(), &path)?,
179                    old,
180                    new,
181                    replace_all: *replace_all != 0,
182                    near_line,
183                };
184                let result =
185                    effects::execute(caller, "code.edit", request, move |caller, _, request| {
186                        Box::pin(async move {
187                            let value = super::edit(
188                                caller,
189                                &mut budget,
190                                &request.path,
191                                &request.old,
192                                &request.new,
193                                request.replace_all,
194                                request.near_line,
195                            )?;
196                            Ok(CodeResult {
197                                value,
198                                _budget: Some(budget),
199                            })
200                        })
201                    })
202                    .await?;
203                encode_json(caller, &result.value)
204            })
205        },
206    )?;
207
208    register_host_value_async(
209        linker,
210        MODULE_NAME,
211        crate::mangle::package_symbol(MODULE_NAME, "insertAt"),
212        FuncType::new(
213            &engine,
214            [string.clone(), ValType::F64, string.clone()],
215            [object.clone()],
216        ),
217        |caller, params| {
218            Box::pin(async move {
219                let mut budget = Budget::new(caller);
220                let path = argument(caller, &mut budget, abi_arg(params, 0)?)?;
221                let line = integer(abi_arg(params, 1)?, "line", 1)?;
222                let text = argument(caller, &mut budget, abi_arg(params, 2)?)?;
223                let request = InsertArguments {
224                    path: super::normalize(caller.data().vfs.cwd(), &path)?,
225                    line,
226                    text,
227                };
228                let result = effects::execute(
229                    caller,
230                    "code.insertAt",
231                    request,
232                    move |caller, _, request| {
233                        Box::pin(async move {
234                            let value = super::insert_at(
235                                caller,
236                                &mut budget,
237                                &request.path,
238                                request.line,
239                                &request.text,
240                            )?;
241                            Ok(CodeResult {
242                                value,
243                                _budget: Some(budget),
244                            })
245                        })
246                    },
247                )
248                .await?;
249                encode_json(caller, &result.value)
250            })
251        },
252    )?;
253
254    register_host_value_async(
255        linker,
256        MODULE_NAME,
257        crate::mangle::package_symbol(MODULE_NAME, "diffText"),
258        FuncType::new(&engine, [string.clone(), string.clone()], [string.clone()]),
259        |caller, params| {
260            Box::pin(async move {
261                let mut budget = Budget::new(caller);
262                let first = units(caller, &mut budget, abi_arg(params, 0)?)?;
263                let second = units(caller, &mut budget, abi_arg(params, 1)?)?;
264                let result = super::diff_text(caller, &mut budget, &first, &second)?;
265                Ok(Val::AnyRef(Some(
266                    write_submilli_string_struct_units(caller, &result)?.to_anyref(),
267                )))
268            })
269        },
270    )?;
271
272    register_host_value_async(
273        linker,
274        MODULE_NAME,
275        crate::mangle::package_symbol(MODULE_NAME, "diffFiles"),
276        FuncType::new(&engine, [string.clone(), string.clone()], [string.clone()]),
277        |caller, params| {
278            Box::pin(async move {
279                let mut budget = Budget::new(caller);
280                let first = argument(caller, &mut budget, abi_arg(params, 0)?)?;
281                let second = argument(caller, &mut budget, abi_arg(params, 1)?)?;
282                let request = DiffFilesArguments {
283                    first: super::normalize(caller.data().vfs.cwd(), &first)?,
284                    second: super::normalize(caller.data().vfs.cwd(), &second)?,
285                };
286                let result = effects::execute(
287                    caller,
288                    "code.diffFiles",
289                    request,
290                    move |caller, _, request| {
291                        Box::pin(async move {
292                            let value = super::diff_files(
293                                caller,
294                                &mut budget,
295                                &request.first,
296                                &request.second,
297                            )?;
298                            Ok(CodeResult {
299                                value,
300                                _budget: Some(budget),
301                            })
302                        })
303                    },
304                )
305                .await?;
306                Ok(Val::AnyRef(Some(
307                    write_submilli_string_struct_units(caller, &result.value)?.to_anyref(),
308                )))
309            })
310        },
311    )?;
312
313    register_host_value_async(
314        linker,
315        MODULE_NAME,
316        crate::mangle::package_symbol(MODULE_NAME, "applyPatch"),
317        FuncType::new(&engine, [string.clone(), string], [object]),
318        |caller, params| {
319            Box::pin(async move {
320                let mut budget = Budget::new(caller);
321                let path = argument(caller, &mut budget, abi_arg(params, 0)?)?;
322                let patch = argument(caller, &mut budget, abi_arg(params, 1)?)?;
323                let request = PatchArguments {
324                    path: super::normalize(caller.data().vfs.cwd(), &path)?,
325                    patch,
326                };
327                let result = effects::execute(
328                    caller,
329                    "code.applyPatch",
330                    request,
331                    move |caller, _, request| {
332                        Box::pin(async move {
333                            let value = super::apply_patch(
334                                caller,
335                                &mut budget,
336                                &request.path,
337                                &request.patch,
338                            )?;
339                            Ok(CodeResult {
340                                value,
341                                _budget: Some(budget),
342                            })
343                        })
344                    },
345                )
346                .await?;
347                encode_json(caller, &result.value)
348            })
349        },
350    )?;
351    Ok(())
352}
353
354#[derive(Serialize)]
355struct ReadArguments {
356    path: String,
357    start: usize,
358    limit: usize,
359}
360
361#[derive(Serialize)]
362struct SearchArguments {
363    pattern: String,
364    options: walk::Options,
365}
366
367#[derive(Serialize)]
368struct TreeArguments {
369    path: String,
370    depth: usize,
371}
372
373#[derive(Serialize)]
374struct EditArguments {
375    path: String,
376    old: String,
377    new: String,
378    replace_all: bool,
379    near_line: usize,
380}
381
382#[derive(Serialize)]
383struct InsertArguments {
384    path: String,
385    line: usize,
386    text: String,
387}
388
389#[derive(Serialize)]
390struct DiffFilesArguments {
391    first: String,
392    second: String,
393}
394
395#[derive(Serialize)]
396struct PatchArguments {
397    path: String,
398    patch: String,
399}
400
401#[derive(Serialize, Deserialize)]
402struct CodeResult<T> {
403    value: T,
404    // Keep live native buffers charged through history encoding and Wasm conversion.
405    #[serde(skip)]
406    _budget: Option<Budget>,
407}
408
409fn encode_json(caller: &mut Caller<'_, StoreData>, result: &Value) -> Result<Val> {
410    let mut budget = Budget::new(caller);
411    let encoded = serde_json::to_string(result)?;
412    budget.check_size(encoded.len())?;
413    budget.charge(caller, encoded.len().saturating_mul(4))?;
414    crate::stdlib::session::value::deserialize(caller, &encoded.encode_utf16().collect::<Vec<_>>())
415}
416
417pub(super) fn argument(
418    caller: &mut Caller<'_, StoreData>,
419    budget: &mut Budget,
420    val: &Val,
421) -> Result<String> {
422    String::from_utf16(&units(caller, budget, val)?).map_err(|_| {
423        wasmtime::Error::msg("code: filesystem text must not contain lone UTF-16 surrogates")
424    })
425}
426fn units(caller: &mut Caller<'_, StoreData>, budget: &mut Budget, val: &Val) -> Result<Vec<u16>> {
427    let value = read_string_units(caller, val, "code argument")?;
428    budget.check_size(value.len().saturating_mul(2))?;
429    budget.charge(caller, value.len().saturating_mul(4))?;
430    Ok(value)
431}
432fn integer(val: &Val, name: &str, min: usize) -> Result<usize> {
433    let n = match val {
434        Val::F64(bits) => f64::from_bits(*bits),
435        _ => {
436            return Err(crate::runtime::host::invariant_trap(
437                "code: invalid numeric argument",
438            ));
439        }
440    };
441    if !n.is_finite() || n.fract() != 0.0 || n < min as f64 || n > u32::MAX as f64 {
442        bail!(
443            "code: {name} must be an integer between {min} and {}",
444            u32::MAX
445        );
446    }
447    Ok(n as usize)
448}