diff --git a/Cargo.lock b/Cargo.lock index f0352102..2747bef2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.15.0" +version = "0.15.1" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.15.0" +version = "0.15.1" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.15.0" +version = "0.15.1" dependencies = [ "ansi_term", "bumpalo", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.15.0" +version = "0.15.1" dependencies = [ "lru", "oxide_bytecode", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1256,7 +1256,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.15.0" +version = "0.15.1" dependencies = [ "num-bigint", "oxc_allocator", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1297,7 +1297,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.15.0" +version = "0.15.1" dependencies = [ "bumpalo", "dashmap", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.15.0" +version = "0.15.1" dependencies = [ "tracing", "tracing-appender", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.15.0" +version = "0.15.1" dependencies = [ "num-bigint", "num-traits", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.15.0" +version = "0.15.1" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.15.0" +version = "0.15.1" dependencies = [ "bumpalo", "dashmap", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.15.0" +version = "0.15.1" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e0837600..8654eec3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.15.0" +version = "0.15.1" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_test262/src/harness.rs b/crates/oxide_test262/src/harness.rs index 2e46fbce..6a85a351 100644 --- a/crates/oxide_test262/src/harness.rs +++ b/crates/oxide_test262/src/harness.rs @@ -2,6 +2,9 @@ //! 前缀拼接与缓存。全部 `include_str!` 路径相对上 3 级到仓库根,依赖本文件平铺 `src/` 一级,禁子目录。 use crate::meta::TestMeta; +use oxide_bytecode::CompiledModule; +use oxide_compiler::compiler::Compiler; +use oxide_engine::Engine; use std::collections::HashMap; use std::sync::{Arc, OnceLock, RwLock}; @@ -13,6 +16,10 @@ pub(crate) struct HarnessSources { /// harness 前缀缓存键:由测试 `includes` 列表唯一确定。 pub(crate) type HarnessPrefixCache = HashMap, String>; +/// harness 前缀编译产物缓存:键为 includes 组合(含 onlyStrict / async 旗标), +/// 值为编译后的前缀模块。命中时直接复用,免每测试重解析重编译前缀。 +pub(crate) type HarnessModuleCache = HashMap, Arc>; + impl HarnessSources { /// 构建 harness 源注册表(键为文件名,值为编译期内嵌源码)。 pub(crate) fn new() -> Self { @@ -94,17 +101,47 @@ pub(crate) fn append_source_chunk(out: &mut String, name: &str, source: &str) { out.push('\n'); } -/// 取测试元数据的 harness 缓存键(即其 `includes` 列表)。 +/// 取测试元数据的 harness 缓存键(`includes` 列表 + onlyStrict / async 旗标)。 +/// 旗标入键是因为它们改变前缀内容(use strict 指令 / async 捕获注入)。 fn harness_key(meta: &TestMeta) -> Vec { let mut key = meta.includes.clone(); if meta.flags.iter().any(|f| f == "onlyStrict") { key.push("__onlyStrict__".into()); } + if meta.flags.iter().any(|f| f == "async") { + key.push("__async__".into()); + } key } +/// 把拼接源码中顶层(行首)let/const 声明改写为 var,使其落全局对象、跨 run 可见。 +/// +/// 引擎的全局词法环境不跨 run 持久化:顶层 let/const 只活在当前 run 的词法环境, +/// 下一个 run(测试体脚本)看不到。改写为 var 后落全局对象,跨 run 共享。 +/// 仅作用于行首声明:harness 文件内函数体代码均缩进,不受影响。 +fn demote_top_level_lexical(source: &str) -> String { + source + .lines() + .map(|line| { + if let Some(rest) = line.strip_prefix("let ") { + format!("var {rest}") + } else if let Some(rest) = line.strip_prefix("const ") { + format!("var {rest}") + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") +} + /// 按测试元数据拼接完整的 harness 前缀源码(prelude + sta/assert + 各 include)。 -fn build_harness_source(meta: &TestMeta, harness: &HarnessSources) -> Result { +/// +/// `demote` 为真时把顶层 let/const 降级为 var(供两模块方案跨 run 共享), +/// 为假时保留原样(供单模块方案,同模块内 let/const 本可见)。 +fn build_harness_source( + meta: &TestMeta, harness: &HarnessSources, demote: bool, +) -> Result { let mut source = String::new(); // onlyStrict 测试在严格模式下运行:指令必须位于脚本最前(harness 之前)。 if meta.flags.iter().any(|f| f == "onlyStrict") { @@ -131,7 +168,24 @@ fn build_harness_source(meta: &TestMeta, harness: &HarnessSources) -> Result>, +) -> Result, String> { + let key = harness_key(meta); + { + let guard = cache.read().unwrap(); + if let Some(m) = guard.get(&key) { + return Ok(Arc::clone(m)); + } + } + let source = build_harness_source(meta, harness, true)?; + let module = engine.compile_with(&source, compiler)?; + cache.write().unwrap().insert(key, Arc::clone(&module)); + Ok(module) +} diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index cfa9b7a5..56db80ee 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -20,7 +20,7 @@ mod test262_log; use chunked::run_chunked; use config::RunConfig; use discovery::discover_tests; -use harness::{HarnessPrefixCache, HarnessSources, HARNESS}; +use harness::{HarnessModuleCache, HarnessPrefixCache, HarnessSources, HARNESS}; use heartbeat::write_heartbeat; use judge::{categorize_fail, TestOutcome}; use oxide_log::{Level, LogConfig, Output, SUBSYSTEM_COUNT}; @@ -220,6 +220,7 @@ fn run_tests() -> bool { let paths_ref = &paths; let heartbeat_ref = &heartbeat_path; let harness_cache = Arc::new(RwLock::new(HarnessPrefixCache::new())); + let harness_module_cache = Arc::new(RwLock::new(HarnessModuleCache::new())); // 保持内存平稳:worker 只返回聚合统计。保留数万个 `TestResult` 会使 // `--no-skip` 运行在套件末尾附近累积 path/error 字符串直至进程 OOM。 @@ -229,6 +230,7 @@ fn run_tests() -> bool { let cursor = &cursor; let progress = &progress; let harness_cache = Arc::clone(&harness_cache); + let harness_module_cache = Arc::clone(&harness_module_cache); // 与 main() 的 16MB 栈一致:VM 在深层嵌套测试程序上递归, // 默认 worker 栈会溢出。 std::thread::Builder::new() @@ -263,6 +265,7 @@ fn run_tests() -> bool { &engine, harness_sources, &harness_cache, + &harness_module_cache, ); stats.record(i, &result); // COMPLETED 心跳:先写心跳再追加旁路失败行(崩溃恢复 diff --git a/crates/oxide_test262/src/runner.rs b/crates/oxide_test262/src/runner.rs index f26e7167..797c08d3 100644 --- a/crates/oxide_test262/src/runner.rs +++ b/crates/oxide_test262/src/runner.rs @@ -5,7 +5,10 @@ #![allow(clippy::arc_with_non_send_sync)] use crate::discovery::{eval_family_excluded, is_skipped, pre_existing_excluded}; -use crate::harness::{append_source_chunk, get_harness_prefix, HarnessPrefixCache, HarnessSources}; +use crate::harness::{ + append_source_chunk, get_harness_module, get_harness_prefix, HarnessModuleCache, + HarnessPrefixCache, HarnessSources, +}; use crate::judge::{ classify_compile_error, judge_async_result, judge_vm_error, panic_payload_str, TestOutcome, TestResult, }; @@ -39,12 +42,16 @@ fn pc_watch_path() -> Option<&'static PathBuf> { #[expect(clippy::too_many_arguments)] fn run_test( path: &Path, source: &str, meta: &TestMeta, engine: &Engine, harness: &HarnessSources, - harness_cache: &Arc>, no_skip: bool, no_regalloc: bool, + harness_cache: &Arc>, + harness_module_cache: &Arc>, no_skip: bool, no_regalloc: bool, ) -> TestResult { let start = std::time::Instant::now(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - run_test_inner(path, source, meta, engine, harness, harness_cache, no_skip, no_regalloc) + run_test_inner( + path, source, meta, engine, harness, harness_cache, harness_module_cache, no_skip, + no_regalloc, + ) })); match result { @@ -94,56 +101,44 @@ impl oxide_emit::module::ModuleSourceLoader for Test262ModuleLoader { } } -/// 单测执行主流程:拼 harness 前缀 → parse → compile → run; -/// 依据 `negative` 元数据校验期望错误,未实现特性按 no_skip 选择跳过或失败。 +/// 单测执行主流程:编译 → run → 判定;依据 `negative` 元数据校验期望错误, +/// 未实现特性按 no_skip 选择跳过或失败。 /// -/// script 臂经 `Engine::compile_with` 便捷路径(parse + CodeForge 缓存);module 臂 -/// 保留本模块的 `parse_module` + 依赖加载器(引擎暂不吸收 module 加载器面)。 -/// 两臂共享同一 compiler,`no_regalloc` 旗标对两臂同口径生效。 +/// module 臂保留本模块的 `parse_module` + 依赖加载器(引擎暂不吸收 module 加载器面), +/// 前缀与测试体拼为单模块编译。script 臂走两模块方案:前缀模块按 includes 组合 +/// 缓存(免每测试重解析重编译),测试体模块每测试编译,顺序执行。两臂共享同一 +/// compiler,`no_regalloc` 旗标对两臂同口径生效。 #[expect(clippy::too_many_arguments)] fn run_test_inner( path: &Path, source: &str, meta: &TestMeta, engine: &Engine, harness: &HarnessSources, - harness_cache: &Arc>, no_skip: bool, no_regalloc: bool, + harness_cache: &Arc>, + harness_module_cache: &Arc>, no_skip: bool, no_regalloc: bool, ) -> TestResult { let start = std::time::Instant::now(); let is_async = meta.flags.iter().any(|f| f == "async"); let is_module = meta.flags.iter().any(|f| f == "module"); + let is_only_strict = meta.flags.iter().any(|f| f == "onlyStrict"); - let code = match get_harness_prefix(meta, harness, harness_cache) { - Ok(prefix) => { - let mut code = prefix; - if is_async { - // 异步测试:注入 print 捕获 + $DONE(asyncTests 用 $DONE 报告结果)。 - // 结果写入 globalThis 全局字符串,run() 结束后由 runner 读取并按 marker 判定。 - // print 为顶层函数声明,经全局对象反射落 globalThis;此处仅初始化累加器。 - append_source_chunk( - &mut code, - "async capture", - "globalThis.$__test262_async_result = \"\";\n\ - function print(msg) { globalThis.$__test262_async_result += String(msg) + \"\\n\"; }", - ); - if let Some(done_src) = harness.get("doneprintHandle.js") { - append_source_chunk(&mut code, "doneprintHandle.js", done_src); - } - } - append_source_chunk(&mut code, "test source", strip_meta(source)); - code - } - Err(e) => { - let dur = start.elapsed().as_millis() as u64; - if no_skip { - return TestResult::fail(path.to_path_buf(), dur, e); - } - return TestResult::skip(path.to_path_buf(), e); - } - }; - - // module 臂保留本模块的 parse_module + 依赖加载器;script 臂经引擎 - // 便捷路径(parse + CodeForge 缓存)。compiler 由两臂共享, - // no_regalloc 旗标对两臂同口径生效。 + // compiler 由两臂共享,no_regalloc 旗标对两臂同口径生效。 let compiler = if no_regalloc { Compiler::new().with_regalloc(false) } else { Compiler::new() }; - let module_result = if is_module { + + // 编译模块:module 臂单模块(前缀 + 测试体),script 臂两模块(缓存前缀模块 + 测试体模块)。 + let modules: Vec> = if is_module { + let code = match get_harness_prefix(meta, harness, harness_cache) { + Ok(prefix) => { + let mut code = prefix; + append_source_chunk(&mut code, "test source", strip_meta(source)); + code + } + Err(e) => { + let dur = start.elapsed().as_millis() as u64; + if no_skip { + return TestResult::fail(path.to_path_buf(), dur, e); + } + return TestResult::skip(path.to_path_buf(), e); + } + }; let alloc = oxide_parser::Allocator::default(); let program = match oxide_parser::parse_module(&alloc, &code) { Ok(p) => p, @@ -157,34 +152,75 @@ fn run_test_inner( } }; let mut loader = Test262ModuleLoader; - compiler - .compile_module(&program, path.to_string_lossy().as_ref(), &mut loader) - .map(Arc::new) + match compiler.compile_module(&program, path.to_string_lossy().as_ref(), &mut loader) { + Ok(m) => vec![Arc::new(m)], + Err(e) => { + let dur = start.elapsed().as_millis() as u64; + let msg = format!("compile error: {e}"); + match classify_compile_error(&msg, meta.negative.is_some(), no_skip) { + TestOutcome::Pass(m) => return TestResult::pass(path.to_path_buf(), dur, m), + TestOutcome::Fail(m) => return TestResult::fail(path.to_path_buf(), dur, m), + TestOutcome::Skip(m) => return TestResult::skip(path.to_path_buf(), m), + } + } + } } else { - engine.compile_with(&code, &compiler) - }; - - let module = match module_result { - Ok(m) => m, - Err(e) => { - let dur = start.elapsed().as_millis() as u64; - let msg = format!("compile error: {e}"); - match classify_compile_error(&msg, meta.negative.is_some(), no_skip) { - TestOutcome::Pass(m) => return TestResult::pass(path.to_path_buf(), dur, m), - TestOutcome::Fail(m) => return TestResult::fail(path.to_path_buf(), dur, m), - TestOutcome::Skip(m) => return TestResult::skip(path.to_path_buf(), m), + // script 臂两模块方案:前缀模块按 includes 组合缓存(免每测试重解析重编译), + // 测试体模块每测试编译。onlyStrict 测试给测试体补 "use strict" 保持严格性。 + let harness_module = match get_harness_module(meta, harness, engine, &compiler, harness_module_cache) + { + Ok(m) => m, + Err(e) => { + let dur = start.elapsed().as_millis() as u64; + if no_skip { + return TestResult::fail(path.to_path_buf(), dur, e); + } + return TestResult::skip(path.to_path_buf(), e); + } + }; + let mut test_body = String::from(strip_meta(source)); + if is_only_strict { + test_body = format!("\"use strict\";\n{test_body}"); + } + match engine.compile_with(&test_body, &compiler) { + Ok(test_module) => vec![harness_module, test_module], + Err(e) => { + let dur = start.elapsed().as_millis() as u64; + let msg = format!("compile error: {e}"); + match classify_compile_error(&msg, meta.negative.is_some(), no_skip) { + TestOutcome::Pass(m) => return TestResult::pass(path.to_path_buf(), dur, m), + TestOutcome::Fail(m) => return TestResult::fail(path.to_path_buf(), dur, m), + TestOutcome::Skip(m) => return TestResult::skip(path.to_path_buf(), m), + } } } }; - // 独立 Vm 不经池;动态编译服务由 `new_vm` 同口径注入(语料含 eval / - // Function 动态编译面)。 + run_and_judge(path, meta, engine, &modules, is_async, no_skip, &start) +} + +/// 在独立 Vm 上顺序执行各模块并判定。独立 Vm 不经池;动态编译服务由 `new_vm` +/// 同口径注入(语料含 eval / Function 动态编译面)。任一模块失败即停,不跑后续模块。 +/// 完成值取最后一个模块的结果(script 臂即测试体模块)。 +fn run_and_judge( + path: &Path, meta: &TestMeta, engine: &Engine, modules: &[Arc], + is_async: bool, no_skip: bool, start: &std::time::Instant, +) -> TestResult { let mut vm = engine.new_vm(); // 监督者注入现场文件路径时开启 VM 的 last-pc 定频写(未注入时零开销)。 if let Some(watch) = pc_watch_path() { vm.set_pc_watch(Some(watch.clone())); } - let run_result = vm.run(&module); + let mut run_result: Result = Ok(JsValue::undefined()); + for module in modules { + match vm.run(module) { + Ok(v) => run_result = Ok(v), + Err(e) => { + run_result = Err(e); + break; + } + } + } let dur = start.elapsed().as_millis() as u64; if is_async { @@ -222,10 +258,12 @@ pub(crate) fn read_async_output(vm: &Vm) -> String { /// 串行与并行执行路径共享的每测试管线: /// 读文件、解析元数据、应用跳过过滤,然后运行。恰好返回一个 `TestResult`。 -/// worker 自有状态(`engine`、`harness_sources`、`harness_cache`)永不跨线程。 +/// worker 自有状态(`engine`、`harness_sources`、`harness_cache`、`harness_module_cache`)永不跨线程。 +#[expect(clippy::too_many_arguments)] pub(crate) fn process_path( path: &Path, filter: &Option, no_skip: bool, no_regalloc: bool, engine: &Engine, harness_sources: &HarnessSources, harness_cache: &Arc>, + harness_module_cache: &Arc>, ) -> TestResult { let source = match std::fs::read_to_string(path) { Ok(s) => s, @@ -264,7 +302,10 @@ pub(crate) fn process_path( } } - run_test(path, &source, &meta, engine, harness_sources, harness_cache, no_skip, no_regalloc) + run_test( + path, &source, &meta, engine, harness_sources, harness_cache, harness_module_cache, + no_skip, no_regalloc, + ) } /// 构建带步数上限的 runner 引擎。每个并行 worker 拥有自己的引擎,