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, e.kind
86            );
87        }
88        Err(Error::Parse(e)) => {
89            return format!(r#"{{"error":{{"stage":"parse","kind":"{:?}"}}}}"#, e);
90        }
91    };
92
93    let ast = expr.to_json();
94    match elly::eval(&expr) {
95        Ok(value) => format!(
96            r#"{{"ast":{},"value":{}}}"#,
97            ast,
98            json_string(&value.to_display())
99        ),
100        // A raise unwound to the top level: report the raised value's display.
101        Err(e) => format!(
102            r#"{{"ast":{},"error":{{"stage":"eval","raised":{}}}}}"#,
103            ast,
104            json_string(&e.value().to_display())
105        ),
106    }
107}
108
109/// Quote and escape `s` as a JSON string literal.
110fn json_string(s: &str) -> String {
111    let mut out = String::with_capacity(s.len() + 2);
112    out.push('"');
113    for ch in s.chars() {
114        match ch {
115            '"' => out.push_str("\\\""),
116            '\\' => out.push_str("\\\\"),
117            '\n' => out.push_str("\\n"),
118            '\r' => out.push_str("\\r"),
119            '\t' => out.push_str("\\t"),
120            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
121            c => out.push(c),
122        }
123    }
124    out.push('"');
125    out
126}