#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use monkey_wasm::{
analyze_lossless, compile_to_arm64, compile_to_snapshot, parse, parse_lossless,
run_gc_with_report, run_snapshot, run_snapshot_with_output,
};
use serde_json::Value;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn pass() {
let input = "let a = 3";
let r = parse(input);
println!("{}", r);
}
#[wasm_bindgen_test]
fn lossless_parse_preserves_i64_literals_as_strings() {
let ast: Value =
serde_json::from_str(&parse_lossless("[9007199254740993, 9223372036854775807]")).unwrap();
assert_eq!(ast["Program"]["body"][0]["elements"][0]["raw"], "9007199254740993");
assert_eq!(ast["Program"]["body"][0]["elements"][1]["raw"], "9223372036854775807");
}
#[wasm_bindgen_test]
fn lossless_analysis_rejects_uninitialized_self_reference_but_allows_recursion() {
let invalid: Value = serde_json::from_str(&analyze_lossless("let x = x;")).unwrap();
assert_eq!(invalid["status"], "error");
assert_eq!(invalid["stage"], "validation");
assert_eq!(invalid["span"]["start"], 8);
assert_eq!(invalid["span"]["end"], 9);
let recursive: Value = serde_json::from_str(&analyze_lossless(
"let f = fn(n) { if (n == 0) { 0 } else { f(n - 1) } }; f(1);",
))
.unwrap();
assert_eq!(recursive["status"], "ok");
}
fn run_gc(source: &str) -> Value {
serde_json::from_str(&run_gc_with_report(source)).expect("valid GC envelope JSON")
}
#[wasm_bindgen_test]
fn gc_success_envelope_reports_collected_instances() {
let envelope = run_gc(
r#"
class Node {
constructor(value) { this.value = value; }
connect(other) { this.next = other; }
}
let makeCycle = fn() {
let a = new Node("a");
let b = new Node("b");
a.connect(b);
b.connect(a);
};
makeCycle();
"#,
);
assert_eq!(envelope["status"], "ok");
assert_eq!(envelope["result"], "null");
assert_eq!(envelope["report"]["before"]["byValueKind"]["instance"], 2);
assert_eq!(envelope["report"]["after"]["byValueKind"]["instance"], 0);
assert_eq!(envelope["report"]["before"]["byValueKind"]["string"], 9);
assert_eq!(envelope["report"]["before"]["byValueKind"]["null"], 1);
assert_eq!(envelope["report"]["before"]["byValueKind"]["compiledFunction"], 4);
assert_eq!(envelope["report"]["before"]["byValueKind"]["other"], 0);
assert_eq!(envelope["report"]["collectedByValueKind"]["instance"], 2);
assert!(envelope["report"]["phases"]["trialDeletion"]["edgesVisited"].is_number());
assert!(envelope["report"]["phases"]["scan"]["garbageCandidates"].is_number());
let restored = envelope["report"]["phases"]["scan"]["restoredObjects"]
.as_array()
.expect("restored object summaries");
assert!(restored.iter().any(|object| {
object["kind"] == "closure"
&& object["label"]
.as_str()
.is_some_and(|label| label.starts_with("Closure(Node.connect)#"))
}));
let garbage = envelope["report"]["phases"]["scan"]["garbageCandidateObjects"]
.as_array()
.expect("garbage candidate summaries");
assert_eq!(garbage.len(), 2);
assert!(garbage.iter().all(|object| {
object["kind"] == "instance"
&& object["label"]
.as_str()
.is_some_and(|label| label.starts_with("Instance(Node)#"))
}));
assert!(envelope["report"]["phases"]["freeCycles"]["freed"].is_number());
assert!(envelope["report"]["objects"].is_array());
let global_roots = envelope["report"]["globalRoots"]
.as_array()
.expect("global roots");
let root_names: Vec<&str> = global_roots
.iter()
.filter_map(|root| root["name"].as_str())
.collect();
assert_eq!(root_names, vec!["Node", "makeCycle"]);
assert!(global_roots.iter().all(|root| root["objectId"].is_number()));
assert!(envelope["report"]["phases"]["trialDeletion"]["objectDecisions"].is_array());
assert!(envelope["report"]["phases"]["trialDeletion"]["visitedEdges"].is_array());
assert!(envelope["report"]["phases"]["scan"]["restorationWitnesses"].is_array());
let decisions = envelope["report"]["phases"]["trialDeletion"]["objectDecisions"]
.as_array()
.expect("object decisions");
assert!(decisions
.iter()
.any(|decision| { decision["decision"] == "candidate" && decision["final"] == "freed" }));
let edges = envelope["report"]["phases"]["trialDeletion"]["visitedEdges"]
.as_array()
.expect("visited edges");
assert!(edges.iter().any(|edge| {
edge["relation"]["kind"] == "instanceField" && edge["relation"]["name"] == "next"
}));
}
#[wasm_bindgen_test]
fn gc_if_arms_without_values_stay_stack_balanced() {
let let_tail = run_gc("let value = if (true) { let y = 1; } else { 2 }; value");
assert_eq!(let_tail["status"], "ok");
assert_eq!(let_tail["result"], "null");
let empty_arm = run_gc("if (true) {} 2;");
assert_eq!(empty_arm["status"], "ok");
assert_eq!(empty_arm["result"], "2");
}
#[wasm_bindgen_test]
fn gc_hash_edges_preserve_key_kinds_and_unicode_boundaries() {
let unicode_key = format!("{}ä¸", "a".repeat(63));
let source = format!(
r#"
let values = {{1: 1, "1": 2, true: 3, "true": 4, "{}": 5}};
values;
"#,
unicode_key
);
let envelope = run_gc(&source);
assert_eq!(envelope["status"], "ok");
let edges = envelope["report"]["phases"]["trialDeletion"]["visitedEdges"]
.as_array()
.expect("visited edges");
let has_hash_key = |key_kind: &str, key: &str| {
edges.iter().any(|edge| {
edge["relation"]["kind"] == "hashValue"
&& edge["relation"]["keyKind"] == key_kind
&& edge["relation"]["key"] == key
})
};
assert!(has_hash_key("integer", "1"));
assert!(has_hash_key("string", "1"));
assert!(has_hash_key("boolean", "true"));
assert!(has_hash_key("string", "true"));
assert!(has_hash_key("string", &format!("{}…", "a".repeat(63))));
}
#[wasm_bindgen_test]
fn gc_error_envelope_distinguishes_all_stages_and_instruction_limit() {
let parse_error = run_gc("let =");
assert_eq!(parse_error["status"], "error");
assert_eq!(parse_error["stage"], "parse");
assert!(parse_error["span"].is_null());
let compile_error = run_gc("this;");
assert_eq!(compile_error["status"], "error");
assert_eq!(compile_error["stage"], "compile");
assert!(compile_error["span"].is_null());
let runtime_error = run_gc("1.value;");
assert_eq!(runtime_error["status"], "error");
assert_eq!(runtime_error["stage"], "runtime");
assert_eq!(runtime_error["span"]["start"], 0);
assert_eq!(runtime_error["span"]["end"], 7);
let instruction_error = run_gc(&"1;".repeat(5_001));
assert_eq!(instruction_error["status"], "error");
assert_eq!(instruction_error["stage"], "runtime");
assert!(instruction_error["message"]
.as_str()
.unwrap()
.contains("instruction limit exceeded"));
}
fn build_snapshot(source: &str, strip_debug: bool) -> Value {
serde_json::from_str(&compile_to_snapshot(source, strip_debug))
.expect("valid snapshot envelope JSON")
}
fn snapshot_bytes(envelope: &Value) -> Vec<u8> {
let hex = envelope["bytesHex"].as_str().expect("bytesHex string");
(0..hex.len())
.step_by(2)
.map(|index| u8::from_str_radix(&hex[index..index + 2], 16).expect("hex byte"))
.collect()
}
fn run_bytes(bytes: &[u8]) -> Value {
serde_json::from_str(&run_snapshot(bytes)).expect("valid snapshot run envelope JSON")
}
#[wasm_bindgen_test]
fn snapshot_envelope_roundtrips_through_the_vm() {
let envelope = build_snapshot("let add = fn(a, b) { a + b }; add(1, 2)", false);
assert_eq!(envelope["status"], "ok");
let bytes = snapshot_bytes(&envelope);
assert_eq!(envelope["layout"]["byteLength"], bytes.len());
let run = run_bytes(&bytes);
assert_eq!(run["status"], "ok");
assert_eq!(run["result"], "3");
}
#[wasm_bindgen_test]
fn snapshot_output_is_captured_before_success_or_failure() {
let success = build_snapshot(r#"puts("before"); 42"#, false);
let run: Value = serde_json::from_str(&run_snapshot_with_output(&snapshot_bytes(&success)))
.expect("valid output envelope");
assert_eq!(run["status"], "ok");
assert_eq!(run["result"], "42");
assert_eq!(run["stdout"], "before\n");
let failure = build_snapshot(r#"puts("before"); 1 / 0"#, false);
let run: Value = serde_json::from_str(&run_snapshot_with_output(&snapshot_bytes(&failure)))
.expect("valid output envelope");
assert_eq!(run["status"], "error");
assert_eq!(run["kind"], "arithmetic");
assert_eq!(run["stdout"], "before\n");
}
#[wasm_bindgen_test]
fn snapshot_layout_regions_tile_the_buffer_and_disassemble() {
let envelope = build_snapshot("let add = fn(a, b) { a + b }; add(1, 2)", false);
let layout = &envelope["layout"];
assert_eq!(layout["formatVersion"], 1);
assert_eq!(layout["hasDebugInfo"], true);
assert!(layout["abiFingerprint"].as_str().unwrap().starts_with("0x"));
let regions = layout["regions"].as_array().expect("regions array");
let labels: Vec<&str> = regions
.iter()
.take(4)
.filter_map(|region| region["label"].as_str())
.collect();
assert_eq!(labels, vec!["magic", "version", "abi fingerprint", "flags"]);
let mut cursor = 0u64;
for region in regions {
assert_eq!(region["offset"], cursor);
cursor += region["length"].as_u64().expect("region length");
}
assert_eq!(Value::from(cursor), layout["byteLength"]);
assert!(regions
.iter()
.any(|region| region["label"] == "OpCall 2" && region["section"] == "main"));
}
#[wasm_bindgen_test]
fn stripped_snapshots_drop_debug_info_and_error_spans() {
let source = "let not_callable = 5; not_callable()";
let with_debug = build_snapshot(source, false);
let stripped = build_snapshot(source, true);
assert_eq!(stripped["layout"]["hasDebugInfo"], false);
assert!(
stripped["layout"]["byteLength"].as_u64().unwrap()
< with_debug["layout"]["byteLength"].as_u64().unwrap()
);
assert!(stripped["layout"]["regions"]
.as_array()
.unwrap()
.iter()
.all(|region| region["section"] != "debug"));
let with_debug_run = run_bytes(&snapshot_bytes(&with_debug));
assert_eq!(with_debug_run["status"], "error");
assert_eq!(with_debug_run["stage"], "runtime");
assert!(with_debug_run["span"]["start"].is_number());
let stripped_run = run_bytes(&snapshot_bytes(&stripped));
assert_eq!(stripped_run["status"], "error");
assert_eq!(stripped_run["stage"], "runtime");
assert!(stripped_run["span"].is_null());
assert_eq!(stripped_run["message"], with_debug_run["message"]);
}
#[wasm_bindgen_test]
fn hostile_snapshot_bytes_are_rejected_before_the_vm() {
let mut bytes = snapshot_bytes(&build_snapshot("1", false));
bytes[0] = b'X';
let run = run_bytes(&bytes);
assert_eq!(run["status"], "error");
assert_eq!(run["stage"], "snapshot");
assert!(run["message"].as_str().unwrap().contains("BadMagic"));
}
#[wasm_bindgen_test]
fn snapshot_runs_share_the_playground_instruction_budget() {
let source = "
let fibonacci = fn(n) {
if (n < 2) { n } else { fibonacci(n - 1) + fibonacci(n - 2) }
};
fibonacci(30);
";
let run = run_bytes(&snapshot_bytes(&build_snapshot(source, false)));
assert_eq!(run["status"], "error");
assert_eq!(run["stage"], "runtime");
assert!(run["message"]
.as_str()
.unwrap()
.contains("instruction limit exceeded"));
}
#[wasm_bindgen_test]
fn snapshot_parse_and_compile_failures_are_envelope_data() {
let parse_error = build_snapshot("let =", false);
assert_eq!(parse_error["status"], "error");
assert_eq!(parse_error["stage"], "parse");
let compile_error = build_snapshot("this;", false);
assert_eq!(compile_error["status"], "error");
assert_eq!(compile_error["stage"], "compile");
}
fn build_arm64(source: &str) -> Value {
serde_json::from_str(&compile_to_arm64(source)).expect("valid arm64 envelope JSON")
}
#[wasm_bindgen_test]
fn arm64_envelope_lines_carry_kinds_and_spans() {
let source = "let answer = 1 + 2; answer";
let envelope = build_arm64(source);
assert_eq!(envelope["status"], "ok");
let lines = envelope["lines"].as_array().expect("lines array");
let has = |kind: &str, needle: &str| {
lines.iter().any(|line| {
line["kind"] == kind && line["text"].as_str().unwrap_or("").contains(needle)
})
};
assert!(has("label", "main:"));
assert!(has("directive", ".globl main"));
assert!(has("comment", "// let answer = 1 + 2;"));
assert!(has("code", "bl rt_globals_init"));
assert!(lines
.iter()
.any(|line| line["kind"] == "blank" && line["text"] == ""));
for line in lines {
if line["span"].is_null() {
continue;
}
let start = line["span"]["start"].as_u64().expect("span start");
let end = line["span"]["end"].as_u64().expect("span end");
assert!(start <= end && end <= source.len() as u64);
}
let initializer = (
source.find("1 + 2").unwrap() as u64,
source.find("1 + 2").unwrap() as u64 + "1 + 2".len() as u64,
);
assert!(lines.iter().any(|line| {
line["kind"] == "code"
&& line["span"]["start"] == initializer.0
&& line["span"]["end"] == initializer.1
}));
}
#[wasm_bindgen_test]
fn arm64_parse_and_lowering_failures_are_envelope_data() {
let parse_error = build_arm64("let =");
assert_eq!(parse_error["status"], "error");
assert_eq!(parse_error["stage"], "parse");
assert!(parse_error["span"].is_null());
let compile_error = build_arm64("missing;");
assert_eq!(compile_error["status"], "error");
assert_eq!(compile_error["stage"], "compile");
assert_eq!(compile_error["message"], "undefined variable 'missing'");
assert_eq!(compile_error["span"]["start"], 0);
assert_eq!(compile_error["span"]["end"], 7);
}