Skip to main content

wasm_mumu/
lib.rs

1// src/lib.rs
2//
3// WASM wrapper for the MuMu/Lava interpreter.
4// - Compiles against `core-mumu` (browser-safe set of modules).
5// - Bakes in array (`array-mumu`) and math (`math-mumu`) plugins.
6// - **Intentionally does NOT pull std-mumu’s host-only parts**.
7//   We register only the std functions that make sense on the web:
8//     • std:put(x)   — print without newline (also drains core iterators)
9//     • std:log(x)   — print with newline   (also drains core iterators)
10//     • std:deep(x)  — deep-printed value (newline), returns x
11//
12// Extras exported to JS:
13//   - `symbols()` and `complete(prefix)` for REPL-style completion.
14//   - `poll()` to drive non-blocking iterator printers one tick.
15//   - `reset()` to re-initialize the VM in-place.
16//   - `drain_prints()` to fetch only the *new* prints since last drain.
17//
18// Returned values are mirrored via a small JSON view so the UI can render
19// them without poking at the internal `Value` representation.
20//
21
22use wasm_bindgen::prelude::*;
23use serde::Serialize;
24use serde_wasm_bindgen as swb;
25
26use std::collections::BTreeSet;
27use std::sync::{Arc, Mutex};
28
29use core_mumu::{Interpreter, Value};
30use core_mumu::parser::core::driver::parse_tokens;
31use core_mumu::parser::lexer::tokenize;
32use core_mumu::parser::types::{FunctionValue, IteratorKind};
33
34/// Better error messages in the browser console
35#[wasm_bindgen(start)]
36pub fn wasm_start() {
37    console_error_panic_hook::set_once();
38}
39
40/// A minimal JSON-serializable mirror of `Value` for JS interop.
41#[derive(Serialize)]
42#[serde(tag = "kind", rename_all = "snake_case")]
43enum JsValueView {
44    Int { value: i32 },
45    Long { value: i64 },
46    Float { value: f64 },
47    Bool { value: bool },
48    String { value: String },
49    IntArray { value: Vec<i32> },
50    FloatArray { value: Vec<f64> },
51    BoolArray { value: Vec<bool> },
52    StrArray { value: Vec<String> },
53    Int2DArray { value: Vec<Vec<i32>> },
54    Float2DArray { value: Vec<Vec<f64>> },
55    MixedArray { value: Vec<JsValueView> },
56    KeyedArray { value: Vec<(String, JsValueView)> },
57    Function,
58    Iterator,
59    Stream { label: String },
60    Tensor,
61    Regex { pattern: String, flags: String },
62    Placeholder,
63    Undefined,
64}
65
66/// Minimal conversion for interop (skip Ref; deref by reading inner).
67fn to_js_view(v: &Value) -> JsValueView {
68    use Value::*;
69    match v {
70        Int(n) => JsValueView::Int { value: *n },
71        Long(l) => JsValueView::Long { value: *l },
72        Float(f) => JsValueView::Float { value: *f },
73        Bool(b) => JsValueView::Bool { value: *b },
74        SingleString(s) => JsValueView::String { value: s.clone() },
75        IntArray(xs) => JsValueView::IntArray { value: xs.clone() },
76        FloatArray(xs) => JsValueView::FloatArray { value: xs.clone() },
77        BoolArray(xs) => JsValueView::BoolArray { value: xs.clone() },
78        StrArray(xs) => JsValueView::StrArray { value: xs.clone() },
79        Int2DArray(rows) => JsValueView::Int2DArray { value: rows.clone() },
80        Float2DArray(rows) => JsValueView::Float2DArray { value: rows.clone() },
81        MixedArray(items) => JsValueView::MixedArray {
82            value: items.iter().map(to_js_view).collect(),
83        },
84        KeyedArray(map) => JsValueView::KeyedArray {
85            value: map
86                .iter()
87                .map(|(k, v)| (k.clone(), to_js_view(v)))
88                .collect(),
89        },
90        Function(_) => JsValueView::Function,
91        Iterator(_) => JsValueView::Iterator,
92        Stream(h) => JsValueView::Stream {
93            label: h.label.clone(),
94        },
95        Tensor(_) => JsValueView::Tensor,
96        Regex(rx) => JsValueView::Regex {
97            pattern: rx.pattern.clone(),
98            flags: rx.flags.clone(),
99        },
100        Ref(cell) => to_js_view(&cell.lock().unwrap()),
101        Placeholder => JsValueView::Placeholder,
102        Undefined => JsValueView::Undefined,
103    }
104}
105
106/// Return a short type label (aligned with core `type(...)`).
107fn short_type_name(v: &Value) -> &'static str {
108    use Value::*;
109    match v {
110        Int(_) => "int",
111        IntArray(_) => "int_array",
112        Int2DArray(_) => "int2d_array",
113        Float(_) => "float",
114        FloatArray(_) => "float_array",
115        Float2DArray(_) => "float2d_array",
116        Long(_) => "long",
117        Bool(_) => "bool",
118        BoolArray(_) => "bool_array",
119        Undefined => "undefined",
120        Placeholder => "placeholder",
121        SingleString(_) => "string",
122        StrArray(_) => "str_array",
123        KeyedArray(_) => "keyed_array",
124        Function(_) => "function",
125        Stream(_) => "stream",
126        Iterator(_) => "iterator",
127        Tensor(_) => "tensor",
128        MixedArray(_) => "mixed_array",
129        Ref(_) => "ref",
130        Regex(_) => "regex",
131    }
132}
133
134#[derive(Serialize)]
135struct ExecOk {
136    ok: bool,
137    value: JsValueView,
138    value_string: String,
139    value_type: String,
140    prints: Vec<String>,
141}
142
143#[derive(Serialize)]
144struct ExecErr {
145    ok: bool,
146    error: String,
147}
148
149/// Public WASM wrapper around the interpreter
150#[wasm_bindgen]
151pub struct WasmMumu {
152    interp: Interpreter,
153    verbose: bool,
154    drained_cursor: usize, // index into print buffer for incremental drains
155}
156
157fn bootstrap_interpreter(verbose: bool) -> Interpreter {
158    let mut i = Interpreter::new();
159    i.set_verbose(verbose);
160
161    // Register **web-safe** core modules (no libloading / filesystem here)
162    core_mumu::modules::compose::register_compose_and_pipe(&mut i);
163    core_mumu::modules::sput::register_sput(&mut i);
164    core_mumu::modules::slog::register_slog(&mut i);
165    core_mumu::modules::step::register_step(&mut i);
166    core_mumu::modules::r#type::register_type(&mut i);
167    core_mumu::modules::check::register_check(&mut i);
168
169    // Disallow extend/include in browser
170    i.disallow_extend();
171
172    // Bake-in plugins
173    mumuarray::register_all(&mut i);
174    mumumath::register_all(&mut i);
175
176    // Register only the **usable** subset of std (no input/input_iter in web)
177    register_web_std(&mut i);
178
179    i
180}
181
182/// ---- Minimal, web-safe std:* registration (put/log/deep only) ----
183fn register_web_std(interp: &mut Interpreter) {
184    macro_rules! reg {
185        ($name:expr, $f:expr) => {{
186            let func = Arc::new(Mutex::new($f));
187            interp.register_dynamic_function($name, func);
188            interp.set_variable(
189                $name,
190                Value::Function(Box::new(FunctionValue::Named($name.into()))),
191            );
192        }};
193    }
194
195    fn to_printable(v: &Value) -> String {
196        match v {
197            Value::SingleString(s) => s.clone(),
198            Value::Int(i) => i.to_string(),
199            Value::Long(l) => l.to_string(),
200            Value::Float(f) => f.to_string(),
201            Value::Bool(b) => b.to_string(),
202            _ => format!("{:?}", v),
203        }
204    }
205
206    // Drain core/plugin iterators synchronously (like std:put/log)
207    fn drain_iterator(
208        interp: &mut Interpreter,
209        it: &core_mumu::parser::types::IteratorHandle,
210        newline: bool,
211    ) -> Result<(), String> {
212        match &it.kind {
213            IteratorKind::Core(state_arc) => {
214                let mut guard = state_arc
215                    .lock()
216                    .map_err(|_| "std (wasm): iterator lock error".to_string())?;
217                while !guard.done && guard.current < guard.end {
218                    let item = guard.current;
219                    guard.current += 1;
220                    if guard.current >= guard.end {
221                        guard.done = true;
222                    }
223                    drop(guard);
224                    let s = item.to_string();
225                    if newline {
226                        interp.push_print(&(s.clone() + "\n"));
227                    } else {
228                        interp.push_print(&s);
229                    }
230                    guard = state_arc
231                        .lock()
232                        .map_err(|_| "std (wasm): iterator lock error".to_string())?;
233                }
234                Ok(())
235            }
236            IteratorKind::Plugin(plugin_arc) => {
237                let mut plugin = plugin_arc
238                    .lock()
239                    .map_err(|_| "std (wasm): plugin iterator lock error".to_string())?;
240                loop {
241                    match plugin.next_value() {
242                        Ok(val) => {
243                            let s = to_printable(&val);
244                            if newline {
245                                interp.push_print(&(s.clone() + "\n"));
246                            } else {
247                                interp.push_print(&s);
248                            }
249                        }
250                        Err(e) if e == "NO_MORE_DATA" => break,
251                        Err(e) => return Err(e),
252                    }
253                }
254                Ok(())
255            }
256        }
257    }
258
259    // std:put(x)
260    fn std_put_bridge(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
261        if args.len() != 1 {
262            return Err(format!(
263                "std:put => expected exactly 1 argument, got {}",
264                args.len()
265            ));
266        }
267        let val = args.remove(0);
268        match &val {
269            Value::Iterator(handle) => {
270                drain_iterator(interp, handle, false)?;
271                Ok(val)
272            }
273            other => {
274                let s = to_printable(other);
275                interp.push_print(&s);
276                Ok(val)
277            }
278        }
279    }
280
281    // std:log(x)
282    fn std_log_bridge(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
283        if args.len() != 1 {
284            return Err(format!(
285                "std:log => expected exactly 1 argument, got {}",
286                args.len()
287            ));
288        }
289        let val = args.remove(0);
290        match &val {
291            Value::Iterator(handle) => {
292                drain_iterator(interp, handle, true)?;
293                Ok(val)
294            }
295            other => {
296                let s = to_printable(other);
297                interp.push_print(&(s + "\n"));
298                Ok(val)
299            }
300        }
301    }
302
303    // Deep string (slightly condensed; uses core value_to_string for stability)
304    fn std_deep_bridge(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
305        if args.len() != 1 {
306            return Err(format!(
307                "std:deep => expected exactly 1 argument, got {}",
308                args.len()
309            ));
310        }
311        let val = args.remove(0);
312        let s = core_mumu::value_to_string(&val);
313        interp.push_print(&(s.clone() + "\n"));
314        Ok(val)
315    }
316
317    reg!("std:put", std_put_bridge);
318    reg!("std:log", std_log_bridge);
319    reg!("std:deep", std_deep_bridge);
320}
321
322impl WasmMumu {
323    /// Gather all known symbol names (dynamic functions + top-level vars).
324    fn collect_symbols(&self) -> Vec<String> {
325        let mut set: BTreeSet<String> = BTreeSet::new();
326
327        // Dynamic functions (bridges)
328        for k in self.interp.get_dynamic_functions_for_clone().keys() {
329            set.insert(k.clone());
330        }
331        // Variables currently bound in the scopes
332        for k in self.interp.get_variables().keys() {
333            set.insert(k.clone());
334        }
335
336        set.into_iter().collect()
337    }
338}
339
340#[wasm_bindgen]
341impl WasmMumu {
342    #[wasm_bindgen(constructor)]
343    pub fn new(verbose: bool) -> WasmMumu {
344        WasmMumu {
345            interp: bootstrap_interpreter(verbose),
346            verbose,
347            drained_cursor: 0,
348        }
349    }
350
351    /// Recreate the interpreter in-place (alternative to constructing a new WasmMumu on the JS side).
352    #[wasm_bindgen]
353    pub fn reset(&mut self) {
354        self.interp = bootstrap_interpreter(self.verbose);
355        self.drained_cursor = 0;
356    }
357
358    /// Execute a full MuMu/Lava source string and return a structured result.
359    /// JS receives an object: { ok, value, value_string, value_type, prints }.
360    #[wasm_bindgen]
361    pub fn exec(&mut self, source: &str) -> JsValue {
362        let res = (|| -> Result<ExecOk, ExecErr> {
363            let tokens = tokenize(source, self.interp.is_verbose())
364                .map_err(|e| ExecErr {
365                    ok: false,
366                    error: e.to_string(),
367                })?;
368            let ast = parse_tokens(&tokens, self.interp.is_verbose())
369                .map_err(|e| ExecErr {
370                    ok: false,
371                    error: e.to_string(),
372                })?;
373
374            let mut last = Value::Bool(true);
375            for stmt in &ast {
376                last = self
377                    .interp
378                    .exec_statement(stmt)
379                    .map_err(|e| ExecErr { ok: false, error: e })?;
380            }
381
382            let view = to_js_view(&last);
383            let value_string = core_mumu::value_to_string(&last);
384            let value_type = short_type_name(&last).to_string();
385            let prints = self.interp.get_prints().clone();
386
387            Ok(ExecOk {
388                ok: true,
389                value: view,
390                value_string,
391                value_type,
392                prints,
393            })
394        })();
395
396        match res {
397            Ok(ok) => swb::to_value(&ok).unwrap(),
398            Err(err) => swb::to_value(&err).unwrap(),
399        }
400    }
401
402    /// Drive any non-blocking iterator printers once (slog/sput with Iterator).
403    /// Returns number of items processed this tick.
404    #[wasm_bindgen]
405    pub fn poll(&mut self) -> u32 {
406        self.interp.poll_all() as u32
407    }
408
409    /// Return **all** known symbols (functions + variables) for client-side completion menus.
410    #[wasm_bindgen]
411    pub fn symbols(&self) -> JsValue {
412        let v = self.collect_symbols();
413        swb::to_value(&v).unwrap()
414    }
415
416    /// Return completions for a case-insensitive `prefix`, sorted and deduped (REPL-like).
417    #[wasm_bindgen]
418    pub fn complete(&self, prefix: &str) -> JsValue {
419        let lower = prefix.to_ascii_lowercase();
420        let mut out: Vec<String> = self
421            .collect_symbols()
422            .into_iter()
423            .filter(|s| s.to_ascii_lowercase().starts_with(&lower))
424            .collect();
425        out.sort();
426        out.dedup();
427        swb::to_value(&out).unwrap()
428    }
429
430    /// Drain and return only the *new* lines printed since the previous drain.
431    /// (If called for the first time, returns everything printed so far.)
432    #[wasm_bindgen]
433    pub fn drain_prints(&mut self) -> JsValue {
434        let all = self.interp.get_prints().clone();
435        let start = if self.drained_cursor <= all.len() {
436            self.drained_cursor
437        } else {
438            0
439        };
440        let new: Vec<String> = all[start..].to_vec();
441        self.drained_cursor = all.len();
442        swb::to_value(&new).unwrap()
443    }
444
445    /// Enable/disable verbose parser/interpreter tracing.
446    #[wasm_bindgen]
447    pub fn set_verbose(&mut self, v: bool) {
448        self.verbose = v;
449        self.interp.set_verbose(v);
450    }
451
452    /// Return a simple version string from the core crate (if any) or a static label.
453    #[wasm_bindgen]
454    pub fn version(&self) -> String {
455        // If core_mumu exposes a version at runtime in future, plumb it here.
456        "wasm-mumu/0.1".to_string()
457    }
458}