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