use quickjs_oxide::engine::api::core::{
GcPolicy, HeapLimits, NativeErrorKind, Runtime, RuntimeError, RuntimeOptions, Value,
declare_thread_stack,
};
fn text(value: &Value) -> String {
match value {
Value::String(text) => text.to_utf8_lossy(),
other => panic!("expected a string, got {other:?}"),
}
}
fn exceptions() {
let runtime = Runtime::new();
let context = runtime.context();
let result = context.eval("function f() { throw new TypeError('bad input') }\nf()");
let Err(RuntimeError::Exception(Value::Object(error))) = result else {
panic!("expected an error object");
};
assert!(runtime.is_error_object(&error).unwrap());
let printed = runtime.print_value(&Value::Object(error)).unwrap();
assert!(printed.starts_with("TypeError: bad input\n at f (<input>:1"));
assert_eq!(
context.eval("throw 7"),
Err(RuntimeError::Exception(Value::Int(7)))
);
}
fn invariants() {
let first = Runtime::new();
let second = Runtime::new();
let object = first.context().new_object().unwrap();
assert!(matches!(
second.context().set_property(
&second.context().global_object().unwrap(),
&second.intern_property_key("borrowed").unwrap(),
Value::Object(object),
),
Err(RuntimeError::Invariant(_))
));
}
fn syntax_errors() {
let runtime = Runtime::new();
let context = runtime.context();
let Err(RuntimeError::Exception(exception)) = context.eval_with_filename("let x = ;", "app.js")
else {
panic!("expected a SyntaxError");
};
let printed = runtime.print_value(&exception).unwrap();
assert!(printed.starts_with("SyntaxError: unexpected token in expression: ';'"));
assert!(printed.contains("at app.js:1:9"));
}
fn native_errors() {
let runtime = Runtime::new();
let context = runtime.context();
let error = context
.new_native_error(NativeErrorKind::Range, "out of range")
.unwrap();
let Value::Object(object) = &error else {
panic!("expected an object")
};
assert!(runtime.is_error_object(object).unwrap());
assert_eq!(NativeErrorKind::Range.name(), "RangeError");
}
fn stack_limits() {
let runtime = Runtime::new();
let context = runtime.context();
let caught = context
.eval("function down(n) { return down(n + 1) + 1 }\ntry { down(0) } catch (e) { `${e.name}: ${e.message}` }")
.unwrap();
assert_eq!(text(&caught), "InternalError: stack overflow");
let depth = |options: RuntimeOptions| {
let runtime = Runtime::with_options(options);
let context = runtime.context();
let reached = context
.eval(
"let depth = 0; function down() { depth++; down() }\ntry { down() } catch {} depth",
)
.unwrap();
let Value::Int(reached) = reached else {
panic!("expected a number")
};
reached
};
let small = depth(RuntimeOptions {
stack_limit: 10_000,
..RuntimeOptions::default()
});
assert!(small < 10_000);
assert!(depth(RuntimeOptions::default()) > small);
let runtime = Runtime::with_options(RuntimeOptions {
native_depth_limit: 64,
..RuntimeOptions::default()
});
let context = runtime.context();
let caught = context
.eval(
"function nest(n) { return n === 0 ? 0 : [n].map((m) => nest(m - 1))[0] + 1 }
try { nest(1000) } catch (e) { `${e.name}: ${e.message}` }",
)
.unwrap();
assert_eq!(text(&caught), "InternalError: stack overflow");
assert_eq!(context.eval("nest(32)").unwrap(), Value::Int(32));
}
fn evaluation_thread() {
const STACK: usize = 64 << 20;
let worker = std::thread::Builder::new()
.stack_size(STACK)
.spawn(|| {
declare_thread_stack(STACK);
let runtime = Runtime::new();
let context = runtime.context();
let nested = |depth: usize| format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
assert!(context.eval(&nested(500)).is_ok());
let Err(RuntimeError::Exception(exception)) = context.eval(&nested(5_000)) else {
panic!("expected a SyntaxError");
};
let printed = runtime.print_value(&exception).unwrap();
assert!(printed.starts_with("SyntaxError: stack overflow"));
})
.unwrap();
worker.join().unwrap();
}
fn heap_cap() {
let runtime = Runtime::with_options(RuntimeOptions::from(HeapLimits {
max_bytes: 16 << 20,
}));
let context = runtime.context();
let caught = context
.eval(
"const chunks = [];
try { for (;;) chunks.push(new Array(1024).fill(0)); }
catch (e) { chunks.length = 0; `${e.name}: ${e.message}` }",
)
.unwrap();
assert_eq!(text(&caught), "InternalError: out of memory");
assert_eq!(context.eval("1 + 1").unwrap(), Value::Int(2));
assert_eq!(
context.eval("const kept = []; for (;;) kept.push(new Array(1024).fill(0));"),
Err(RuntimeError::OutOfMemory)
);
assert!(runtime.memory_report().unwrap().reserved_bytes() <= 16 << 20);
}
fn collector() {
let runtime = Runtime::with_options(RuntimeOptions {
min_heap: 4 << 20, growth: 1.5, ..RuntimeOptions::default()
});
runtime.set_gc_policy(GcPolicy::Manual).unwrap();
let context = runtime.context();
context
.eval("let garbage = []; for (let i = 0; i < 10000; i++) garbage.push({ i }); garbage = null;")
.unwrap();
let before = runtime.gc_stats().unwrap().collections;
let stats = runtime.run_gc().unwrap();
assert_eq!(stats.collections, before + 1);
assert!(stats.freed_records >= 10_000);
let report = runtime.memory_report().unwrap();
assert!(report.objects.live > 0);
assert_eq!(report.cap, usize::MAX);
assert!(report.to_string().contains("heap reserved"));
}
fn main() {
exceptions();
syntax_errors();
invariants();
native_errors();
stack_limits();
evaluation_thread();
heap_cap();
collector();
println!("limits: ok");
}