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 parser::parse as parser_pase;
10use parser::parse_ast_json_string;
11use wasm_bindgen::prelude::*;
12use wasm_bindgen::throw_str;
13
14const PLAYGROUND_GC_INSTRUCTION_BUDGET: usize = 10_000;
15
16// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
17// allocator.
18#[cfg(feature = "wee_alloc")]
19#[global_allocator]
20static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
21
22#[wasm_bindgen]
23pub fn parse(input: &str) -> String {
24    set_panic_hook();
25    match parse_ast_json_string(input) {
26        Ok(node) => node.to_string(),
27        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
28    }
29}
30
31#[wasm_bindgen]
32pub fn compile(input: &str) -> String {
33    set_panic_hook();
34
35    let program = match parser_pase(input) {
36        Ok(ast) => ast,
37        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
38    };
39    let mut compiler = Compiler::new();
40    match compiler.compile(&program) {
41        Ok(bytecode) => return bytecode.instructions.string(),
42        Err(e) => throw_str(format!("compile error: {}", e).as_str()),
43    }
44}
45
46#[wasm_bindgen]
47pub fn compile_detail(input: &str) -> String {
48    set_panic_hook();
49
50    let program = match parser_pase(input) {
51        Ok(ast) => ast,
52        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
53    };
54    let mut compiler = Compiler::new();
55    match compiler.compile(&program) {
56        Ok(bytecode) => return bytecode.string(),
57        Err(e) => throw_str(format!("compile error: {}", e).as_str()),
58    }
59}
60
61#[wasm_bindgen]
62pub fn compile_with_debug(input: &str) -> String {
63    set_panic_hook();
64
65    let program = match parser_pase(input) {
66        Ok(ast) => ast,
67        Err(e) => throw_str(format!("parse error: {}", e[0]).as_str()),
68    };
69    let mut compiler = Compiler::new();
70    match compiler.compile(&program) {
71        Ok(bytecode) => match serde_json::to_string(&bytecode.debug_view()) {
72            Ok(json) => json,
73            Err(e) => throw_str(format!("json error: {}", e).as_str()),
74        },
75        Err(e) => throw_str(format!("compile error: {}", e).as_str()),
76    }
77}
78
79/// Execute Monkey source on the cycle-collecting VM and return a tagged JSON envelope.
80///
81/// User parse, compile, runtime, and execution-limit failures are data in the envelope,
82/// not JavaScript exceptions. This keeps the playground's Run GC path deterministic.
83#[wasm_bindgen]
84pub fn run_gc_with_report(input: &str) -> String {
85    set_panic_hook();
86
87    let envelope = match gc::run_source_with_report(input, PLAYGROUND_GC_INSTRUCTION_BUDGET) {
88        Ok(success) => serde_json::json!({
89            "status": "ok",
90            "result": success.result,
91            "report": success.report,
92        }),
93        Err(error) => serde_json::json!({
94            "status": "error",
95            "stage": error.stage,
96            "message": error.message,
97            "span": error.span,
98        }),
99    };
100
101    serde_json::to_string(&envelope).expect("GC run envelope serialization should not fail")
102}
103
104/// Compile Monkey source to AArch64 assembly and return a tagged JSON envelope
105/// of per-line `text`/`kind`/`span` records for the playground's godbolt-style
106/// ARM64 view (arm64 backend design §12 V1).
107///
108/// The browser only renders the text `monkey-asm emit` would produce — nothing
109/// executes arm64 here. Parse and lowering failures are data in the envelope,
110/// not JavaScript exceptions, mirroring [`run_gc_with_report`].
111#[wasm_bindgen]
112pub fn compile_to_arm64(input: &str) -> String {
113    set_panic_hook();
114
115    let envelope = match arm64_envelope(input) {
116        Ok(envelope) => envelope,
117        Err((stage, message, span)) => serde_json::json!({
118            "status": "error",
119            "stage": stage,
120            "message": message,
121            "span": span.map(|(start, end)| serde_json::json!({ "start": start, "end": end })),
122        }),
123    };
124    serde_json::to_string(&envelope).expect("arm64 envelope serialization should not fail")
125}
126
127type Arm64Failure = (&'static str, String, Option<(usize, usize)>);
128
129fn arm64_envelope(input: &str) -> Result<serde_json::Value, Arm64Failure> {
130    let node = parser_pase(input).map_err(|errors| {
131        let message = errors
132            .first()
133            .cloned()
134            .unwrap_or_else(|| "unknown parse error".to_string());
135        ("parse", message, None)
136    })?;
137    // The playground always shows the Linux/ELF spelling (design §12).
138    let assembly = lower_node(input, &node, AsmDialect::LinuxElf, false)
139        .map_err(|error| ("compile", error.message, error.span))?;
140
141    // `Assembly` guarantees one `line_spans` entry per `\n`-terminated line.
142    let lines: Vec<serde_json::Value> = assembly
143        .text
144        .lines()
145        .zip(assembly.line_spans.iter())
146        .map(|(text, span)| {
147            serde_json::json!({
148                "text": text,
149                "kind": arm64_line_kind(text),
150                "span": span.map(|(start, end)| serde_json::json!({ "start": start, "end": end })),
151            })
152        })
153        .collect();
154    Ok(serde_json::json!({ "status": "ok", "lines": lines }))
155}
156
157/// Presentation-level line class for the playground: the emitter only ever
158/// writes `//` comments, so everything before the first `//` is the code part.
159fn arm64_line_kind(text: &str) -> &'static str {
160    let code = match text.find("//") {
161        Some(index) => &text[..index],
162        None => text,
163    };
164    let trimmed = code.trim();
165    if trimmed.is_empty() {
166        if text.trim().is_empty() {
167            "blank"
168        } else {
169            "comment"
170        }
171    } else if trimmed.ends_with(':') {
172        "label"
173    } else if trimmed.starts_with('.') {
174        "directive"
175    } else {
176        "code"
177    }
178}
179
180/// Compile Monkey source into a `.mbc` snapshot and return a tagged JSON envelope
181/// with the raw bytes (lowercase hex) plus a byte-range annotation of the container
182/// layout for the playground inspector.
183///
184/// User parse and compile failures are data in the envelope, not JavaScript
185/// exceptions, mirroring [`run_gc_with_report`].
186#[wasm_bindgen]
187pub fn compile_to_snapshot(input: &str, strip_debug: bool) -> String {
188    set_panic_hook();
189
190    let envelope = match snapshot_envelope(input, strip_debug) {
191        Ok(envelope) => envelope,
192        Err((stage, message)) => serde_json::json!({
193            "status": "error",
194            "stage": stage,
195            "message": message,
196        }),
197    };
198    serde_json::to_string(&envelope).expect("snapshot envelope serialization should not fail")
199}
200
201fn snapshot_envelope(
202    input: &str,
203    strip_debug: bool,
204) -> Result<serde_json::Value, (&'static str, String)> {
205    let program = parser_pase(input).map_err(|errors| {
206        let message = errors
207            .first()
208            .cloned()
209            .unwrap_or_else(|| "unknown parse error".to_string());
210        ("parse", message)
211    })?;
212    let mut compiler = Compiler::new();
213    let bytecode = compiler
214        .compile(&program)
215        .map_err(|message| ("compile", message))?;
216    let bytes = write_bytecode(&bytecode, strip_debug)
217        .map_err(|error| ("snapshot", format!("{:?}", error)))?;
218    let layout = describe_bytecode(&bytes).map_err(|error| ("snapshot", format!("{:?}", error)))?;
219    Ok(serde_json::json!({
220        "status": "ok",
221        "bytesHex": hex_encode(&bytes),
222        "layout": layout,
223    }))
224}
225
226fn hex_encode(bytes: &[u8]) -> String {
227    use std::fmt::Write;
228
229    let mut out = String::with_capacity(bytes.len() * 2);
230    for byte in bytes {
231        write!(out, "{:02x}", byte).expect("writing to a String cannot fail");
232    }
233    out
234}
235
236/// Execute `.mbc` snapshot bytes on the cycle-collecting VM — the browser twin of
237/// `monkey-gc run foo.mbc`, sharing its execution path (`gc::run_bytecode`).
238///
239/// The buffer is untrusted input: it goes through the validating snapshot reader
240/// before the VM. Failures are data in the envelope — stage `snapshot` when the
241/// bytes are rejected, `runtime` when the VM errors (the span is only present
242/// when the snapshot kept its debug info).
243#[wasm_bindgen]
244pub fn run_snapshot(bytes: &[u8]) -> String {
245    set_panic_hook();
246
247    let envelope = match read_bytecode(bytes) {
248        Ok(bytecode) => match gc::run_bytecode(bytecode, PLAYGROUND_GC_INSTRUCTION_BUDGET) {
249            Ok(result) => serde_json::json!({
250                "status": "ok",
251                "result": result,
252            }),
253            Err(error) => serde_json::json!({
254                "status": "error",
255                "stage": "runtime",
256                "message": error.message,
257                "span": error.span,
258            }),
259        },
260        Err(error) => serde_json::json!({
261            "status": "error",
262            "stage": "snapshot",
263            "message": format!("{:?}", error),
264            "span": null,
265        }),
266    };
267    serde_json::to_string(&envelope).expect("snapshot run envelope serialization should not fail")
268}