Skip to main content

elly_wasm/
lib.rs

1//! A `wasm32-unknown-unknown` wrapper exposing the [`elly`] parse + eval
2//! pipeline to JavaScript.
3//!
4//! The `elly` and `muon` crates are `no_std`; this thin shim links `std` only
5//! for the allocator that `wasm32-unknown-unknown` provides, so no
6//! `wasm-bindgen` (or any post-processing tool) is needed — the `.wasm` is
7//! loaded directly.
8//!
9//! ## ABI
10//!
11//! Strings cross the boundary as `(ptr, len)` into wasm linear memory:
12//!
13//! - `elly_alloc(len) -> ptr` — reserve `len` bytes for JS to write UTF-8 into.
14//! - `elly_run(ptr, len) -> out` — parse and evaluate those bytes;
15//!   returns a pointer to a length-prefixed result: a little-endian `u32` byte
16//!   length followed by that many bytes of UTF-8 JSON (see below).
17//! - `elly_free(ptr, len)` — release a block obtained from `elly_alloc`.
18//!
19//! The JSON result is one of:
20//!
21//! - `{"ast":<expr>,"value":"<display>"}` — parsed and evaluated. `<expr>` is
22//!   the tagged AST dump (`elly::Expr::to_json`).
23//! - `{"ast":<expr>,"error":{"stage":"eval","raised":"<display>"}}` — parsed,
24//!   but evaluation raised a value that unwound to the top level (an explicit
25//!   `__Err.raise` or a host failure like `.div_by_zero`); the AST is still shown.
26//! - `{"error":{"stage":"parse","kind":"…"}}` — a valid Muon tree but not a
27//!   valid Elly program in this subset.
28//! - `{"error":{"stage":"syntax","offset":N,"kind":"…"}}` — the syntax layer
29//!   (Muon) rejected the input.
30
31use elly_core as elly;
32use elly_core::Error;
33
34/// Reserve `len` bytes of linear memory and hand ownership to the caller.
35#[no_mangle]
36pub extern "C" fn elly_alloc(len: usize) -> *mut u8 {
37    let mut buf = Vec::<u8>::with_capacity(len);
38    let ptr = buf.as_mut_ptr();
39    core::mem::forget(buf);
40    ptr
41}
42
43/// Release a block previously returned by [`elly_alloc`] (same `len`).
44///
45/// # Safety
46/// `ptr`/`len` must come from a prior `elly_alloc(len)` and not be freed twice.
47#[no_mangle]
48pub unsafe extern "C" fn elly_free(ptr: *mut u8, len: usize) {
49    drop(Vec::from_raw_parts(ptr, 0, len));
50}
51
52/// Run `len` UTF-8 bytes at `ptr` through the pipeline; return length-prefixed
53/// JSON.
54///
55/// # Safety
56/// `ptr`/`len` must describe a valid, initialized block of linear memory (e.g.
57/// one from [`elly_alloc`] that JS has filled). The returned block is owned by
58/// the caller and must be released with [`elly_free`].
59#[no_mangle]
60pub unsafe extern "C" fn elly_run(ptr: *const u8, len: usize) -> *mut u8 {
61    let input = core::slice::from_raw_parts(ptr, len);
62    let json = match core::str::from_utf8(input) {
63        Ok(src) => run(src),
64        Err(_) => r#"{"error":{"stage":"parse","offset":0,"kind":"NotUtf8"}}"#.to_string(),
65    };
66
67    // Frame as: [u32 little-endian length][UTF-8 bytes].
68    let bytes = json.into_bytes();
69    let mut out = Vec::with_capacity(4 + bytes.len());
70    out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
71    out.extend_from_slice(&bytes);
72    let p = out.as_mut_ptr();
73    core::mem::forget(out);
74    p
75}
76
77/// Parse → eval, producing the JSON described in the module docs. The error
78/// `stage` mirrors the pipeline vocabulary: `syntax` (the Muon layer), `parse`
79/// (Muon tree → Elly AST), `eval` (a raise that unwound to the top level).
80fn run(src: &str) -> String {
81    let expr = match elly::parse(src) {
82        Ok(expr) => expr,
83        Err(Error::Syntax(e)) => {
84            return format!(
85                r#"{{"error":{{"stage":"syntax","offset":{},"kind":{}}}}}"#,
86                e.offset,
87                json_string(&format!("{:?}", e.kind))
88            );
89        }
90        // The `{:?}` of a `ParseError` may embed a quoted payload (e.g.
91        // `UnboundName("__List_")`), so escape it as a JSON string rather than
92        // splicing the raw debug text — otherwise the inner quotes break the JSON.
93        Err(Error::Parse(e)) => {
94            return format!(
95                r#"{{"error":{{"stage":"parse","kind":{}}}}}"#,
96                json_string(&format!("{:?}", e))
97            );
98        }
99    };
100
101    let ast = expr.to_json();
102    match elly::eval(&expr) {
103        Ok(value) => format!(
104            r#"{{"ast":{},"value":{}}}"#,
105            ast,
106            json_string(&value.to_display())
107        ),
108        // A raise unwound to the top level: report the raised value's display.
109        Err(e) => format!(
110            r#"{{"ast":{},"error":{{"stage":"eval","raised":{}}}}}"#,
111            ast,
112            json_string(&e.value().to_display())
113        ),
114    }
115}
116
117/// Quote and escape `s` as a JSON string literal.
118fn json_string(s: &str) -> String {
119    let mut out = String::with_capacity(s.len() + 2);
120    out.push('"');
121    for ch in s.chars() {
122        match ch {
123            '"' => out.push_str("\\\""),
124            '\\' => out.push_str("\\\\"),
125            '\n' => out.push_str("\\n"),
126            '\r' => out.push_str("\\r"),
127            '\t' => out.push_str("\\t"),
128            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
129            c => out.push(c),
130        }
131    }
132    out.push('"');
133    out
134}