mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
vectorcraft-js gains a Realm that runs ECMAScript 5.1 scripts. The parsed script compiles to stack-machine bytecode, a verifier checks every jump target, handler, constant index and stack depth before the code runs, and the interpreter keeps JavaScript calls on its own frame stack. Closures, this, arguments, with, direct and indirect eval, new Function, try/catch/finally, labeled break and continue, and for-in in insertion order follow sections 10 to 14 of ECMA-262 5.1. Variable references are resolved before the right-hand side of an assignment runs (11.13). Objects and environments live in an arena with a mark-and-sweep collector and index handles. Built-ins are limited to Object and Object.prototype's basic methods, Function with call and apply, eval and the error constructors. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag another thread can set, with smaller defaults in the wasm build. A run that reaches a limit ends with a limit error that a script's try cannot catch. Source passed to eval and Function counts against Options::max_source_units. cargo xtask test262 also runs the suite's ES5 test/language tests that need only these built-ins and prints their counts apart from the parse counts. Nothing in the app calls the interpreter yet.
75 lines
3.7 KiB
Rust
75 lines
3.7 KiB
Rust
//! `cargo xtask test262`: the official ECMAScript conformance suite (https://github.com/tc39/test262,
|
|
//! BSD-3-Clause) at a pinned commit, fetched as GitHub's archive of that commit, checked against
|
|
//! its SHA-256 and size, and unpacked into `corpus/test262/` (gitignored, never committed). It then
|
|
//! runs `vectorcraft-js`'s `test262` tests, which parse the ES5 parse subset, run the ES5
|
|
//! `test/language` tests that need only the interpreter's built-ins, and print each one's counts.
|
|
//!
|
|
//! Moving the pin: change [`COMMIT`], run the command once with the new commit to see the
|
|
//! archive's SHA-256 in the error, check the archive, and update [`SHA256`].
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
use crate::{cargo, root, run, sha256};
|
|
|
|
/// The tc39/test262 commit the suite is read at.
|
|
const COMMIT: &str = "d53ebc8b1931d9f1541e43d59650776e4b766248";
|
|
|
|
/// The SHA-256 of GitHub's `tar.gz` archive of [`COMMIT`].
|
|
const SHA256: &str = "6db8566044d1a3f5a68aac87806e09b2fe5d21f5869f3a74cfd8b7236ebaf5c8";
|
|
|
|
/// The largest archive accepted (the pinned one is 9.6 MB).
|
|
const MAX_BYTES: u64 = 64 << 20;
|
|
|
|
pub fn run_suite() -> Result<(), String> {
|
|
let dir = fetch()?;
|
|
let mut c = cargo();
|
|
c.env("VECTORCRAFT_TEST262_DIR", &dir).args(["test", "-p", "vectorcraft-js", "--release", "--test", "test262", "--", "--nocapture"]);
|
|
run(c, &format!("cargo test -p vectorcraft-js --release --test test262 -- --nocapture (VECTORCRAFT_TEST262_DIR={})", dir.display()))
|
|
}
|
|
|
|
/// Fetches and unpacks the suite unless it is there already, and returns its folder.
|
|
fn fetch() -> Result<std::path::PathBuf, String> {
|
|
let base = root().join("corpus").join("test262");
|
|
let dir = base.join(format!("test262-{COMMIT}"));
|
|
let marker = dir.join(".vectorcraft-sha256");
|
|
if std::fs::read_to_string(&marker).is_ok_and(|s| s.trim() == SHA256) {
|
|
println!("test262 {COMMIT} is in {}", dir.display());
|
|
return Ok(dir);
|
|
}
|
|
std::fs::create_dir_all(&base).map_err(|e| format!("create {}: {e}", base.display()))?;
|
|
let archive = base.join(format!("test262-{COMMIT}.tar.gz"));
|
|
if !verified(&archive)? {
|
|
let url = format!("https://codeload.github.com/tc39/test262/tar.gz/{COMMIT}");
|
|
let mut curl = Command::new("curl");
|
|
curl.args(["-fsSL", "--retry", "3", "-A", "VectorCraft-dev", "--max-filesize", &MAX_BYTES.to_string(), "-o"]).arg(&archive).arg(&url);
|
|
run(curl, &format!("curl {url}"))?;
|
|
if !verified(&archive)? {
|
|
let got = std::fs::read(&archive).map(|b| sha256::hex(&b)).unwrap_or_default();
|
|
let _ = std::fs::remove_file(&archive);
|
|
return Err(format!("test262 archive: sha256 {got} does not match the pinned {SHA256}"));
|
|
}
|
|
}
|
|
if dir.exists() {
|
|
std::fs::remove_dir_all(&dir).map_err(|e| format!("remove {}: {e}", dir.display()))?;
|
|
}
|
|
let mut tar = Command::new("tar");
|
|
tar.arg("-xzf").arg(&archive).arg("-C").arg(&base);
|
|
run(tar, &format!("tar -xzf {}", archive.display()))?;
|
|
if !dir.join("test").join("language").is_dir() {
|
|
return Err(format!("{} has no test/language folder after unpacking", dir.display()));
|
|
}
|
|
std::fs::write(&marker, SHA256).map_err(|e| format!("{}: {e}", marker.display()))?;
|
|
println!("test262 {COMMIT} unpacked into {}", dir.display());
|
|
Ok(dir)
|
|
}
|
|
|
|
/// The archive is there, within the size limit, with the pinned SHA-256.
|
|
fn verified(path: &Path) -> Result<bool, String> {
|
|
let Ok(meta) = std::fs::metadata(path) else { return Ok(false) };
|
|
if meta.len() > MAX_BYTES {
|
|
return Err(format!("{} is {} bytes, over the {MAX_BYTES}-byte limit", path.display(), meta.len()));
|
|
}
|
|
Ok(std::fs::read(path).is_ok_and(|b| sha256::hex(&b) == SHA256))
|
|
}
|