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::Error;
32
33/// Reserve `len` bytes of linear memory and hand ownership to the caller.
34#[no_mangle]
35pub extern "C" fn elly_alloc(len: usize) -> *mut u8 {
36    let mut buf = Vec::<u8>::with_capacity(len);
37    let ptr = buf.as_mut_ptr();
38    core::mem::forget(buf);
39    ptr
40}
41
42/// Release a block previously returned by [`elly_alloc`] (same `len`).
43///
44/// # Safety
45/// `ptr`/`len` must come from a prior `elly_alloc(len)` and not be freed twice.
46#[no_mangle]
47pub unsafe extern "C" fn elly_free(ptr: *mut u8, len: usize) {
48    drop(Vec::from_raw_parts(ptr, 0, len));
49}
50
51/// Run `len` UTF-8 bytes at `ptr` through the pipeline; return length-prefixed
52/// JSON.
53///
54/// # Safety
55/// `ptr`/`len` must describe a valid, initialized block of linear memory (e.g.
56/// one from [`elly_alloc`] that JS has filled). The returned block is owned by
57/// the caller and must be released with [`elly_free`].
58#[no_mangle]
59pub unsafe extern "C" fn elly_run(ptr: *const u8, len: usize) -> *mut u8 {
60    let input = core::slice::from_raw_parts(ptr, len);
61    let json = match core::str::from_utf8(input) {
62        Ok(src) => run(src),
63        Err(_) => r#"{"error":{"stage":"parse","offset":0,"kind":"NotUtf8"}}"#.to_string(),
64    };
65
66    // Frame as: [u32 little-endian length][UTF-8 bytes].
67    let bytes = json.into_bytes();
68    let mut out = Vec::with_capacity(4 + bytes.len());
69    out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
70    out.extend_from_slice(&bytes);
71    let p = out.as_mut_ptr();
72    core::mem::forget(out);
73    p
74}
75
76/// Parse → eval, producing the JSON described in the module docs. The error
77/// `stage` mirrors the pipeline vocabulary: `syntax` (the Muon layer), `parse`
78/// (Muon tree → Elly AST), `eval` (a raise that unwound to the top level).
79fn run(src: &str) -> String {
80    let expr = match elly::parse(src) {
81        Ok(expr) => expr,
82        Err(Error::Syntax(e)) => {
83            return format!(
84                r#"{{"error":{{"stage":"syntax","offset":{},"kind":{}}}}}"#,
85                e.offset,
86                json_string(&format!("{:?}", e.kind))
87            );
88        }
89        // The `{:?}` of a `ParseError` may embed a quoted payload (e.g.
90        // `UnboundName("__List_")`), so escape it as a JSON string rather than
91        // splicing the raw debug text — otherwise the inner quotes break the JSON.
92        Err(Error::Parse(e)) => {
93            return format!(
94                r#"{{"error":{{"stage":"parse","kind":{}}}}}"#,
95                json_string(&format!("{:?}", e))
96            );
97        }
98    };
99
100    let ast = expr.to_json();
101    match elly::eval(&expr) {
102        Ok(value) => format!(
103            r#"{{"ast":{},"value":{}}}"#,
104            ast,
105            json_string(&value.to_display())
106        ),
107        // A raise unwound to the top level: report the raised value's display.
108        Err(e) => format!(
109            r#"{{"ast":{},"error":{{"stage":"eval","raised":{}}}}}"#,
110            ast,
111            json_string(&e.value().to_display())
112        ),
113    }
114}
115
116/// Quote and escape `s` as a JSON string literal.
117fn json_string(s: &str) -> String {
118    let mut out = String::with_capacity(s.len() + 2);
119    out.push('"');
120    for ch in s.chars() {
121        match ch {
122            '"' => out.push_str("\\\""),
123            '\\' => out.push_str("\\\\"),
124            '\n' => out.push_str("\\n"),
125            '\r' => out.push_str("\\r"),
126            '\t' => out.push_str("\\t"),
127            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
128            c => out.push(c),
129        }
130    }
131    out.push('"');
132    out
133}