Skip to main content

monkey_wasm/
lib.rs

1mod utils;
2
3use crate::utils::set_panic_hook;
4use compiler::compiler::Compiler;
5use compiler::snapshot::{read_bytecode, write_bytecode};
6use compiler::snapshot_layout::describe_bytecode;
7use monkey_asm::emitter::AsmDialect;
8use monkey_asm::lower::lower_node;
9use object::builtins::BuiltIns;
10use parser::ast::Node;
11use parser::parse as parser_pase;
12use parser::validation::validate_program;
13use parser::{parse_ast_json_string, parse_ast_lossless_json_string, stringify_integer_literals};
14use wasm_bindgen::prelude::*;
15use wasm_bindgen::throw_str;
16
17const PLAYGROUND_GC_INSTRUCTION_BUDGET: usize = 10_000;
18
19// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
20// allocator.
21#[cfg(feature = "wee_alloc")]
22#[global_allocator]
23static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
24
25#[wasm_bindgen]
26pub fn parse(input: &str) -> String {
27    set_panic_hook();
28    match parse_ast_json_string(input) {
29        Ok(node) => node.to_string(),
30        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
31    }
32}
33
34/// Parse Monkey source to JSON while encoding every i64 literal as a decimal
35/// string so JavaScript consumers do not lose integer precision.
36#[wasm_bindgen]
37pub fn parse_lossless(input: &str) -> String {
38    set_panic_hook();
39    match parse_ast_lossless_json_string(input) {
40        Ok(node) => node,
41        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
42    }
43}
44
45/// Parse *and* validate Monkey source, returning a tagged JSON envelope the
46/// linter consumes. Unlike [`parse_lossless`], this runs `parser::validation`
47/// (the semantic pass the interpreter and compiler share) so callers see
48/// undefined-variable, misplaced-`this`, and constructor-return errors — not
49/// just syntax errors. `analyze` is the linter's single entry into the Rust
50/// side: TypeScript never re-implements parse or validation.
51///
52/// Failures are data in the envelope, not JavaScript exceptions:
53/// `{ status: "error", stage, message, span? }`. Parser errors are plain
54/// strings without a span; validation errors carry a UTF-8 byte span. On
55/// success the AST is serialized losslessly (i64 literals as decimal strings),
56/// matching [`parse_lossless`]: `{ status: "ok", program }`.
57///
58/// Standalone source is validated against the same predefined globals a fresh
59/// interpreter/compiler sees — the full builtin table (`len`, `puts`, `first`,
60/// `last`, `rest`, `push`, `print`).
61#[wasm_bindgen]
62pub fn analyze_lossless(input: &str) -> String {
63    set_panic_hook();
64
65    let envelope = match analyze_envelope(input) {
66        Ok(program) => serde_json::json!({
67            "status": "ok",
68            "program": program,
69        }),
70        Err((stage, message, span)) => serde_json::json!({
71            "status": "error",
72            "stage": stage,
73            "message": message,
74            "span": span.map(|(start, end)| serde_json::json!({ "start": start, "end": end })),
75        }),
76    };
77    serde_json::to_string(&envelope).expect("analyze envelope serialization should not fail")
78}
79
80type AnalyzeFailure = (&'static str, String, Option<(usize, usize)>);
81
82fn analyze_envelope(input: &str) -> Result<serde_json::Value, AnalyzeFailure> {
83    let node = parser_pase(input).map_err(|errors| {
84        let message = errors
85            .first()
86            .cloned()
87            .unwrap_or_else(|| "unknown parse error".to_string());
88        ("parse", message, None)
89    })?;
90    let Node::Program(program) = &node else {
91        unreachable!("parse always returns a Program node");
92    };
93
94    let predefined = BuiltIns
95        .iter()
96        .map(|builtin| builtin.name)
97        .collect::<Vec<_>>();
98    validate_program(program, &predefined)
99        .map_err(|error| ("validation", error.message, Some((error.span.start, error.span.end))))?;
100
101    let mut ast = serde_json::to_value(program).expect("AST serialization should not fail");
102    stringify_integer_literals(&mut ast);
103    Ok(ast)
104}
105
106#[wasm_bindgen]
107pub fn compile(input: &str) -> String {
108    set_panic_hook();
109
110    let program = match parser_pase(input) {
111        Ok(ast) => ast,
112        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
113    };
114    let mut compiler = Compiler::new();
115    match compiler.compile(&program) {
116        Ok(bytecode) => return bytecode.instructions.string(),
117        Err(e) => throw_str(format!("compile error: {}", e).as_str()),
118    }
119}
120
121#[wasm_bindgen]
122pub fn compile_detail(input: &str) -> String {
123    set_panic_hook();
124
125    let program = match parser_pase(input) {
126        Ok(ast) => ast,
127        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
128    };
129    let mut compiler = Compiler::new();
130    match compiler.compile(&program) {
131        Ok(bytecode) => return bytecode.string(),
132        Err(e) => throw_str(format!("compile error: {}", e).as_str()),
133    }
134}
135
136#[wasm_bindgen]
137pub fn compile_with_debug(input: &str) -> String {
138    set_panic_hook();
139
140    let program = match parser_pase(input) {
141        Ok(ast) => ast,
142        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
143    };
144    let mut compiler = Compiler::new();
145    match compiler.compile(&program) {
146        Ok(bytecode) => match serde_json::to_string(&bytecode.debug_view()) {
147            Ok(json) => json,
148            Err(e) => throw_str(format!("json error: {}", e).as_str()),
149        },
150        Err(e) => throw_str(format!("compile error: {}", e).as_str()),
151    }
152}
153
154/// Execute Monkey source on the cycle-collecting VM and return a tagged JSON envelope.
155///
156/// User parse, compile, runtime, and execution-limit failures are data in the envelope,
157/// not JavaScript exceptions. This keeps the playground's Run GC path deterministic.
158#[wasm_bindgen]
159pub fn run_gc_with_report(input: &str) -> String {
160    set_panic_hook();
161
162    let envelope =
163        match gc::run_source_with_report_classified(input, PLAYGROUND_GC_INSTRUCTION_BUDGET) {
164            Ok(success) => serde_json::json!({
165                "status": "ok",
166                "result": success.result,
167                "report": success.report,
168            }),
169            Err(error) => serde_json::json!({
170                "status": "error",
171                "stage": error.stage,
172                "kind": error.kind,
173                "message": error.message,
174                "span": error.span,
175            }),
176        };
177
178    serde_json::to_string(&envelope).expect("GC run envelope serialization should not fail")
179}
180
181/// Compile Monkey source to AArch64 assembly and return a tagged JSON envelope
182/// of per-line `text`/`kind`/`span` records for the playground's godbolt-style
183/// ARM64 view (arm64 backend design §12 V1).
184///
185/// The browser only renders the text `monkey-asm emit` would produce — nothing
186/// executes arm64 here. Parse and lowering failures are data in the envelope,
187/// not JavaScript exceptions, mirroring [`run_gc_with_report`].
188#[wasm_bindgen]
189pub fn compile_to_arm64(input: &str) -> String {
190    set_panic_hook();
191
192    let envelope = match arm64_envelope(input) {
193        Ok(envelope) => envelope,
194        Err((stage, message, span)) => serde_json::json!({
195            "status": "error",
196            "stage": stage,
197            "kind": stage,
198            "message": message,
199            "span": span.map(|(start, end)| serde_json::json!({ "start": start, "end": end })),
200        }),
201    };
202    serde_json::to_string(&envelope).expect("arm64 envelope serialization should not fail")
203}
204
205type Arm64Failure = (&'static str, String, Option<(usize, usize)>);
206
207fn arm64_envelope(input: &str) -> Result<serde_json::Value, Arm64Failure> {
208    let node = parser_pase(input).map_err(|errors| {
209        let message = errors
210            .first()
211            .cloned()
212            .unwrap_or_else(|| "unknown parse error".to_string());
213        ("parse", message, None)
214    })?;
215    // The playground always shows the Linux/ELF spelling (design §12).
216    let assembly = lower_node(input, &node, AsmDialect::LinuxElf, false)
217        .map_err(|error| ("compile", error.message, error.span))?;
218
219    // `Assembly` guarantees one `line_spans` entry per `\n`-terminated line.
220    let lines: Vec<serde_json::Value> = assembly
221        .text
222        .lines()
223        .zip(assembly.line_spans.iter())
224        .map(|(text, span)| {
225            serde_json::json!({
226                "text": text,
227                "kind": arm64_line_kind(text),
228                "span": span.map(|(start, end)| serde_json::json!({ "start": start, "end": end })),
229            })
230        })
231        .collect();
232    Ok(serde_json::json!({ "status": "ok", "lines": lines }))
233}
234
235/// Presentation-level line class for the playground: the emitter only ever
236/// writes `//` comments, so everything before the first `//` is the code part.
237fn arm64_line_kind(text: &str) -> &'static str {
238    let code = match text.find("//") {
239        Some(index) => &text[..index],
240        None => text,
241    };
242    let trimmed = code.trim();
243    if trimmed.is_empty() {
244        if text.trim().is_empty() {
245            "blank"
246        } else {
247            "comment"
248        }
249    } else if trimmed.ends_with(':') {
250        "label"
251    } else if trimmed.starts_with('.') {
252        "directive"
253    } else {
254        "code"
255    }
256}
257
258/// Compile Monkey source into a `.mbc` snapshot and return a tagged JSON envelope
259/// with the raw bytes (lowercase hex) plus a byte-range annotation of the container
260/// layout for the playground inspector.
261///
262/// User parse and compile failures are data in the envelope, not JavaScript
263/// exceptions, mirroring [`run_gc_with_report`].
264#[wasm_bindgen]
265pub fn compile_to_snapshot(input: &str, strip_debug: bool) -> String {
266    set_panic_hook();
267
268    let envelope = match snapshot_envelope(input, strip_debug) {
269        Ok(envelope) => envelope,
270        Err((stage, message)) => serde_json::json!({
271            "status": "error",
272            "stage": stage,
273            "kind": stage,
274            "message": message,
275        }),
276    };
277    serde_json::to_string(&envelope).expect("snapshot envelope serialization should not fail")
278}
279
280fn snapshot_envelope(
281    input: &str,
282    strip_debug: bool,
283) -> Result<serde_json::Value, (&'static str, String)> {
284    let program = parser_pase(input).map_err(|errors| {
285        let message = errors
286            .first()
287            .cloned()
288            .unwrap_or_else(|| "unknown parse error".to_string());
289        ("parse", message)
290    })?;
291    let mut compiler = Compiler::new();
292    let bytecode = compiler
293        .compile(&program)
294        .map_err(|message| ("compile", message))?;
295    let bytes = write_bytecode(&bytecode, strip_debug)
296        .map_err(|error| ("snapshot", format!("{:?}", error)))?;
297    let layout = describe_bytecode(&bytes).map_err(|error| ("snapshot", format!("{:?}", error)))?;
298    Ok(serde_json::json!({
299        "status": "ok",
300        "bytesHex": hex_encode(&bytes),
301        "layout": layout,
302    }))
303}
304
305fn hex_encode(bytes: &[u8]) -> String {
306    use std::fmt::Write;
307
308    let mut out = String::with_capacity(bytes.len() * 2);
309    for byte in bytes {
310        write!(out, "{:02x}", byte).expect("writing to a String cannot fail");
311    }
312    out
313}
314
315/// Execute `.mbc` snapshot bytes on the cycle-collecting VM — the browser twin of
316/// `monkey-gc run foo.mbc`, running the same VM with raise-site error
317/// classification so the envelope can carry a stable `kind`.
318///
319/// The buffer is untrusted input: it goes through the validating snapshot reader
320/// before the VM. Failures are data in the envelope — stage `snapshot` when the
321/// bytes are rejected, `runtime` when the VM errors (the span is only present
322/// when the snapshot kept its debug info).
323#[wasm_bindgen]
324pub fn run_snapshot(bytes: &[u8]) -> String {
325    set_panic_hook();
326
327    let envelope = match read_bytecode(bytes) {
328        Ok(bytecode) => {
329            let mut vm = gc::GcVM::new(bytecode);
330            match vm
331                .run_with_budget_classified(PLAYGROUND_GC_INSTRUCTION_BUDGET)
332                .map(|()| vm.last_result_string())
333            {
334                Ok(result) => serde_json::json!({
335                    "status": "ok",
336                    "result": result,
337                }),
338                Err(error) => serde_json::json!({
339                    "status": "error",
340                    "stage": "runtime",
341                    "kind": error.kind,
342                    "message": error.message,
343                    "span": error.span,
344                }),
345            }
346        }
347        Err(error) => serde_json::json!({
348            "status": "error",
349            "stage": "snapshot",
350            "kind": "invalidSnapshot",
351            "message": format!("{:?}", error),
352            "span": null,
353        }),
354    };
355    serde_json::to_string(&envelope).expect("snapshot run envelope serialization should not fail")
356}
357
358/// Execute an untrusted snapshot on a fresh GC VM and capture observable
359/// output. `stdout` is present on both success and failure so callers can
360/// compare programs that print before raising an error.
361#[wasm_bindgen]
362pub fn run_snapshot_with_output(bytes: &[u8]) -> String {
363    set_panic_hook();
364
365    let envelope = match read_bytecode(bytes) {
366        Ok(bytecode) => {
367            let (result, stdout) =
368                gc::run_bytecode_with_output(bytecode, PLAYGROUND_GC_INSTRUCTION_BUDGET);
369            match result {
370                Ok(result) => serde_json::json!({
371                    "status": "ok",
372                    "result": result,
373                    "stdout": stdout,
374                }),
375                Err(error) => serde_json::json!({
376                    "status": "error",
377                    "stage": "runtime",
378                    "kind": error.kind,
379                    "message": error.message,
380                    "span": error.span,
381                    "stdout": stdout,
382                }),
383            }
384        }
385        Err(error) => serde_json::json!({
386            "status": "error",
387            "stage": "snapshot",
388            "kind": "invalidSnapshot",
389            "message": format!("{:?}", error),
390            "span": null,
391            "stdout": "",
392        }),
393    };
394    serde_json::to_string(&envelope).expect("snapshot run envelope serialization should not fail")
395}