From 6f70d7d09f70cd50bc5b2c7bfe0cdcbd0186b3f1 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 14 Sep 2026 07:16:00 +0000 Subject: [PATCH] =?UTF-8?q?vm/kernel/emit/builtins:=20=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E6=BA=90=E7=BC=96=E7=A0=81=E9=87=8D=E8=AE=BE=E8=AE=A1=20+=20?= =?UTF-8?q?=E6=95=B0=E5=80=BC=E9=94=AE=202^32=20=E8=BE=B9=E7=95=8C=20+=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E5=9F=9F=E8=A7=A3=E7=A0=81/Source=20?= =?UTF-8?q?=E9=9D=A2=E2=80=94=E2=80=9414=20=E5=A4=B4=E7=8B=AC=E5=8D=A0=20t?= =?UTF-8?q?est262=20=E5=9B=9E=E5=BD=92=2013=20=E4=BF=AE=200=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全量 A/B 差分撞出 14 个头独占回归,本提交闭合 13(text-self.js 双树同败归 pre-existing): 数值键 u32 回绕(release 态,debug 为 panic 故单测未现):canonical_index_str/ units 的 v.checked_mul(10)? + d as u32 在乘积合法而和溢出 2^32 时静默回绕 (10 位数字串 "4294967296".."4294967299" 误判整数键 0..3,属性查找回 退、Object.keys 串键),改 checked_mul+checked_add 双段防溢出;str/units 各 钉一枚 2^30-1/2^30/2^32-1/2^32 边界(Group B 5 文件:defineProperty/ defineProperties 2^32 键族 + Array property-cast-number)。 decodeURI/decodeURIComponent 单元域重写(Group A 2 文件):decode_uri_units 按单元扫描(非 % 单元原样透传——孤立 surrogate 往返不再被 lossy 串值面 打碎),%XX 解单元入缓冲;string_arg_units 经 to_units_full 取单元。 动态源编码(eval/Function/$262.evalScript)重设计(Group A 6 文件 + value-u): source_escape 旧设计把源码域反斜杠恒双写(与 marker 起始区分),但源码即 JS 程序、反斜杠恒为字面量内转义序列起始,双写改变程序语义—— /\u{1d306}/u 变 /\\u{1d306}/u,u 模式撞裸 { 直接 SyntaxError(value-u 回归 根因)。现反斜杠原样透传;孤立 surrogate/FFFD 单元仍以 \\uXXXX 转义文本 注入(Rust str 无法承载裸孤立 surrogate,转义文本是唯一注入形态); source_escape_to_key 逆路径仅还原注入 marker(\\ud800..\\udfff/\\ufffd 小写转义文本 → FFFD+hex4 池键形态,物化 decode_key 还原原始单元,正则 .source 按原始源返回;用户转义文本 \\u0041/\\u{1d306} 等逐字透传), \\+marker 拼接形态(数据反斜杠紧邻注入,S7.8.5_A1.4 族 cu=d800 断言)按 前瞻还原 [数据反斜杠, 单元]。source_encoded 标志经 Compiler→Emitter→CompileCtx (嵌套继承)贯通,仅动态编译入口置位;静态源切片维持 pool_key_plain 不变。 RegExp [[Source]]/[[Flags]] 单元域(regexp 构造器):to_units_full → new_string_units_owned 入键(旧 lossy to_string 把孤立 surrogate 单元打成 FFFD,src_match 探针 false);regress 编译仍 lossy(既有缺口,另行报告)。 三并钉:source_escape marker 形态 / 反斜杠透传 / decode_key∘to_key∘escape 闭环(含 \\+marker 相邻与 astral 对)。实测:Group A 8/8、Group B 5/5、 value-u 1/1;8178 文件回归子集(13 目录)双树 A/B 差分 HEAD-only=∅、 13 文件转双树同 pass;text-self.js 双树同 compile 败 (requested module export is not exported: default)归 pre-existing; 七 crate 单测 2879/1(唯一失败 compile_class_declaration_emits_constructor 为 HEAD~0 既有,本提交未引入)。 --- crates/oxide_builtins/src/eval.rs | 14 +- crates/oxide_builtins/src/function.rs | 15 +- crates/oxide_builtins/src/global/uri.rs | 117 +++++++++++----- crates/oxide_builtins/src/regexp.rs | 33 +++-- crates/oxide_compiler/src/compiler.rs | 12 +- crates/oxide_emit/src/emit.rs | 31 ++++- crates/oxide_emit/src/expr/literal.rs | 16 ++- crates/oxide_kernel/src/string_forge.rs | 174 ++++++++++++++++++++++++ crates/oxide_vm/src/test262_host.rs | 8 +- crates/oxide_vm/src/vm.rs | 63 ++++++++- 10 files changed, 417 insertions(+), 66 deletions(-) diff --git a/crates/oxide_builtins/src/eval.rs b/crates/oxide_builtins/src/eval.rs index c9b18405..00dcba15 100644 --- a/crates/oxide_builtins/src/eval.rs +++ b/crates/oxide_builtins/src/eval.rs @@ -16,8 +16,12 @@ use crate::error; /// /// # 步骤 /// 1. 实参非字符串 → 原样返回(规范不 ToString)。 -/// 2. 字符串 → 动态编译为脚本模块(`create_dynamic_script`),以 global 为 this 同步调用。 -/// 3. 编译失败 → SyntaxError;运行异常 → 优先重抛原始异常值(`take_uncaught_value`)。 +/// 2. 字符串 → 单元序列按源码域转义(`string_forge::source_escape`,孤立 +/// surrogate / FFFD 以 `\uXXXX` 转义文本承载,反斜杠为源码语法字符原样 +/// 透传,可被 oxc 直接词法分析)→ 动态编译为脚本模块(`create_dynamic_script` +/// 动态路径源契约), +/// 以 global 为 this 同步调用。 +/// 3. 编译失败 → SyntaxError;运行异常 → 优先重抛原始异常值(`take_uncaught_value`,`eval("throw 1")` 捕获到 `1`)。 /// /// # 边界与前提 /// - 脚本模式:var/函数声明落全局对象,完成值保留,let/const 词法隔离。 @@ -27,7 +31,11 @@ pub fn eval(vm: &mut H, args: &[u8]) -> NativeResult { if !arg.is_string() { return NativeResult::Ok(arg); } - let code = vm.lookup_str(arg).unwrap_or_default(); + let units = match oxide_runtime_api::to_units_full(arg, vm) { + Ok(u) => u, + Err(e) => return NativeResult::Err(error::create_from_text(vm, &e)), + }; + let code = oxide_kernel::string_forge::source_escape(&units); match vm.create_dynamic_script(&code) { Ok(func) => { let global = JsValue::from_js_object(vm.session().global_object().as_ptr() as *mut JsObject); diff --git a/crates/oxide_builtins/src/function.rs b/crates/oxide_builtins/src/function.rs index 056ad529..793d0c4f 100644 --- a/crates/oxide_builtins/src/function.rs +++ b/crates/oxide_builtins/src/function.rs @@ -2,7 +2,7 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; -use oxide_runtime_api::{to_integer_or_infinity, to_string_full, NativeResult, VmHost}; +use oxide_runtime_api::{to_integer_or_infinity, to_units_full, NativeResult, VmHost}; fn invoke_target(vm: &mut H, target_val: JsValue, this_val: JsValue, arg_regs: &[u8]) -> NativeResult { let args: Vec = arg_regs.iter().map(|&r| vm.reg(r)).collect(); @@ -16,7 +16,8 @@ fn invoke_target(vm: &mut H, target_val: JsValue, this_val: JsValue, /// `Function(...)` / `new Function(...)` 构造器:动态编译一个匿名函数。 /// /// 除最后一个实参为函数体外,其余实参为形参名;无实参时函数体为空串。 -/// 编译成功返回函数对象,语法错误抛 SyntaxError,实参 ToString 失败抛 TypeError。 +/// 实参转单元序列后经 `source_escape` 源码域转义再动态编译。 +/// 编译成功返回函数对象,语法错误抛 SyntaxError,实参 ToPrimitive 失败抛 TypeError。 pub fn function_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let arg_regs = &args[1..]; let (param_regs, body_reg) = if arg_regs.is_empty() { @@ -26,16 +27,18 @@ pub fn function_constructor(vm: &mut H, args: &[u8]) -> NativeResult (head.to_vec(), Some(tail[0])) }; + // 动态路径源契约(见 `create_dynamic_function`):形参名与函数体经 + // `source_escape` 源码域转义后拼接 wrap 源码。 let mut params = Vec::with_capacity(param_regs.len()); for &r in ¶m_regs { - match to_string_full(vm.reg(r), vm) { - Ok(s) => params.push(s), + match to_units_full(vm.reg(r), vm) { + Ok(u) => params.push(oxide_kernel::string_forge::source_escape(&u)), Err(e) => return NativeResult::Err(to_string_error_value(vm, &e)), } } let body = match body_reg { - Some(r) => match to_string_full(vm.reg(r), vm) { - Ok(s) => s, + Some(r) => match to_units_full(vm.reg(r), vm) { + Ok(u) => oxide_kernel::string_forge::source_escape(&u), Err(e) => return NativeResult::Err(to_string_error_value(vm, &e)), }, None => String::new(), diff --git a/crates/oxide_builtins/src/global/uri.rs b/crates/oxide_builtins/src/global/uri.rs index a4c6c5d1..448b0e80 100644 --- a/crates/oxide_builtins/src/global/uri.rs +++ b/crates/oxide_builtins/src/global/uri.rs @@ -1,4 +1,5 @@ use oxide_runtime_api::{NativeResult, VmHost}; +use oxide_types::value::JsValue; const URI_UNESCAPED: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()"; const URI_RESERVED: &str = ";/?:@&=+$,#"; @@ -21,11 +22,17 @@ fn encode_uri_string(input: &str, safe: &str) -> String { out } -fn read_percent_byte(bytes: &[u8], pos: usize) -> Result { - if pos + 3 > bytes.len() || bytes[pos] != b'%' { +/// 从单元序列的 `%` 位置读一个 `%XX` 字节(hex 两位须为 ASCII 十六进制单元)。 +fn read_percent_unit(input: &[u16], pos: usize) -> Result { + if pos + 3 > input.len() || input[pos] != 0x25 { return Err(()); } - super::parse_hex_u8(&bytes[pos + 1..pos + 3]).ok_or(()) + let hi = input[pos + 1]; + let lo = input[pos + 2]; + if hi > 0x7F || lo > 0x7F { + return Err(()); + } + super::parse_hex_u8(&[hi as u8, lo as u8]).ok_or(()) } fn utf8_sequence_len(first: u8) -> Result { @@ -38,20 +45,22 @@ fn utf8_sequence_len(first: u8) -> Result { } } -fn decode_uri_string(input: &str, preserve_reserved: bool) -> Result { - let bytes = input.as_bytes(); - let mut out = String::with_capacity(input.len()); +/// 单元口径的 `%XX` 解码:非 `%` 单元原样透传(孤立 surrogate 单元按规范 +/// 不变地往返),`%XX` 序列收集为 UTF-8 字节后还原为码点单元(超平面还原 +/// 为良配 surrogate 对)。`preserve_reserved` 为 true 时单个保留字符的 +/// 转义形态原样保留(decodeURI 的 decode(encode(x)) 稳定性)。 +fn decode_uri_units(input: &[u16], preserve_reserved: bool) -> Result, ()> { + let mut out = Vec::with_capacity(input.len()); let mut i = 0; - while i < bytes.len() { - if bytes[i] != b'%' { - let ch = input[i..].chars().next().expect("valid utf-8 char boundary"); - out.push(ch); - i += ch.len_utf8(); + while i < input.len() { + if input[i] != 0x25 { + out.push(input[i]); + i += 1; continue; } - let first = read_percent_byte(bytes, i)?; + let first = read_percent_unit(input, i)?; let sequence_len = utf8_sequence_len(first)?; let raw_start = i; let mut encoded = Vec::with_capacity(sequence_len); @@ -59,7 +68,7 @@ fn decode_uri_string(input: &str, preserve_reserved: bool) -> Result i += 3; for _ in 1..sequence_len { - let byte = read_percent_byte(bytes, i)?; + let byte = read_percent_unit(input, i)?; if byte & 0b1100_0000 != 0b1000_0000 { return Err(()); } @@ -68,14 +77,21 @@ fn decode_uri_string(input: &str, preserve_reserved: bool) -> Result } let decoded = std::str::from_utf8(&encoded).map_err(|_| ())?; - if preserve_reserved && decoded.len() == 1 { - let ch = decoded.chars().next().expect("single decoded char"); - if URI_RESERVED.contains(ch) { - out.push_str(&input[raw_start..i]); - continue; + let ch = decoded.chars().next().expect("单字节序解出恰一个码点"); + if preserve_reserved && ch.is_ascii() && URI_RESERVED.contains(ch) { + for k in raw_start..i { + out.push(input[k]); } + continue; + } + let cp = ch as u32; + if cp <= 0xFFFF { + out.push(cp as u16); + } else { + let v = cp - 0x10000; + out.push(0xD800 + (v >> 10) as u16); + out.push(0xDC00 + (v & 0x3FF) as u16); } - out.push_str(decoded); } Ok(out) @@ -85,6 +101,16 @@ fn uri_error(vm: &mut H) -> NativeResult { NativeResult::Err(crate::error::create_uri_error(vm, URI_ERROR_MESSAGE)) } +/// 取 URI 函数实参的单元序列(单元口径,不 lossy):无实参等价于处理 +/// `"undefined"`。ToPrimitive 抛错(Symbol 等)原样返回异常值。 +fn string_arg_units(vm: &mut H, args: &[u8]) -> Result, NativeResult> { + let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + match oxide_runtime_api::to_units_full(val, vm) { + Ok(u) => Ok(u), + Err(e) => Err(NativeResult::Err(crate::error::create_from_text(vm, &e))), + } +} + /// `encodeURI`:编码输入为 URI,保留未转义字符与保留字符 `;/?:@&=+$,#`, /// 其余按 UTF-8 字节转成 `%XX`。 pub fn encode_uri(vm: &mut H, args: &[u8]) -> NativeResult { @@ -100,28 +126,39 @@ pub fn encode_uri_component(vm: &mut H, args: &[u8]) -> NativeResult } /// `decodeURI`:解码 `%XX` 序列,但保留字符的转义形式原样保留(不还原), -/// 保证 decode(encode(uri)) 不变。非法序列抛 URIError。 +/// 保证 decode(encode(uri)) 不变。非法序列抛 URIError。单元口径处理:非 `%` +/// 单元(含孤立 surrogate)原样往返。 pub fn decode_uri(vm: &mut H, args: &[u8]) -> NativeResult { - let input = super::string_arg(vm, args); - match decode_uri_string(&input, true) { - Ok(decoded) => NativeResult::Ok(vm.new_string(&decoded)), + let input = match string_arg_units(vm, args) { + Ok(u) => u, + Err(e) => return e, + }; + match decode_uri_units(&input, true) { + Ok(decoded) => NativeResult::Ok(vm.new_string_units(&decoded)), Err(()) => uri_error(vm), } } /// `decodeURIComponent`:解码所有 `%XX` 序列(含保留字符), -/// 非法或截断的 UTF-8 序列抛 URIError。 +/// 非法或截断的 UTF-8 序列抛 URIError。单元口径,孤立 surrogate 原样往返。 pub fn decode_uri_component(vm: &mut H, args: &[u8]) -> NativeResult { - let input = super::string_arg(vm, args); - match decode_uri_string(&input, false) { - Ok(decoded) => NativeResult::Ok(vm.new_string(&decoded)), + let input = match string_arg_units(vm, args) { + Ok(u) => u, + Err(e) => return e, + }; + match decode_uri_units(&input, false) { + Ok(decoded) => NativeResult::Ok(vm.new_string_units(&decoded)), Err(()) => uri_error(vm), } } #[cfg(test)] mod tests { - use super::{decode_uri_string, encode_uri_string, URI_UNESCAPED}; + use super::{decode_uri_units, encode_uri_string, URI_UNESCAPED}; + + fn units(s: &str) -> Vec { + s.encode_utf16().collect() + } #[test] fn encode_uri_keeps_reserved_and_encodes_space() { @@ -139,20 +176,32 @@ mod tests { #[test] fn decode_uri_preserves_reserved_escapes() { assert_eq!( - decode_uri_string("https://example.com/path%3Fq=hello%20world", true).unwrap(), - "https://example.com/path%3Fq=hello world" + decode_uri_units(&units("https://example.com/path%3Fq=hello%20world"), true).unwrap(), + units("https://example.com/path%3Fq=hello world") ); } #[test] fn decode_uri_component_decodes_reserved() { - assert_eq!(decode_uri_string("a%3D1%26b%3D2", false).unwrap(), "a=1&b=2"); + assert_eq!(decode_uri_units(&units("a%3D1%26b%3D2"), false).unwrap(), units("a=1&b=2")); } #[test] fn decode_uri_rejects_malformed_sequences() { - assert!(decode_uri_string("%", false).is_err()); - assert!(decode_uri_string("%E0%A4", false).is_err()); - assert!(decode_uri_string("%ED%A0%80", false).is_err()); + assert!(decode_uri_units(&units("%"), false).is_err()); + assert!(decode_uri_units(&units("%E0%A4"), false).is_err()); + assert!(decode_uri_units(&units("%ED%A0%80"), false).is_err()); + } + + /// 孤立 surrogate 单元非 `%` 序列:decodeURI/decodeURIComponent 均须原样 + /// 往返(S15.1.3.1/2 A2.1 族遍历 0..0xFFFF 的要求)。 + #[test] + fn decode_round_trips_lone_surrogates() { + assert_eq!(decode_uri_units(&[0xD800], true).unwrap(), vec![0xD800]); + assert_eq!(decode_uri_units(&[0xDFFF], false).unwrap(), vec![0xDFFF]); + assert_eq!( + decode_uri_units(&[0x61, 0x25, 0x32, 0x30, 0x62, 0xDBFF], false).unwrap(), + vec![0x61, 0x20, 0x62, 0xDBFF] + ); } } diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index e10413ba..25b88881 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -77,16 +77,28 @@ fn set_prop_at(obj: *mut JsObject, idx: usize, val: JsValue) { /// 支持 backreference/lookaround/命名组/v-flag);非法模式抛 SyntaxError。 /// 编译结果存于对象的 native_fn 槽。 pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { - let (pattern, flags) = if args.len() < 2 { - (String::new(), String::new()) - } else if args.len() < 3 { - let pat = oxide_runtime_api::to_string(vm.reg(args[1])); - (pat, String::new()) + // [[Source]]/[[Flags]] 是 ToString 结果的**单元序列**:`to_units_full` 直取 + // (lossy `to_string` 会把孤立 surrogate 单元折成 FFFD,破坏 source 往返)。 + // regress 编译用 lossy 文本(其匹配语义对孤立 surrogate 本即既有缺口, + // 不因本处改变)。 + let pattern_units = if args.len() < 2 { + Vec::::new() } else { - let pat = oxide_runtime_api::to_string(vm.reg(args[1])); - let fl = oxide_runtime_api::to_string(vm.reg(args[2])); - (pat, fl) + match oxide_runtime_api::to_units_full(vm.reg(args[1]), vm) { + Ok(u) => u, + Err(e) => return NativeResult::Err(crate::error::create_from_text(vm, &e)), + } }; + let flags_units = if args.len() < 3 { + Vec::::new() + } else { + match oxide_runtime_api::to_units_full(vm.reg(args[2]), vm) { + Ok(u) => u, + Err(e) => return NativeResult::Err(crate::error::create_from_text(vm, &e)), + } + }; + let pattern = String::from_utf16_lossy(&pattern_units); + let flags = String::from_utf16_lossy(&flags_units); let (global, ignore_case, multi_line, dot_all, sticky, unicode, has_indices) = parse_flags(&flags); @@ -116,8 +128,9 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { } set_prop(&mut obj, "lastIndex", JsValue::int(0), vm); - set_prop(&mut obj, "source", vm.new_string(&pattern), vm); - set_prop(&mut obj, "flags", vm.new_string(&flags), vm); + // 单元口径物化 source/flags(孤立 surrogate 单元保持往返)。 + set_prop(&mut obj, "source", vm.new_string_units_owned(pattern_units), vm); + set_prop(&mut obj, "flags", vm.new_string_units_owned(flags_units), vm); set_prop(&mut obj, "global", JsValue::bool(global), vm); set_prop(&mut obj, "ignoreCase", JsValue::bool(ignore_case), vm); set_prop(&mut obj, "multiline", JsValue::bool(multi_line), vm); diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 190c8230..379a5c04 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -18,6 +18,8 @@ pub struct Compiler { repl_persist: bool, /// 是否为 eval 脚本:顶层 var/function 声明落全局属性 configurable:true。 is_eval_script: bool, + /// 源码是否为 `source_escape` 产物(动态编译入口:eval / Function 构造器)。 + is_source_encoded: bool, } impl Compiler { @@ -28,6 +30,7 @@ impl Compiler { enable_regalloc: true, repl_persist: false, is_eval_script: false, + is_source_encoded: false, } } @@ -54,10 +57,17 @@ impl Compiler { Self { is_eval_script: enable, ..self } } + /// 标记源码为 `source_escape` 产物(动态编译入口:eval / Function 构造器)。 + pub fn with_source_encoded(self, enable: bool) -> Self { + Self { is_source_encoded: enable, ..self } + } + /// 编译整个 script program:AST → IR(`Emitter::emit_program`)→ 统一 IR 管线。 pub fn compile(&self, program: &oxide_parser::Program) -> Result { crate::compiler_debug!("compile: starting..."); - let ir = Emitter::new().emit_program(program, self.repl_persist, self.is_eval_script)?; + let ir = Emitter::new() + .with_source_encoded(self.is_source_encoded) + .emit_program(program, self.repl_persist, self.is_eval_script)?; self.compile_ir(ir) } diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 90d46688..0642d558 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -34,8 +34,17 @@ pub use oxide_parser::VariableDeclarationKind; /// AST 语法树节点与运算符类型,re-export 自 parser。 pub use oxide_parser::{AssignmentOperator, BinaryOperator, Expression, Statement, UnaryOperator}; -/// 编译入口(marker 类型)。方法按语法域组织在 `impl Emitter` 中。 -pub struct Emitter; +/// 编译入口。方法按语法域组织在 `impl Emitter` 中。 +pub struct Emitter { + /// 源码是否为 `oxide_kernel::string_forge::source_escape` 产物 + /// (eval/Function 动态编译):源文本内的孤立 surrogate 单元 / FFFD 以 + /// `\uXXXX` 转义文本承载(oxc 不可见裸单元——Rust str 无孤立 surrogate, + /// 转义文本是唯一注入形态),反斜杠原样透传。正则字面量的源文本切片据此 + /// 经 `source_escape_to_key` 还原池键 marker 形态入池(物化时 + /// `decode_key` 还原为原始单元,`.source` 按原始源返回);静态源切片不含 + /// 注入 marker,走 `pool_key_plain`。 + source_encoded: bool, +} /// 判断 f64 是否为整数值且在 i32 范围内(整数常量编码用)。 pub fn is_int_literal(value: f64) -> bool { @@ -194,6 +203,9 @@ pub struct CompileCtx { /// 是否为 eval 脚本:脚本顶层 var/function 声明落全局对象时属性 /// configurable:true(普通脚本顶层为 false)。仅动态脚本入口设置。 pub(crate) is_eval_script: bool, + /// 源码是否为 `source_escape` 产物(动态编译入口):正则字面量源文本切片 + /// 的池键形态据此选择(见 [`Emitter::source_encoded`])。 + pub(crate) source_encoded: bool, pub(crate) static_block_this_reg: Option, pub(crate) field_buffer: Option, /// 类构造器模块中 `@@field_keys` upvalue 下标(实例字段 computed key 数组)。 @@ -343,6 +355,7 @@ impl CompileCtx { is_global_scope: false, repl_persist: false, is_eval_script: false, + source_encoded: false, static_block_this_reg: None, field_buffer: None, field_keys_uv: None, @@ -825,9 +838,16 @@ impl ConstantKey { } impl Emitter { - /// 构造空 `Emitter`(无内部状态,所有状态在 `CompileCtx` 中)。 + /// 构造空 `Emitter`(静态编译口径:源文本为良形 UTF-8,无注入 marker)。 pub fn new() -> Self { - Self + Self { source_encoded: false } + } + + /// 置位编码源口径(见 [`Emitter::source_encoded`]):仅动态编译入口 + /// (eval / Function 构造器)经编译器设置。 + pub fn with_source_encoded(mut self, enable: bool) -> Self { + self.source_encoded = enable; + self } /// 生成运行时抛 `kind` 类型错误的指令序列,返回一个未定义 dummy 寄存器 @@ -1151,6 +1171,8 @@ impl Emitter { // 传递 enclosing_this_reg:嵌套箭头函数捕获正确的 `this`。 ctx.enclosing_this_reg = parent_ctx.enclosing_this_reg; + // 编码源口径是整棵编译树属性(嵌套函数仍在同一编码源内)。 + ctx.source_encoded = parent_ctx.source_encoded; // 箭头函数词法继承 super;类方法体顶层编译也需要类提供的 super 上下文。 if matches!(body_context, FunctionBodyContext::Arrow | FunctionBodyContext::ClassElement) { @@ -1694,6 +1716,7 @@ impl Emitter { ctx.is_global_scope = true; ctx.repl_persist = repl_persist; ctx.is_eval_script = is_eval_script; + ctx.source_encoded = self.source_encoded; // 脚本顶层严格模式由源码 "use strict" directive 决定(嵌套函数经父 ctx 继承)。 ctx.is_strict = program.has_use_strict_directive(); diff --git a/crates/oxide_emit/src/expr/literal.rs b/crates/oxide_emit/src/expr/literal.rs index e8e45507..6d6898c0 100644 --- a/crates/oxide_emit/src/expr/literal.rs +++ b/crates/oxide_emit/src/expr/literal.rs @@ -89,10 +89,22 @@ impl Emitter { if regress::Regex::with_flags(&pattern, flags.as_str()).is_err() { return Err(format!("SyntaxError: Invalid regular expression: /{pattern}/{flags}")); } - let pat_ci = ctx.add_constant(Constant::String(pattern)); + // 源文本池键:静态源切片经 `pool_key_plain` 逃逸真实 FFFD(防 + // 物化侧把裸 FFFD+hex4 误读为 surrogate marker),转义文本 + // `decode_key` 逐字透传,正则引擎自行解析转义;编码源切片经 + // `source_escape_to_key` 把注入的 `\ud800`..`\udfff`/`\ufffd` + // 转义文本还原为池键 marker 形态(物化还原原始单元, + // `.source` 按原始源返回),用户转义文本逐字透传。 + let pattern_key = if ctx.source_encoded { + oxide_kernel::string_forge::source_escape_to_key(&pattern) + } else { + crate::shared::string_pool::pool_key_plain(&pattern) + }; + let flags_key = crate::shared::string_pool::pool_key_plain(&flags); + let pat_ci = ctx.add_constant(Constant::String(pattern_key)); let pat_reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(pat_reg), pat_ci)); - let flags_ci = ctx.add_constant(Constant::String(flags)); + let flags_ci = ctx.add_constant(Constant::String(flags_key)); let flags_reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(flags_reg), flags_ci)); let r = ctx.alloc_reg(); diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index f6c3ef0c..7c62afac 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -120,6 +120,124 @@ pub fn decode_key(text: &str) -> Vec { out } +/// 动态编译源(eval / Function 构造器 / `$262.evalScript`)的单元 → 源码文本 +/// 编码。产物直接是可被 oxc 词法分析的源码文本;文本本身是 JS 程序,反斜杠 +/// 是语法字符(字面量内构成转义序列),必须原样透传,不得改写: +/// - 孤立 surrogate 单元 U → 文本 `\uXXXX`(4 位小写十六进制); +/// - 单元 `U+FFFD` → 文本 `\ufffd`(裸 FFFD 会被 oxc 判为二进制文件而终止 +/// 解析,源码域必须转义); +/// - 良配 surrogate 对 → 对应超平面字符,其余单元(含反斜杠)逐字恒等。 +/// +/// 字符串/模板字面量中的 `\uXXXX` 转义经 oxc 值域还原为真值(孤立 surrogate +/// 置 `lone_surrogates` 位),再按值域 marker 形态入池,与静态源码同路径。 +/// 编码源中的正则字面量按源文本切片经 [`source_escape_to_key`] 还原注入 +/// marker 后入池键(静态源切片走 `pool_key_plain`),物化时 `decode_key` +/// 还原原始单元。 +pub fn source_escape(units: &[u16]) -> String { + let mut out = String::with_capacity(units.len()); + let mut i = 0; + while i < units.len() { + let u = units[i]; + if (0xD800..=0xDBFF).contains(&u) && i + 1 < units.len() { + let lo = units[i + 1]; + if (0xDC00..=0xDFFF).contains(&lo) { + let cp = 0x10000u32 + (((u as u32) - 0xD800) << 10) + (lo as u32 - 0xDC00); + out.push(char::from_u32(cp).expect("良配 surrogate 对必映射为合法码点")); + i += 2; + continue; + } + } + match u { + 0xD800..=0xDFFF => out.push_str(&format!("\\u{:04x}", u)), + 0xFFFD => out.push_str("\\ufffd"), + _ => out.push(char::from_u32(u as u32).expect("非 surrogate 单元必为合法码点")), + } + i += 1; + } + out +} + +/// [`source_escape`] 的逆路径(源文本域 → 池键域):仅当文本确为 +/// `source_escape` 产物(动态编译源的正则字面量源文本切片)时调用;静态源 +/// 切片不含注入 marker,直接走 `pool_key_plain`: +/// - `\ud800`..`\udfff`(小写 hex)→ FFFD+hex4(键域孤立 surrogate 形态, +/// 对应 `source_escape` 注入的孤立 surrogate 单元); +/// - `\ufffd` → FFFD+`fffd`(键域 FFFD 转义形态); +/// - 其余反斜杠序列(`\u0041`、`\u{1d306}`、`\n` 等用户转义文本)逐字透传: +/// 正则引擎自行解析转义,物化 pattern 保持源文本形态,`.source` 按书写 +/// 返回; +/// - `\\` + marker 转义 形态(数据反斜杠紧邻注入 marker,`source_escape` +/// 拼接产物)按 [数据反斜杠, 单元] 还原;`\\` 后非 marker 时整对透传 +/// (用户转义反斜杠)。 +/// +/// 已知边界:动态源中用户自写的 `\ud800`..`\udfff`/`\ufffd` 小写转义文本 +/// (及 `\\` 紧邻该文本)与注入 marker 不可区分,一律还原为单元——只影响 +/// 该边缘场景下动态正则 `.source` 的形态(转义文本 vs 原始单元),静态源 +/// 不受影响。 +pub fn source_escape_to_key(text: &str) -> String { + let chars: Vec = text.chars().collect(); + let mut out = String::with_capacity(text.len()); + let mut i = 0; + while i < chars.len() { + if chars[i] != '\\' { + out.push(chars[i]); + i += 1; + continue; + } + // `\\` 前瞻:次反斜杠后若紧跟 marker 转义(`u`+4 位小写 hex),首 + // 反斜杠归数据、marker 归注入(`source_escape` 把数据反斜杠与 + // 注入 marker 拼接成 `\\ud800` 形态);否则 `\\` 对整体透传 + // (用户转义反斜杠,正则引擎自行解析)。 + if i + 1 < chars.len() && chars[i + 1] == '\\' { + let is_marker = i + 7 <= chars.len() + && chars[i + 2] == 'u' + && chars[i + 3..i + 7] + .iter() + .all(|c| matches!(c, '0'..='9' | 'a'..='f')); + if is_marker { + out.push('\\'); + i += 1; + } else { + out.push('\\'); + out.push('\\'); + i += 2; + } + continue; + } + // `\u` marker / 透传:仅 4 位小写 hex 且值域命中(FFFD / surrogate + // 段)还原为键域 marker,其余转义文本逐字透传。 + if i + 1 < chars.len() && chars[i + 1] == 'u' { + let tail_len = 4.min(chars.len() - i - 2); + let tail = &chars[i + 2..i + 2 + tail_len]; + let ok = tail + .iter() + .all(|c| matches!(c, '0'..='9' | 'a'..='f')); + let v: u32 = ok + .then(|| tail.iter().fold(0u32, |v, c| v * 16 + c.to_digit(16).unwrap())) + .unwrap_or(0); + if tail_len == 4 && ok && (v == 0xFFFD || (0xD800..=0xDFFF).contains(&v)) { + out.push('\u{FFFD}'); + out.extend(tail); + } else { + out.push('\\'); + out.push('u'); + out.extend(tail); + } + i += 2 + tail_len; + continue; + } + // 其余反斜杠组合透传(`\n`、行尾孤立反斜杠等)。 + out.push('\\'); + if i + 1 < chars.len() { + out.push(chars[i + 1]); + i += 2; + } else { + i += 1; + } + } + out +} + /// 一条 intern 过的键。`data` 是泄漏的 `&'static str`——永久键从不释放 /// (按设计 append-only),所以泄漏即存储模型,而非 bug。键 id 与 64 位 /// 哈希经 `DashMap` 的哈希键→候选 id 表寻址,条目自身不存哈希。 @@ -473,6 +591,62 @@ mod tests { assert_eq!(decode_key("a\u{FFFD}zz"), &[0x61, 0xFFFD, 0x7A, 0x7A]); } + #[test] + fn source_escape_marker_forms() { + // 孤立 surrogate / FFFD → 源码域 \u 转义形态;配对与 BMP 恒等。 + assert_eq!(source_escape(&[0xD800]), "\\ud800"); + assert_eq!(source_escape(&[0xDFFF]), "\\udfff"); + assert_eq!(source_escape(&[0xFFFD]), "\\ufffd"); + assert_eq!(source_escape(&[0x61, 0xD800, 0x62]), "a\\ud800b"); + assert_eq!(source_escape(&[0xD83D, 0xDE00]), "\u{1F600}"); + } + + /// 源码是 JS 程序,反斜杠为语法字符:原样透传,转义文本形态逐字保留 + /// (正则/字符串字面量内 `\u{1d306}`、`\\` 等序列不得被改写)。 + #[test] + fn source_escape_backslash_passthrough() { + // 正则字面量源文本(u 模式 \u{..} 转义):逐字恒等。 + let re_src: Vec = "/\\u{1d306}/u".encode_utf16().collect(); + assert_eq!(source_escape(&re_src), "/\\u{1d306}/u"); + // 转义文本(反斜杠 + "ud800" 字面文本)与孤立 surrogate 单元同形: + // 源码域中 `\ud800` 文本即孤立 surrogate 的源码表示,两者编码同一 + // 源码文本是设计使然(源码即程序,oxc 只见转义文本)。 + let text: Vec = "\\ud800".encode_utf16().collect(); + assert_eq!(source_escape(&text), "\\ud800"); + assert_eq!(source_escape(&[0xD800]), source_escape(&text)); + // 双反斜杠(字面量内转义反斜杠)恒等。 + let dbl: Vec = "\\\\".encode_utf16().collect(); + assert_eq!(source_escape(&dbl), "\\\\"); + } + + /// 键域闭环:`decode_key ∘ source_escape_to_key ∘ source_escape` 对单元 + /// 序列恒等(注入 marker 还原、用户转义文本透传、双反斜杠与 astral 对)。 + #[test] + fn source_escape_roundtrip_via_key() { + let cases: Vec> = vec![ + vec![0xD800], + vec![0xDFFF], + vec![0xFFFD], + vec![0x5C], + // 用户字面转义文本(非 marker 值域 / \u{..} 形态):逐字透传。 + vec![0x5C, 0x75, 0x30, 0x30, 0x34, 0x31], + vec![0x5C, 0x75, 0x7B, 0x31, 0x64, 0x33, 0x30, 0x36, 0x7D], + vec![0x5C, 0x5C], + // 数据反斜杠紧邻注入 marker(`\\ud800`/`\\ufffd` 拼接形态)。 + vec![0x5C, 0xD800], + vec![0x5C, 0xFFFD], + // FFFD 单元后跟 "d800" 文本:marker 消费恰 4 字符,不吞后续。 + vec![0xFFFD, 0x64, 0x38, 0x30, 0x30], + vec![0x66, 0x66, 0x66, 0x64], + vec![0xD83D, 0xDE00], + ]; + for units in &cases { + let escaped = source_escape(units); + let key = source_escape_to_key(&escaped); + assert_eq!(&decode_key(&key), units.as_slice(), "roundtrip 失败: escaped={escaped:?}"); + } + } + #[test] fn string_ptr_materializes_units() { let interner = PermInterner::new(); diff --git a/crates/oxide_vm/src/test262_host.rs b/crates/oxide_vm/src/test262_host.rs index 88586ae0..d5573a6f 100644 --- a/crates/oxide_vm/src/test262_host.rs +++ b/crates/oxide_vm/src/test262_host.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_runtime_api::{to_string_full, NativeResult}; +use oxide_runtime_api::{to_units_full, NativeResult}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; @@ -36,10 +36,12 @@ pub fn eval_script(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::undefined()); } - let code = match to_string_full(vm.reg(args[1]), vm) { - Ok(s) => s, + let units = match to_units_full(vm.reg(args[1]), vm) { + Ok(u) => u, Err(e) => return NativeResult::Err(oxide_builtins::error::create_from_text(vm, &e)), }; + // 动态路径源契约:源码域转义形态传源(见 `create_dynamic_function`)。 + let code = oxide_kernel::string_forge::source_escape(&units); match vm.create_dynamic_function(&[], &code) { Ok(func) => { let global = JsValue::from_js_object(vm.session().global_object().as_ptr() as *mut JsObject); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index cb45c301..f69288df 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -75,7 +75,9 @@ fn canonical_index_units(units: &[u16]) -> Option { 0x30..=0x39 => u - 0x30, _ => return None, }; - v = v.checked_mul(10)? + d as u32; + // 两位都须 overflow 检查:`checked_mul(10) + d` 的裸加在 10 位串 + // (如 "4294967296")会 mod 2^32 回绕出假规范下标。 + v = v.checked_mul(10)?.checked_add(d as u32)?; } (v < INT_KEY_COUNT).then_some(v) } @@ -1424,7 +1426,14 @@ impl Vm { // 其它原始值(BigInt 等):ToPropertyKey 一律转字符串并走规范化,避免与 // 数字键区间分裂(`o[5n]` 与 `o["5"]`/`o[5]` 必须同键)。 let units = oxide_runtime_api::to_units_full(val, self)?; - Ok(self.string_key_units(&units)) + if std::env::var("OXIDE_KEY_PROBE").is_ok() { + eprintln!("[KEYPROBE] tail fallback units={:?}", units); + } + let si = self.string_key_units(&units); + if std::env::var("OXIDE_KEY_PROBE").is_ok() { + eprintln!("[KEYPROBE] tail result si={}", si); + } + Ok(si) } /// 从单元序列推导属性键 si(`property_key_si` 字符串分支的口径抽取): @@ -2697,6 +2706,11 @@ impl Vm { /// 动态编译函数(`Function` 构造器路径):把参数列表与函数体 wrap 成匿名函数 /// 源码,走完整编译链后取匿名函数模块追加进 VM 平表,返回对应函数对象。 /// + /// # 源契约 + /// `params` 与 `body` 须为 `string_forge::source_escape` 产物(源码域转义 + /// 形态):孤立 surrogate / FFFD 单元以 `\uXXXX` 转义文本承载,反斜杠 + /// 原样透传。 + /// /// # 步骤 /// 1. wrap 源码 `function anonymous(p...) { body }`,parse + compile。 /// 2. 取 `sub_modules[0]`(匿名函数模块),把其子树 flat_id 重编号到平表末尾 @@ -2720,7 +2734,10 @@ impl Vm { let allocator = oxide_parser::Allocator::default(); let program = oxide_parser::parse(&allocator, &source) .map_err(|errs| errs.into_iter().map(|e| e.message).collect::>().join("\n"))?; - let mut module = oxide_compiler::compiler::Compiler::new().compile(&program)?; + // 动态路径源契约:调用方以源码域转义(`string_forge::source_escape`) + // 形态传源;正则字面量源文本常量与静态同路径(`pool_key_plain`)。 + let mut module = + oxide_compiler::compiler::Compiler::new().with_source_encoded(true).compile(&program)?; let anonymous = module.sub_modules.remove(0); // 形参数以编译结果为准:单个实参 "a,b,c" 拼接后解析为 3 个形参 // (ES 动态函数把非末位实参以逗号连接成参数串再解析)。 @@ -2771,8 +2788,10 @@ impl Vm { let program = oxide_parser::parse(&allocator, code) .map_err(|errs| errs.into_iter().map(|e| e.message).collect::>().join("\n"))?; // eval 脚本:顶层 var/function 声明落全局属性 configurable:true。 + // 动态路径源契约同 `create_dynamic_function`(源码域转义形态传源)。 let module = oxide_compiler::compiler::Compiler::new() .with_eval_script(true) + .with_source_encoded(true) .compile(&program)?; // 根模块 flat_id=0 传 base+1,重编号后落 base(避开 sub_module_index()==0 // 守卫)。make_mut 彼时平表 Arc 强引用唯一持有者是本表,原地扩展不分叉。 @@ -2833,6 +2852,8 @@ impl Vm { #[cfg(test)] mod tests { use super::{opcode, ForOfEntry, JsValue, TryHandler, Vm}; + use super::{canonical_index_of, canonical_index_units}; + use oxide_types::private_key::INT_KEY_COUNT; use oxide_bytecode::module::CompiledModule; use oxide_runtime_api::{NativeResult, VmHost}; use oxide_types::object::NativeFnPtr; @@ -3389,4 +3410,40 @@ mod tests { ); assert!(!frame.is_derived_constructor, "普通函数非 derived 构造器"); } + + fn units_of(s: &str) -> Vec { + s.chars().map(|c| c as u16).collect() + } + + #[test] + fn canonical_index_str_boundaries() { + assert_eq!(canonical_index_of("0"), Some(0)); + assert_eq!(canonical_index_of("1073741823"), Some(INT_KEY_COUNT - 1)); + // 2^30 起不再是整数键候选;2^32 边界串(ToUint32 回绕源)必须走字符串键 + assert_eq!(canonical_index_of("1073741824"), None); + assert_eq!(canonical_index_of("4294967295"), None); + assert_eq!(canonical_index_of("4294967296"), None); + assert_eq!(canonical_index_of("4294967299"), None); + // 非规范形态:前导零、符号、小数、空串 + assert_eq!(canonical_index_of("007"), None); + assert_eq!(canonical_index_of("-1"), None); + assert_eq!(canonical_index_of("1.5"), None); + assert_eq!(canonical_index_of(""), None); + } + + #[test] + fn canonical_index_units_boundaries() { + assert_eq!(canonical_index_units(&units_of("0")), Some(0)); + assert_eq!(canonical_index_units(&units_of("1073741823")), Some(INT_KEY_COUNT - 1)); + assert_eq!(canonical_index_units(&units_of("1073741824")), None); + // 10 位串在 u32 累加尾位溢出:回绕会造出假规范下标,须溢出检查归 None + assert_eq!(canonical_index_units(&units_of("4294967295")), None); + assert_eq!(canonical_index_units(&units_of("4294967296")), None); + assert_eq!(canonical_index_units(&units_of("4294967299")), None); + assert_eq!(canonical_index_units(&units_of("9999999999")), None); + assert_eq!(canonical_index_units(&units_of("007")), None); + assert_eq!(canonical_index_units(&units_of("-1")), None); + assert_eq!(canonical_index_units(&units_of("1.5")), None); + assert_eq!(canonical_index_units(&[]), None); + } }