From 84bcebee538668ff4ed06b669e87337ce2e2063d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 12 Oct 2026 12:47:12 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=95=B0=E5=80=BC=E5=9F=9F=E5=8F=8C?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E6=8A=98=E5=8F=A0=E5=8A=A0=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 41 ++--- Cargo.toml | 2 +- crates/oxide_emit/Cargo.toml | 1 + crates/oxide_emit/src/expr/operator/fold.rs | 190 ++++++++++++++++++++ crates/oxide_emit/src/expr/operator/mod.rs | 13 ++ crates/oxide_emit/tests/emit_ir_tests.rs | 19 ++ crates/oxide_emit/tests/fold_tests.rs | 145 +++++++++++++++ crates/oxide_runtime_api/src/coercion.rs | 102 +++++++---- crates/oxide_runtime_api/src/lib.rs | 3 +- 9 files changed, 461 insertions(+), 55 deletions(-) create mode 100644 crates/oxide_emit/src/expr/operator/fold.rs create mode 100644 crates/oxide_emit/tests/fold_tests.rs diff --git a/Cargo.lock b/Cargo.lock index b4d2fb1a..a7e51a92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.16.14" +version = "0.16.15" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.16.14" +version = "0.16.15" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.16.14" +version = "0.16.15" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.16.14" +version = "0.16.15" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1271,7 +1271,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.16.14" +version = "0.16.15" dependencies = [ "num-bigint", "oxc_allocator", @@ -1279,6 +1279,7 @@ dependencies = [ "oxide_ir", "oxide_log", "oxide_parser", + "oxide_runtime_api", "oxide_types", "regex", "regress", @@ -1286,7 +1287,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1298,7 +1299,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1307,7 +1308,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.16.14" +version = "0.16.15" dependencies = [ "bumpalo", "dashmap", @@ -1321,7 +1322,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1333,7 +1334,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.16.14" +version = "0.16.15" dependencies = [ "tracing", "tracing-appender", @@ -1342,11 +1343,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.16.14" +version = "0.16.15" [[package]] name = "oxide_parser" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1361,7 +1362,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1377,7 +1378,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.16.14" +version = "0.16.15" dependencies = [ "num-bigint", "num-traits", @@ -1390,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.16.14" +version = "0.16.15" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1410,7 +1411,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.16.14" +version = "0.16.15" dependencies = [ "bumpalo", "dashmap", @@ -1423,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.16.14" +version = "0.16.15" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index c1adea69..821360dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.16.14" +version = "0.16.15" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_emit/Cargo.toml b/crates/oxide_emit/Cargo.toml index 7064d75b..60d8ecd4 100644 --- a/crates/oxide_emit/Cargo.toml +++ b/crates/oxide_emit/Cargo.toml @@ -13,6 +13,7 @@ oxc_allocator.workspace = true oxide_bytecode = { path = "../oxide_bytecode" } oxide_ir = { path = "../oxide_ir" } oxide_log = { path = "../oxide_log" } +oxide_runtime_api = { path = "../oxide_runtime_api" } regex.workspace = true regress.workspace = true num-bigint = "0.4" diff --git a/crates/oxide_emit/src/expr/operator/fold.rs b/crates/oxide_emit/src/expr/operator/fold.rs new file mode 100644 index 00000000..e5e947cc --- /dev/null +++ b/crates/oxide_emit/src/expr/operator/fold.rs @@ -0,0 +1,190 @@ +//! 双常量折叠:两操作数均为字面量且通过语义守卫时,编译期折成单常量, +//! 发射单条 `LOAD_CONST`。折值编码逐运算符镜像运行时指令的产物编码, +//! 保证物化后与运行时执行结果逐字节一致。 +//! +//! 三条语义守卫:任一操作数为 BigInt 字面量不折(BigInt 混合运行期抛 +//! TypeError,BigInt 与 BigInt 有独立指令族);`DIV`/`MOD` 除数为零不折、 +//! `EXP` 指数为负不折(保持运行时指令为除零/负指数编码的唯一来源)。 +//! 字符串字面量不折(字符串域是独立任务)。 + +use oxide_bytecode::module::Constant; +use oxide_parser::{BinaryOperator, Expression, UnaryOperator}; +use oxide_runtime_api::{ + abstract_eq_primitives, relational_compare, strict_equality, to_int32, to_number, to_uint32, +}; +use oxide_types::value::JsValue; + +use crate::is_int_literal; + +/// 字面量表达式提取常量:剥括号与一元负号后,数字(`is_int_literal` 分派 +/// `Int`/`Number`)、布尔、null 三类;BigInt 字面量、字符串字面量、RegExp 与 +/// 非字面量一律 `None`。 +/// +/// 一元负号只沿 `UnaryNegation` 链剥(含嵌套负号与括号),其余一元运算符 +/// (`!`/`typeof`/`void`/`~`/`+`/`delete`)终止剥壳不折。 +pub(crate) fn literal_constant(expr: &Expression) -> Option { + let (inner, negated) = peel_negation(expr); + match inner { + Expression::NumericLiteral(n) => { + let value = if negated { -n.value } else { n.value }; + if is_int_literal(value) { + Some(Constant::Int(value as i32)) + } else { + Some(Constant::Number(value)) + } + } + Expression::BooleanLiteral(b) if !negated => Some(Constant::Boolean(b.value)), + Expression::NullLiteral(_) if !negated => Some(Constant::Null), + _ => None, + } +} + +/// 剥括号与一元负号链:返回内层表达式与负号层数奇偶(奇数层取负)。 +fn peel_negation<'a>(expr: &'a Expression<'a>) -> (&'a Expression<'a>, bool) { + let mut e = expr; + let mut negated = false; + loop { + while let Expression::ParenthesizedExpression(p) = e { + e = &p.expression; + } + if let Expression::UnaryExpression(u) = e { + if u.operator == UnaryOperator::UnaryNegation { + negated = !negated; + e = &u.argument; + continue; + } + } + break; + } + (e, negated) +} + +/// 常量转 `JsValue`(纯 NaN-box 构造,零分配):`Int`/`Number`/`Boolean`/`Null` +/// 四臂,其余臂不可达(BigInt/String/Undefined 已被守卫排除)。 +fn const_to_jsvalue(c: &Constant) -> JsValue { + match c { + Constant::Int(i) => JsValue::int(*i), + Constant::Number(d) => JsValue::float(*d), + Constant::Boolean(b) => JsValue::bool(*b), + Constant::Null => JsValue::null(), + _ => unreachable!("折叠操作数守卫已排除 BigInt/String/Undefined"), + } +} + +/// 常量的数值(`Int`/`Number`/`Boolean`/`Null` 均走 `ToNumber` 口径)。 +fn const_to_number(c: &Constant) -> f64 { + to_number(const_to_jsvalue(c)) +} + +/// 双常量折叠分派:按操作码计算折值,守卫命中返回 `None`。 +/// +/// 折值编码逐运算符镜像运行时产物: +/// - `ADD`:int+int 经 `checked_add` 在 i32 内保 `Int`,溢出升 `Number`(镜像 +/// `int_add`);其余组合 f64 加(镜像 `dispatch_add` 的 `to_number` 路径) +/// - `SUB`/`MUL`/`DIV`/`MOD`:恒 `Number`(镜像 `binary_arith!` 的 `as f64`) +/// - `EXP`:恒 `Number` 的 `powf`(镜像 `exp_result`) +/// - `BIT_AND`/`BIT_OR`/`BIT_XOR`/`SHL`/`SHR`:`Int`(镜像 `to_int32`) +/// - `USHR`:结果 ≤ i32::MAX 保 `Int`,否则升 `Number`(镜像 `write_ushr_result`) +/// - `LT`/`GT`/`LTE`/`GTE`:`Boolean`(镜像 `relational_compare`,GT/LTE 交换 +/// 操作数,LTE/GTE 取反,`None` 归 false) +/// - `EQ`/`NEQ`:`Boolean`(镜像 `abstract_eq` 原始值核心); +/// `STRICT_EQ`/`STRICT_NEQ`:`Boolean`(镜像 `strict_equality`) +/// +/// 守卫:`DIV`/`MOD` 除数为零(`Int(0)` 与 `Number(±0.0)` 均判零)、`EXP` +/// 指数为负(`Int`/`Number` 负值均判负)返回 `None`,落回运行时路径。 +pub(crate) fn fold_binary(op: BinaryOperator, l: &Constant, r: &Constant) -> Option { + match op { + BinaryOperator::Addition => { + // int+int:checked_add 在 i32 内保 Int,溢出升 f64(镜像 int_add)。 + if let (Constant::Int(a), Constant::Int(b)) = (l, r) { + return a + .checked_add(*b) + .map(Constant::Int) + .or(Some(Constant::Number(*a as f64 + *b as f64))); + } + Some(Constant::Number(const_to_number(l) + const_to_number(r))) + } + BinaryOperator::Subtraction + | BinaryOperator::Multiplication + | BinaryOperator::Division + | BinaryOperator::Remainder => { + // 除零守卫:Int(0) 与 Number(±0.0) 均判零(v == 0.0 对 -0.0 亦真)。 + if matches!(op, BinaryOperator::Division | BinaryOperator::Remainder) && const_to_number(r) == 0.0 { + return None; + } + // 恒 f64(镜像 binary_arith! 宏的 as f64)。 + let (ln, rn) = (const_to_number(l), const_to_number(r)); + Some(match op { + BinaryOperator::Subtraction => Constant::Number(ln - rn), + BinaryOperator::Multiplication => Constant::Number(ln * rn), + BinaryOperator::Division => Constant::Number(ln / rn), + _ => Constant::Number(ln % rn), + }) + } + BinaryOperator::Exponential => { + // 负指数守卫:Int 与 Number 负值均判负。 + if const_to_number(r) < 0.0 { + return None; + } + // 恒 f64 powf(镜像 exp_result)。 + Some(Constant::Number(const_to_number(l).powf(const_to_number(r)))) + } + BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => { + let (a, b) = (to_int32(const_to_jsvalue(l)), to_int32(const_to_jsvalue(r))); + Some(match op { + BinaryOperator::BitwiseAnd => Constant::Int(a & b), + BinaryOperator::BitwiseOR => Constant::Int(a | b), + _ => Constant::Int(a ^ b), + }) + } + BinaryOperator::ShiftLeft | BinaryOperator::ShiftRight => { + let a = to_int32(const_to_jsvalue(l)); + let shift = to_uint32(const_to_jsvalue(r)) & 0x1F; + Some(match op { + BinaryOperator::ShiftLeft => Constant::Int(a << shift), + _ => Constant::Int(a >> shift), + }) + } + BinaryOperator::ShiftRightZeroFill => { + let a = to_int32(const_to_jsvalue(l)) as u32; + let shift = to_uint32(const_to_jsvalue(r)) & 0x1F; + let result = a >> shift; + // 结果 ≤ i32::MAX 保 Int,否则升 Number(镜像 write_ushr_result)。 + if result <= i32::MAX as u32 { + Some(Constant::Int(result as i32)) + } else { + Some(Constant::Number(result as f64)) + } + } + BinaryOperator::LessThan => relational(l, r, false, false), + BinaryOperator::GreaterThan => relational(l, r, true, false), + BinaryOperator::LessEqualThan => relational(l, r, true, true), + BinaryOperator::GreaterEqualThan => relational(l, r, false, true), + BinaryOperator::Equality => { + Some(Constant::Boolean(abstract_eq_primitives(const_to_jsvalue(l), const_to_jsvalue(r)))) + } + BinaryOperator::Inequality => { + Some(Constant::Boolean(!abstract_eq_primitives(const_to_jsvalue(l), const_to_jsvalue(r)))) + } + BinaryOperator::StrictEquality => { + Some(Constant::Boolean(strict_equality(const_to_jsvalue(l), const_to_jsvalue(r)))) + } + BinaryOperator::StrictInequality => { + Some(Constant::Boolean(!strict_equality(const_to_jsvalue(l), const_to_jsvalue(r)))) + } + // IN/INSTANCEOF 是对象操作,不折。 + BinaryOperator::In | BinaryOperator::Instanceof => None, + } +} + +/// 关系比较折叠:镜像运行时 dispatch 口径(GT/LTE 交换操作数,LTE/GTE 取反, +/// `None` 归 false)。 +fn relational(l: &Constant, r: &Constant, swap: bool, negate: bool) -> Option { + let (a, b) = if swap { + (const_to_jsvalue(r), const_to_jsvalue(l)) + } else { + (const_to_jsvalue(l), const_to_jsvalue(r)) + }; + let cmp = relational_compare(a, b).unwrap_or(false); + Some(Constant::Boolean(if negate { !cmp } else { cmp })) +} diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index cc94c745..46db97bf 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -2,6 +2,8 @@ //! 逻辑与 `??` 用短路跳转,复合求值利用 `is_side_effect_free` 优化。 //! 函数:`emit_operator` 及各类 `emit_*_expression`。 +mod fold; + use crate::{is_side_effect_free, BinaryOperator, CompileCtx, Emitter}; use oxide_bytecode::module::Constant; use oxide_bytecode::opcode::OpCode; @@ -30,6 +32,17 @@ impl Emitter { fn emit_binary_expression( &self, bin: &oxide_parser::BinaryExpression, ctx: &mut CompileCtx, ) -> Result { + // 双常量折叠:两操作数均为字面量且通过语义守卫时,编译期折单常量, + // 发射单条 LOAD_CONST(编码镜像运行时指令产物,见 fold.rs)。 + // 置于 CONCAT_N 摊平之前,命中即提前返回。 + if let (Some(l), Some(r)) = (fold::literal_constant(&bin.left), fold::literal_constant(&bin.right)) { + if let Some(folded) = fold::fold_binary(bin.operator, &l, &r) { + let idx = ctx.add_constant(folded); + let r = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(r), idx)); + return Ok(r); + } + } // Addition 左结合链摊平:≥3 操作数合并为单条 CONCAT_N(单趟预分配拼接, // 消除 N-2 次中间串分配)。只下钻 left 链、right 括号不拆(a+(b+c) 退化 ADD), // 保 f64 结合性与源码求值序。 diff --git a/crates/oxide_emit/tests/emit_ir_tests.rs b/crates/oxide_emit/tests/emit_ir_tests.rs index 4e16d1ef..a84c8f05 100644 --- a/crates/oxide_emit/tests/emit_ir_tests.rs +++ b/crates/oxide_emit/tests/emit_ir_tests.rs @@ -409,3 +409,22 @@ fn block_fn_suppressed_by_destructured_param_leaf() { "非抑制块函数应有入口 undefined 写与外层 var 写回" ); } + +/// 面 14:双常量折叠入口结构回归——双字面量算术编译后目标操作码计数为 0、 +/// 常量池含折值,锁死折叠入口不随后续重构丢失。 +#[test] +fn binary_literal_fold_entry() { + // `2 * 3` 乘法编码恒为 f64 → Number(6.0);`1 + 2` 为 int+int → Int(3)。 + // 两操作数均为直接字面量才折(复合子表达式如 `1 + 2 * 3` 的外层加法不折)。 + let ir = emit_ir("var a = 2 * 3; var b = 1 + 2;"); + assert_eq!(count_op(&ir.insts, OpCode::MUL), 0, "双字面量乘法应折叠,MUL 计数为 0"); + assert_eq!(count_op(&ir.insts, OpCode::ADD), 0, "双字面量加法应折叠,ADD 计数为 0"); + assert!( + ir.constants.contains(&Constant::Number(6.0)), + "常量池应含乘法折值 Number(6.0)" + ); + assert!( + ir.constants.contains(&Constant::Int(3)), + "常量池应含加法折值 Int(3)" + ); +} diff --git a/crates/oxide_emit/tests/fold_tests.rs b/crates/oxide_emit/tests/fold_tests.rs new file mode 100644 index 00000000..37c7666b --- /dev/null +++ b/crates/oxide_emit/tests/fold_tests.rs @@ -0,0 +1,145 @@ +//! 双常量折叠的 IR 级结构断言:parse → `emit_program` 为止,钉死折叠命中、 +//! 语义守卫不折与发射确定性。 +//! +//! 命中矩阵断言目标操作码计数为 0 且常量池含折值;守卫矩阵断言目标操作码 +//! 计数为 1(落回运行时路径);确定性断言同源串连续编译八字节码指纹全等。 + +use oxide_bytecode::module::Constant; +use oxide_bytecode::opcode::OpCode; +use oxide_emit::Emitter; +use oxide_ir::inst::Inst; +use oxide_ir::IRFunction; + +/// 夹具:parse + 脚本口径 emit,失败 panic 带源串。 +fn emit_ir(src: &str) -> IRFunction { + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, src).unwrap_or_else(|errs| panic!("parse 失败 {src:?}: {errs:?}")); + Emitter::new() + .emit_program(&program, false, false) + .unwrap_or_else(|e| panic!("emit 失败 {src:?}: {e}")) +} + +fn count_op(insts: &[Inst], op: OpCode) -> usize { + insts.iter().filter(|i| i.op == op).count() +} + +/// 常量池含指定常量(按 `PartialEq` 精确匹配,`Int(3)` 与 `Number(3.0)` 是不同键)。 +fn pool_contains(ir: &IRFunction, c: &Constant) -> bool { + ir.constants.contains(c) +} + +/// 命中矩阵:每行一条源串,断言目标操作码计数为 0 且常量池含折值。 +/// 折值编码逐运算符镜像运行时产物(ADD 保 Int、SUB/MUL/DIV/MOD/EXP 恒 f64、 +/// 位运算 Int、USHR 越 i32::MAX 升 Number、比较/等值 Boolean)。 +#[test] +fn fold_hit_matrix() { + // (源串, 目标操作码, 期望折值) + let cases: &[(&str, OpCode, Constant)] = &[ + ("1 + 2", OpCode::ADD, Constant::Int(3)), + ("1 - 2", OpCode::SUB, Constant::Number(-1.0)), + ("2 * 3", OpCode::MUL, Constant::Number(6.0)), + ("1 / 2", OpCode::DIV, Constant::Number(0.5)), + ("7 % 3", OpCode::MOD, Constant::Number(1.0)), + ("2 ** 10", OpCode::EXP, Constant::Number(1024.0)), + ("5 & 3", OpCode::BIT_AND, Constant::Int(1)), + ("5 | 3", OpCode::BIT_OR, Constant::Int(7)), + ("5 ^ 3", OpCode::BIT_XOR, Constant::Int(6)), + // 移位量模 32:34 模 32 为 2。 + ("1 << 34", OpCode::SHL, Constant::Int(4)), + ("-8 >> 1", OpCode::SHR, Constant::Int(-4)), + // USHR 越 i32::MAX 升 Number。 + ("-1 >>> 0", OpCode::USHR, Constant::Number(4294967295.0)), + ("1 < 2", OpCode::LT, Constant::Boolean(true)), + ("2 > 1", OpCode::GT, Constant::Boolean(true)), + ("1 <= 1", OpCode::LTE, Constant::Boolean(true)), + ("1 >= 2", OpCode::GTE, Constant::Boolean(false)), + // 布尔经 to_number。 + ("true < 2", OpCode::LT, Constant::Boolean(true)), + ("null < 1", OpCode::LT, Constant::Boolean(true)), + // int 与 double 混合表示按数值。 + ("1 === 1.0", OpCode::STRICT_EQ, Constant::Boolean(true)), + ("1 == 1.0", OpCode::EQ, Constant::Boolean(true)), + // 宽松相等 Boolean 重入。 + ("true == 1", OpCode::EQ, Constant::Boolean(true)), + ("null == null", OpCode::EQ, Constant::Boolean(true)), + ("1 !== 2", OpCode::STRICT_NEQ, Constant::Boolean(true)), + // 括号剥壳。 + ("(1) + (2)", OpCode::ADD, Constant::Int(3)), + ]; + for (src, op, folded) in cases { + let ir = emit_ir(src); + assert_eq!(count_op(&ir.insts, *op), 0, "{src:?} 应折叠,目标操作码 {op:?} 计数应为 0"); + assert!(pool_contains(&ir, folded), "{src:?} 常量池应含折值 {folded:?}"); + } +} + +/// 守卫不折矩阵:断言目标操作码计数为 1(落回运行时路径)。 +/// 除零守卫须同时覆盖 Int(0) 与 Number(±0.0);负指数守卫须同时覆盖 Int/Number +/// 负值;BigInt 字面量、字符串字面量、标识符均不折。 +#[test] +fn fold_guard_matrix() { + // (源串, 目标操作码) + let cases: &[(&str, OpCode)] = &[ + // DIV/MOD 除零(Int 0 与 Number ±0.0)。 + ("1 / 0", OpCode::DIV), + ("1 / -0.0", OpCode::DIV), + ("1 % 0", OpCode::MOD), + // EXP 负指数。 + ("2 ** -1", OpCode::EXP), + // BigInt 字面量(BigInt 混合、BigInt-BigInt 独立指令族)。 + ("1n + 2n", OpCode::ADD), + ("1 + 1n", OpCode::ADD), + ("1n < 2n", OpCode::LT), + // 字符串域不折。 + ("\"a\" + \"b\"", OpCode::ADD), + // 标识符不折。 + ("1 + x", OpCode::ADD), + ]; + for (src, op) in cases { + let ir = emit_ir(src); + assert_eq!(count_op(&ir.insts, *op), 1, "{src:?} 守卫命中,目标操作码 {op:?} 应保留 1 条"); + } +} + +/// 字节码指纹:`CompiledModule` 未派生 `Debug`,逐字段展开为确定串(子模块递归)。 +fn bytecode_fingerprint(m: &oxide_bytecode::module::CompiledModule) -> String { + let mut out = format!( + "bc={:?};k={:?};nr={};na={};pb={};bim={:?};uv={:?};cn={};", + m.bytecode, + m.constants, + m.n_registers, + m.n_args, + m.param_base, + m.builtin_reg_map, + m.upvalue_captures, + m.cells_needed + ); + for sub in &m.sub_modules { + out.push_str(&bytecode_fingerprint(sub)); + } + out +} + +/// 确定性:同一源串连续编译八次,IR 与字节码指纹全等(折叠路径纯函数无迭代序依赖)。 +#[test] +fn fold_deterministic() { + let srcs = ["1 + 2 * 3;", "var a = -1 >>> 0; var b = 1 < 2;", "var c = 5 & 3 | 4;"]; + for src in srcs { + let mut fingerprints: Vec<(String, String)> = Vec::new(); + for _ in 0..8 { + let alloc = oxide_parser::Allocator::default(); + let program = + oxide_parser::parse(&alloc, src).unwrap_or_else(|e| panic!("parse 失败 {src:?}: {e:?}")); + let ir = Emitter::new() + .emit_program(&program, false, false) + .unwrap_or_else(|e| panic!("emit 失败 {src:?}: {e}")); + let bytecode = oxide_ir::lower::lower(&ir).unwrap_or_else(|e| panic!("lower 失败 {src:?}: {e}")); + fingerprints.push((format!("{ir:?}"), bytecode_fingerprint(&bytecode))); + } + let first = &fingerprints[0]; + for (i, fp) in fingerprints.iter().enumerate().skip(1) { + assert_eq!(fp.0, first.0, "第 {i} 次编译 IR 指纹漂移:{src:?}"); + assert_eq!(fp.1, first.1, "第 {i} 次编译字节码指纹漂移:{src:?}"); + } + } +} diff --git a/crates/oxide_runtime_api/src/coercion.rs b/crates/oxide_runtime_api/src/coercion.rs index fdb0c579..74b0a3e1 100644 --- a/crates/oxide_runtime_api/src/coercion.rs +++ b/crates/oxide_runtime_api/src/coercion.rs @@ -455,6 +455,52 @@ pub fn to_boolean(val: JsValue) -> bool { } } +/// IsLooselyEqual(x, y) 原始值核心 — ECMA-262 §7.2.15(`==`)的无宿主、 +/// 无异常通道版本,两侧均须为原始值(对象臂不可达,由调用方保证)。 +/// +/// 同类型委托严格相等;null 与 undefined 互等;BigInt/Number/String 两两 +/// 转数值比较;Boolean 操作数先 ToNumber 再重入。 +pub fn abstract_eq_primitives(lhs: JsValue, rhs: JsValue) -> bool { + let tl = lhs.js_type(); + let tr = rhs.js_type(); + // 同类型 → 严格相等。 + if tl == tr { + return strict_equality(lhs, rhs); + } + match (tl, tr) { + // null 与 undefined 互等。 + (JsType::Null, JsType::Undefined) | (JsType::Undefined, JsType::Null) => true, + // Number 混合表示(int vs double):同属 ECMAScript Number,按严格相等。 + (JsType::Int | JsType::Double, JsType::Int | JsType::Double) => strict_equality(lhs, rhs), + // BigInt 与 Number:转 f64 比较(精度内精确;超出精度近似)。 + (JsType::BigInt, JsType::Int | JsType::Double) => { + bigint_to_f64(unsafe { bigint_data(lhs) }) == to_number(rhs) + } + (JsType::Int | JsType::Double, JsType::BigInt) => { + to_number(lhs) == bigint_to_f64(unsafe { bigint_data(rhs) }) + } + // BigInt 与 String:字符串解析为数字后比较(lossy 文本口径,见 to_number)。 + (JsType::BigInt, JsType::String) => { + let r = parse_js_number(&unsafe { string_data(rhs) }); + bigint_to_f64(unsafe { bigint_data(lhs) }) == r + } + (JsType::String, JsType::BigInt) => { + let l = parse_js_number(&unsafe { string_data(lhs) }); + l == bigint_to_f64(unsafe { bigint_data(rhs) }) + } + // Number 与 String:字符串经 ToNumber 后按严格数值比较。 + (JsType::Int | JsType::Double, JsType::String) | (JsType::String, JsType::Int | JsType::Double) => { + strict_double_eq(to_number(lhs), to_number(rhs)) + } + // Boolean 操作数:先 ToNumber 再重入。int(0/1) 轻量编码,省 float + // 装箱与 to_number 链;int 属于 Number,重入后各数值分支全部认。 + (JsType::Bool, _) => abstract_eq_primitives(JsValue::int(lhs.as_bool() as i32), rhs), + (_, JsType::Bool) => abstract_eq_primitives(lhs, JsValue::int(rhs.as_bool() as i32)), + // 其余组合不相等(含 null/undefined 与其它类型)。 + _ => false, + } +} + /// IsLooselyEqual(x, y) — ECMA-262 §7.2.15(`==`)。 /// /// 同类型委托严格相等;null 与 undefined 互等;BigInt/Number/String 两两 @@ -479,49 +525,39 @@ pub fn abstract_eq(lhs: JsValue, rhs: JsValue, host: &mut H) -> Resul return abstract_eq(lhs, JsValue::undefined(), host); } } - // 同类型 → 严格相等。 + // 同类型 → 严格相等(含 Object/Object 指针同一)。 if tl == tr { return Ok(strict_equality(lhs, rhs)); } + // 双侧原始值:委托原始值核心(无宿主、无异常通道)。 + if tl != JsType::Object && tr != JsType::Object { + return Ok(abstract_eq_primitives(lhs, rhs)); + } + // Object 与原始值:ToPrimitive 后重入。to_primitive 必返原始值, + // 重入即收敛到原始值核心,与单次委托等价。 match (tl, tr) { - // null 与 undefined 互等。 - (JsType::Null, JsType::Undefined) | (JsType::Undefined, JsType::Null) => Ok(true), - // Number 混合表示(int vs double):同属 ECMAScript Number,按严格相等。 - (JsType::Int | JsType::Double, JsType::Int | JsType::Double) => Ok(strict_equality(lhs, rhs)), - // BigInt 与 Number:转 f64 比较(精度内精确;超出精度近似)。 - (JsType::BigInt, JsType::Int | JsType::Double) => { - Ok(bigint_to_f64(unsafe { bigint_data(lhs) }) == to_number(rhs)) - } - (JsType::Int | JsType::Double, JsType::BigInt) => { - Ok(to_number(lhs) == bigint_to_f64(unsafe { bigint_data(rhs) })) - } - // BigInt 与 String:字符串解析为数字后比较(lossy 文本口径,见 to_number)。 - (JsType::BigInt, JsType::String) => { - let r = parse_js_number(&unsafe { string_data(rhs) }); - Ok(bigint_to_f64(unsafe { bigint_data(lhs) }) == r) - } - (JsType::String, JsType::BigInt) => { - let l = parse_js_number(&unsafe { string_data(lhs) }); - Ok(l == bigint_to_f64(unsafe { bigint_data(rhs) })) - } - // Number 与 String:字符串经 ToNumber 后按严格数值比较。 - (JsType::Int | JsType::Double, JsType::String) | (JsType::String, JsType::Int | JsType::Double) => { - Ok(strict_double_eq(to_number(lhs), to_number(rhs))) - } - // Boolean 操作数:先 ToNumber 再重入。int(0/1) 轻量编码,省 float - // 装箱与 to_number 链;int 属于 Number,重入后各数值分支全部认。 - (JsType::Bool, _) => abstract_eq(JsValue::int(lhs.as_bool() as i32), rhs, host), - (_, JsType::Bool) => abstract_eq(lhs, JsValue::int(rhs.as_bool() as i32), host), - // Object 与原始值(Number/String/BigInt/Symbol):ToPrimitive 后重入。 - (JsType::Object, JsType::Int | JsType::Double | JsType::String | JsType::BigInt | JsType::Symbol) => { + (JsType::Object, JsType::Int | JsType::Double | JsType::String | JsType::BigInt | JsType::Symbol | JsType::Bool) => { + let rhs = if tr == JsType::Bool { + // Boolean 先 ToNumber(int(0/1) 轻量编码),与原始值核心 + // 的 Boolean 重入口径一致。 + JsValue::int(rhs.as_bool() as i32) + } else { + rhs + }; let prim = to_primitive(lhs, ToPrimitiveHint::Default, host)?; abstract_eq(prim, rhs, host) } - (JsType::Int | JsType::Double | JsType::String | JsType::BigInt | JsType::Symbol, JsType::Object) => { + (JsType::Int | JsType::Double | JsType::String | JsType::BigInt | JsType::Symbol | JsType::Bool, JsType::Object) => { + let lhs = if tl == JsType::Bool { + // 同上。 + JsValue::int(lhs.as_bool() as i32) + } else { + lhs + }; let prim = to_primitive(rhs, ToPrimitiveHint::Default, host)?; abstract_eq(lhs, prim, host) } - // 其余组合不相等(含 null/undefined 与其它类型、Object/Object 已由同类型分支处理)。 + // 其余组合不相等(含 null/undefined 与其它类型;Object/Object 已由同类型分支处理)。 _ => Ok(false), } } diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index dd8b9d10..1a3ac4bf 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -23,7 +23,8 @@ mod native_result; mod runtime_api_log; pub use coercion::{ - abstract_eq, bigint_data, bigint_to_f64, bigint_to_string, format_error_message, js_number_to_string, + abstract_eq, abstract_eq_primitives, bigint_data, bigint_to_f64, bigint_to_string, format_error_message, + js_number_to_string, materialize_string_box, push_to_string, push_units_to, relational_compare, same_value, same_value_zero, strict_equality, string_concat, string_value_eq, to_bigint_full, to_boolean, to_int32, to_integer_or_infinity, to_length, to_number, to_number_full, to_object, to_primitive, to_string, to_string_for_string_constructor,