Some checks failed
ci / fast (linux) (push) Failing after 13m32s
Implements full class support: prototype-chain-based inheritance on JsObject, class/extends/super/static/instanceof keywords, parser and VM execution for class declarations and expressions, super() constructor calls and super.method() dispatch via dedicated SuperInfo fields (not user-observable properties), and instanceof operator with prototype chain walking. Includes depth guards, Box<JsValue> in RuntimeError::Thrown to satisfy clippy result_large_err, and 11 JS262 conformance tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
593 lines
17 KiB
Rust
593 lines
17 KiB
Rust
use js::{CaptureSink, JsEngine, JsValue};
|
|
|
|
fn engine_with_capture() -> JsEngine {
|
|
let sink = CaptureSink::new();
|
|
let mut engine = JsEngine::with_output(Box::new(sink));
|
|
engine.prime_runtime().unwrap();
|
|
engine
|
|
}
|
|
|
|
fn engine() -> JsEngine {
|
|
let mut engine = JsEngine::new();
|
|
engine.prime_runtime().unwrap();
|
|
engine
|
|
}
|
|
|
|
#[test]
|
|
fn determinism_100_runs() {
|
|
let script = r#"
|
|
var x = 0;
|
|
function inc(n) { return n + 1; }
|
|
x = inc(x);
|
|
x = inc(x);
|
|
x = inc(x);
|
|
x;
|
|
"#;
|
|
|
|
let mut results = Vec::new();
|
|
for _ in 0..100 {
|
|
let mut eng = engine();
|
|
let val = eng.execute(script).unwrap();
|
|
results.push(format!("{val}"));
|
|
}
|
|
|
|
let first = &results[0];
|
|
assert!(
|
|
results.iter().all(|r| r == first),
|
|
"non-deterministic: got different results across 100 runs"
|
|
);
|
|
assert_eq!(first, "3");
|
|
}
|
|
|
|
#[test]
|
|
fn multi_script_state_persistence() {
|
|
let mut eng = engine();
|
|
eng.execute("var counter = 0;").unwrap();
|
|
eng.execute("counter = counter + 1;").unwrap();
|
|
eng.execute("counter = counter + 1;").unwrap();
|
|
let val = eng.execute("counter;").unwrap();
|
|
assert!(matches!(val, JsValue::Number(n) if n == 2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn error_recovery_after_failure() {
|
|
let mut eng = engine();
|
|
|
|
// First execution fails (undefined variable)
|
|
let err = eng.execute("nonexistent;");
|
|
assert!(err.is_err());
|
|
assert!(err.unwrap_err().is_reference_error());
|
|
|
|
// Re-prime and try again
|
|
eng.prime_runtime().unwrap();
|
|
let val = eng.execute("42;").unwrap();
|
|
assert!(matches!(val, JsValue::Number(n) if n == 42.0));
|
|
}
|
|
|
|
#[test]
|
|
fn large_script_many_statements() {
|
|
let mut eng = engine();
|
|
let mut script = String::from("var total = 0;\n");
|
|
for i in 1..=50 {
|
|
script.push_str(&format!("total = total + {i};\n"));
|
|
}
|
|
script.push_str("total;\n");
|
|
|
|
let val = eng.execute(&script).unwrap();
|
|
// Sum of 1..=50 = 1275
|
|
assert!(matches!(val, JsValue::Number(n) if n == 1275.0));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_error_classification() {
|
|
let mut eng = engine();
|
|
let err = eng.execute("var ;").unwrap_err();
|
|
assert!(err.is_parse_error());
|
|
assert!(!err.is_reference_error());
|
|
assert!(!err.is_type_error());
|
|
assert!(!err.is_runtime_error());
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_error_classification() {
|
|
let mut eng = engine();
|
|
let err = eng.execute("missing_var;").unwrap_err();
|
|
assert!(err.is_reference_error());
|
|
assert!(!err.is_parse_error());
|
|
}
|
|
|
|
#[test]
|
|
fn console_log_output_captured() {
|
|
let mut eng = engine_with_capture();
|
|
eng.execute("console.log(\"hello\");").unwrap();
|
|
eng.execute("console.log(1 + 2);").unwrap();
|
|
eng.execute("console.log(\"a\", \"b\", \"c\");").unwrap();
|
|
|
|
// We can't directly inspect the sink from here without downcasting,
|
|
// but the test verifies no panics/errors occur during console.log execution.
|
|
}
|
|
|
|
#[test]
|
|
fn function_hoisting_works() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var r = greet(); function greet() { return \"hi\"; } r;")
|
|
.unwrap();
|
|
assert!(matches!(val, JsValue::String(ref s) if s == "hi"));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_function_calls() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute(
|
|
r#"
|
|
function add(a, b) { return a + b; }
|
|
function mul(a, b) { return a * b; }
|
|
mul(add(2, 3), add(4, 1));
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(val, JsValue::Number(n) if n == 25.0));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_and_comment_scripts() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("").unwrap();
|
|
assert!(matches!(val, JsValue::Undefined));
|
|
|
|
let val = eng.execute("// just a comment").unwrap();
|
|
assert!(matches!(val, JsValue::Undefined));
|
|
|
|
let val = eng.execute("/* block comment */").unwrap();
|
|
assert!(matches!(val, JsValue::Undefined));
|
|
}
|
|
|
|
// --- Recursive call hits statement limit, not stack overflow ---
|
|
|
|
#[test]
|
|
fn finite_recursion_executes_correctly() {
|
|
// A bounded recursion that terminates well within the statement limit
|
|
// must return the correct value. Debug-mode stack frames are large,
|
|
// so run on a bigger thread to avoid overflowing the default 8 MiB stack.
|
|
std::thread::Builder::new()
|
|
.stack_size(16 * 1024 * 1024)
|
|
.spawn(|| {
|
|
let script = r#"
|
|
function countdown(n) {
|
|
if (n === 0) { return 0; }
|
|
return countdown(n - 1);
|
|
}
|
|
countdown(50);
|
|
"#;
|
|
let mut eng = engine();
|
|
let val = eng.execute(script).unwrap();
|
|
assert!(matches!(val, JsValue::Number(n) if n == 0.0));
|
|
})
|
|
.unwrap()
|
|
.join()
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn recursive_function_hits_statement_limit_not_stack_overflow() {
|
|
// An infinitely recursive function should be stopped by the statement
|
|
// counter (producing a RuntimeError::Generic), never by a Rust stack
|
|
// overflow. The unit test in js_vm::interpreter::tests covers the case
|
|
// where the limit is small enough (50) to fire before the stack runs out.
|
|
// With the default limit (100,000), debug-mode stack frames are large
|
|
// enough to exhaust the default 8 MiB stack, so run on a bigger thread.
|
|
std::thread::Builder::new()
|
|
.stack_size(16 * 1024 * 1024)
|
|
.spawn(|| {
|
|
let mut eng = engine();
|
|
let err = eng
|
|
.execute("function inf(n) { return inf(n + 1); } inf(0);")
|
|
.unwrap_err();
|
|
assert!(
|
|
err.is_runtime_error(),
|
|
"expected RuntimeError (statement limit) for infinite recursion, got: {err}"
|
|
);
|
|
})
|
|
.unwrap()
|
|
.join()
|
|
.unwrap();
|
|
}
|
|
|
|
// --- typeof on undeclared variable returns "undefined" (spec-correct) ---
|
|
|
|
#[test]
|
|
fn typeof_undeclared_returns_undefined() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("typeof totallyMissingVariable;").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::String(ref s) if s == "undefined"),
|
|
"expected \"undefined\" for typeof on undeclared var, got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- var hoisting across un-taken if branches (end-to-end) ---
|
|
|
|
#[test]
|
|
fn var_hoisting_in_untaken_if_branch() {
|
|
let mut eng = engine();
|
|
// var declared inside a never-executed if block must be hoisted to the
|
|
// enclosing scope per the JS spec.
|
|
let val = eng.execute("if (false) { var x = 1; } x;").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::Undefined),
|
|
"expected undefined (hoisted var x), got: {val}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn var_hoisting_in_else_branch_when_if_taken() {
|
|
let mut eng = engine();
|
|
// The else branch is not taken, so `b` is hoisted but never initialised.
|
|
let val = eng
|
|
.execute("if (true) { var a = 1; } else { var b = 2; } b;")
|
|
.unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::Undefined),
|
|
"expected undefined (b hoisted from else), got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- Non-ASCII string literals round-trip through parse → execute ---
|
|
|
|
#[test]
|
|
fn non_ascii_string_executes_correctly() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("\"café\";").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::String(ref s) if s == "café"),
|
|
"expected café, got: {val}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cjk_string_executes_correctly() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("\"日本語\";").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::String(ref s) if s == "日本語"),
|
|
"expected 日本語, got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- Chained member access returns TypeError (not crash) ---
|
|
|
|
#[test]
|
|
fn chained_member_access_returns_type_error() {
|
|
// a.b.c — accessing a non-console member should produce a TypeError, not panic.
|
|
let mut eng = engine();
|
|
let err = eng.execute("var a = 1; a.b;").unwrap_err();
|
|
assert!(
|
|
err.is_type_error(),
|
|
"expected TypeError for member access on number, got: {err}"
|
|
);
|
|
}
|
|
|
|
// --- Empty function body ---
|
|
|
|
#[test]
|
|
fn empty_function_returns_undefined() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("function noop() {} noop();").unwrap();
|
|
assert!(matches!(val, JsValue::Undefined));
|
|
}
|
|
|
|
// --- Function with fewer args than params ---
|
|
|
|
#[test]
|
|
fn fewer_args_than_params_extra_are_undefined() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("function f(a, b, c) { return c; } f(1, 2);")
|
|
.unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::Undefined),
|
|
"expected undefined for missing third arg, got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- Function with more args than params ---
|
|
|
|
#[test]
|
|
fn more_args_than_params_extras_ignored() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("function f(a) { return a; } f(42, 99, 100);")
|
|
.unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::Number(n) if n == 42.0),
|
|
"expected 42, got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- Multiple return paths ---
|
|
|
|
#[test]
|
|
fn multiple_return_paths_execute_correctly() {
|
|
let script = r#"
|
|
function classify(n) {
|
|
if (n > 0) { return "positive"; }
|
|
else { return "non-positive"; }
|
|
}
|
|
classify(5);
|
|
"#;
|
|
let mut eng = engine();
|
|
let val = eng.execute(script).unwrap();
|
|
assert!(matches!(val, JsValue::String(ref s) if s == "positive"));
|
|
|
|
let val = eng.execute("classify(-1);").unwrap();
|
|
assert!(matches!(val, JsValue::String(ref s) if s == "non-positive"));
|
|
}
|
|
|
|
// --- Assignment to undeclared variable ---
|
|
|
|
#[test]
|
|
fn assignment_to_undeclared_variable_is_reference_error() {
|
|
let mut eng = engine();
|
|
let err = eng.execute("undeclaredVar = 42;").unwrap_err();
|
|
assert!(
|
|
err.is_reference_error(),
|
|
"expected ReferenceError for assignment to undeclared variable, got: {err}"
|
|
);
|
|
}
|
|
|
|
// --- Nested blocks with mixed var/let scoping ---
|
|
|
|
#[test]
|
|
fn nested_var_let_mixed_scoping() {
|
|
let script = r#"
|
|
var outer = 1;
|
|
{
|
|
var inner_var = 2;
|
|
let inner_let = 3;
|
|
outer = outer + inner_var + inner_let;
|
|
}
|
|
outer;
|
|
"#;
|
|
let mut eng = engine();
|
|
let val = eng.execute(script).unwrap();
|
|
assert!(matches!(val, JsValue::Number(n) if n == 6.0));
|
|
}
|
|
|
|
#[test]
|
|
fn let_not_visible_outside_block_integration() {
|
|
let mut eng = engine();
|
|
let err = eng
|
|
.execute("{ let block_only = 5; } block_only;")
|
|
.unwrap_err();
|
|
assert!(
|
|
err.is_reference_error(),
|
|
"expected ReferenceError for let outside its block, got: {err}"
|
|
);
|
|
}
|
|
|
|
// --- Function defined in script 1, called in script 2 ---
|
|
|
|
#[test]
|
|
fn function_defined_in_script1_called_in_script2() {
|
|
let mut eng = engine();
|
|
eng.execute("function greetName(name) { return \"hello_\" + name; }")
|
|
.unwrap();
|
|
let val = eng.execute("greetName(\"world\");").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::String(ref s) if s == "hello_world"),
|
|
"expected hello_world, got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- Parse error in script 1 doesn't prevent script 2 from running ---
|
|
|
|
#[test]
|
|
fn parse_error_does_not_block_subsequent_script() {
|
|
let mut eng = engine();
|
|
// First script has a parse error
|
|
let err = eng.execute("var ;");
|
|
assert!(err.is_err());
|
|
// Second script should work (VM never entered Failed state for parse errors)
|
|
let val = eng.execute("42;").unwrap();
|
|
assert!(matches!(val, JsValue::Number(n) if n == 42.0));
|
|
}
|
|
|
|
// --- Deep function call nesting (10+ levels, under 64 limit) ---
|
|
|
|
#[test]
|
|
fn deep_call_nesting_under_limit() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute(
|
|
r#"
|
|
function a(n) { if (n === 0) { return "done"; } return b(n - 1); }
|
|
function b(n) { return a(n); }
|
|
a(20);
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::String(ref s) if s == "done"),
|
|
"expected done, got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- typeof in binary expression comparison ---
|
|
|
|
#[test]
|
|
fn typeof_in_equality_comparison() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("typeof 42 === \"number\";").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::Boolean(true)),
|
|
"expected true for typeof 42 === \"number\", got: {val}"
|
|
);
|
|
|
|
let val = eng.execute("typeof \"hello\" === \"string\";").unwrap();
|
|
assert!(
|
|
matches!(val, JsValue::Boolean(true)),
|
|
"expected true for typeof \"hello\" === \"string\", got: {val}"
|
|
);
|
|
}
|
|
|
|
// --- Multiple console.log calls with varied types ---
|
|
|
|
#[test]
|
|
fn switch_basic_case_match() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var r; switch(2) { case 1: r = 'one'; break; case 2: r = 'two'; break; } r;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "two");
|
|
}
|
|
|
|
#[test]
|
|
fn switch_default_case() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var r; switch(99) { case 1: r = 'one'; break; default: r = 'other'; break; } r;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "other");
|
|
}
|
|
|
|
#[test]
|
|
fn switch_fall_through_behavior() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var r = ''; switch(1) { case 1: r = r + 'a'; case 2: r = r + 'b'; } r;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "ab");
|
|
}
|
|
|
|
#[test]
|
|
fn switch_return_from_function() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute(
|
|
"function f(x) { switch(x) { case 1: return 'one'; default: return 'other'; } } f(1);",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "one");
|
|
}
|
|
|
|
#[test]
|
|
fn switch_no_match_no_default_is_noop() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var r = 'init'; switch(99) { case 1: r = 'one'; break; } r;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "init");
|
|
}
|
|
|
|
#[test]
|
|
fn console_log_varied_types_no_panic() {
|
|
// Verify console.log does not panic or error on any JsValue type.
|
|
let mut eng = engine();
|
|
eng.execute("console.log(undefined);").unwrap();
|
|
eng.execute("console.log(null);").unwrap();
|
|
eng.execute("console.log(true);").unwrap();
|
|
eng.execute("console.log(false);").unwrap();
|
|
eng.execute("console.log(0);").unwrap();
|
|
eng.execute("console.log(\"\");").unwrap();
|
|
eng.execute("console.log(\"hello\", \"world\");").unwrap();
|
|
eng.execute("console.log(1, 2, 3, 4, 5);").unwrap();
|
|
}
|
|
|
|
// --- Loop tests ---
|
|
|
|
#[test]
|
|
fn for_loop_accumulates_sum() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var sum = 0; for (var i = 1; i <= 5; i++) { sum += i; } sum;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "15");
|
|
}
|
|
|
|
#[test]
|
|
fn while_loop_counts_to_five() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("var n = 0; while (n < 5) { n++; } n;").unwrap();
|
|
assert_eq!(val.to_string(), "5");
|
|
}
|
|
|
|
#[test]
|
|
fn do_while_runs_at_least_once() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var x = 0; do { x++; } while (false); x;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "1");
|
|
}
|
|
|
|
#[test]
|
|
fn for_loop_with_break() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var r = 0; for (var i = 0; i < 100; i++) { if (i === 7) { break; } r = i; } i;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "7");
|
|
}
|
|
|
|
#[test]
|
|
fn for_loop_with_continue() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var sum = 0; for (var i = 0; i < 10; i++) { if (i % 2 === 0) { continue; } sum += i; } sum;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "25");
|
|
}
|
|
|
|
#[test]
|
|
fn while_loop_with_break() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var n = 0; while (true) { n++; if (n >= 4) { break; } } n;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "4");
|
|
}
|
|
|
|
#[test]
|
|
fn increment_decrement_pre_post() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var a = 1; var b = a++; var c = ++a; b;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "1");
|
|
let val = eng.execute("a;").unwrap();
|
|
assert_eq!(val.to_string(), "3");
|
|
let val = eng.execute("c;").unwrap();
|
|
assert_eq!(val.to_string(), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn compound_assignment_operators() {
|
|
let mut eng = engine();
|
|
let val = eng.execute("var x = 10; x += 5; x;").unwrap();
|
|
assert_eq!(val.to_string(), "15");
|
|
let val = eng.execute("x -= 3; x;").unwrap();
|
|
assert_eq!(val.to_string(), "12");
|
|
let val = eng.execute("x *= 2; x;").unwrap();
|
|
assert_eq!(val.to_string(), "24");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_for_loops() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var total = 0; for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { total++; } } total;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "9");
|
|
}
|
|
|
|
#[test]
|
|
fn do_while_with_continue_integration() {
|
|
let mut eng = engine();
|
|
let val = eng
|
|
.execute("var sum = 0; var i = 0; do { i++; if (i === 3) { continue; } sum += i; } while (i < 5); sum;")
|
|
.unwrap();
|
|
assert_eq!(val.to_string(), "12");
|
|
}
|