Skip to main content

monkey_wasm/
lib.rs

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