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}