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}