Files
OxideJS-New/crates/oxide_emit/tests/emit_ir_tests.rs
T

539 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 脚本顶层形态的 IR 级结构断言:parse → `emit_program` 为止,钉死现状 emit 行为。
//!
//! 结构断言顺序无关:只表达 存在 / 计数 / 名字集 / 键名集。GDI 序言对组与 var 入口
//! MAKE_CELL 组的发射序已按名排序固定,但断言仍只取键名集,不依赖寄存器号
//! 与 `n_registers` 绝对值;发射序确定性由 `emission_order_stable_across_compiles`
//! 单独锁定。
use std::collections::HashMap;
use std::collections::HashSet;
use oxide_bytecode::opcode::OpCode;
use oxide_emit::{Constant, Emitter};
use oxide_ir::inst::Inst;
use oxide_ir::operand::Operand;
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}"))
}
/// 字节码指纹:`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
}
/// 同进程连续编译 src 八次,逐次比对 IR 与字节码指纹,断言全等。
fn assert_emission_stable(src: &str, is_eval_script: bool) {
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, is_eval_script)
.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:?}");
}
}
/// 发射序确定性:同进程多次编译,GDI 序言、函数入口 MAKE_CELL 的指令序及
/// 常量池/内置槽映射/字节码逐位不随 HashSet 随机迭代序漂移。
#[test]
fn emission_order_stable_across_compiles() {
// 多个顶层 var 驱动 GDI 序言;函数内多个被捕获 var 驱动函数入口 MAKE_CELL。
assert_emission_stable(
"var b=1,a=2,d=3,c=4; function f(){ var z=1,y=2; return function(){ return z+y+a; }; } f();",
false,
);
// eval 顶层函数声明撞多个不可写全局内置名时,抛错消息名选择按源序稳定。
assert_emission_stable("function Infinity(){} function NaN(){}", true);
}
fn count_op(insts: &[Inst], op: OpCode) -> usize {
insts.iter().filter(|i| i.op == op).count()
}
fn tree_count(ir: &IRFunction, op: OpCode) -> usize {
let mut n = count_op(&ir.insts, op);
for sub in &ir.nested {
n += tree_count(sub, op);
}
n
}
fn pool_strings(ir: &IRFunction) -> HashSet<String> {
ir.constants
.iter()
.filter_map(|c| if let Constant::String(s) = c { Some(s.clone()) } else { None })
.collect()
}
/// 寄存器 → 其被 LOAD_CONST 装入的常量(vreg 号单调不复用,映射唯一)。
fn loaded_consts(ir: &IRFunction) -> HashMap<u32, &Constant> {
let mut m: HashMap<u32, &Constant> = HashMap::new();
for i in &ir.insts {
if i.op == OpCode::LOAD_CONST {
if let (Operand::Reg(r), Operand::Const(c)) = (&i.rd, &i.a) {
m.insert(*r, &ir.constants[*c as usize]);
}
}
}
m
}
/// 指定 opcode 的 b 槽键寄存器回溯其 LOAD_CONST 装入的池字符串名集。
/// 用于 GDI 序言对组(`DEFINE_GLOBAL_PROP_IF_ABSENT`)与顶层 A 侧全局写
/// (`DEFINE_GLOBAL_PROP`)的键名集断言(对序逐进程轮换,只按名集断言)。
fn key_names_of(ir: &IRFunction, op: OpCode) -> HashSet<String> {
let loads = loaded_consts(ir);
let mut keys = HashSet::new();
for i in &ir.insts {
if i.op == op {
if let Operand::Reg(r) = &i.b {
if let Some(Constant::String(s)) = loads.get(r) {
keys.insert(s.clone());
}
}
}
}
keys
}
fn gdi_key_names(ir: &IRFunction) -> HashSet<String> {
key_names_of(ir, OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT)
}
fn define_global_prop_keys(ir: &IRFunction) -> HashSet<String> {
key_names_of(ir, OpCode::DEFINE_GLOBAL_PROP)
}
fn class_flags_scan(f: &IRFunction, base: &mut bool, derived: &mut bool, home: &mut bool) {
if f.is_class_constructor && !f.is_derived_constructor {
*base = true;
}
if f.is_class_constructor && f.is_derived_constructor {
*derived = true;
}
if f.needs_home_object {
*home = true;
}
for n in &f.nested {
class_flags_scan(n, base, derived, home);
}
}
/// 面 1:GDI 序言只覆盖顶层 var 名,lexical(let/const)不进序言。
#[test]
fn gdi_prologue_covers_var_not_lexical() {
let ir = emit_ir("var a = 1; var b = 2; let c = 3;");
assert_eq!(
gdi_key_names(&ir),
["a", "b"].map(String::from).into_iter().collect(),
"GDI 序言键名集应恰为顶层 var 名"
);
}
/// 面 2:顶层函数声明的 GDI 检查、全局绑定与闭包物化形态。
#[test]
fn top_level_function_declaration() {
let ir = emit_ir("function f(n) { return n + 1; }");
assert_eq!(count_op(&ir.insts, OpCode::CAN_DECLARE_GLOBAL_FUNC), 1);
assert_eq!(count_op(&ir.insts, OpCode::DEFINE_GLOBAL_FUNC_BIND), 1);
assert_eq!(count_op(&ir.insts, OpCode::CREATE_CLOSURE), 1);
assert_eq!(ir.nested.len(), 1);
assert!(ir.is_top_level);
let f = &ir.nested[0];
assert_eq!(f.function_name.as_deref(), Some("f"));
assert_eq!(f.function_length, 1);
assert!(!f.is_top_level);
}
/// 面 3:块级函数入口物化——每块函数全流恰 1 个 CREATE_CLOSURE(声明点不重编),
/// 外层 var 写回存在;三常量名(NaN)被守卫:无 GDI 对、无 A 侧全局写。
#[test]
fn block_fn_entry_materialized_once() {
let src = "{ function g() { return 2; } var y = g(); }\n\
label: { function h() { return 3; } }\n\
for (var i = 0; i < 1; i++) { function f() { return i; } }\n\
label3: { function NaN() { return 0; } }";
let ir = emit_ir(src);
assert_eq!(tree_count(&ir, OpCode::CREATE_CLOSURE), 4, "块函数数 = 闭包物化数");
assert_eq!(
gdi_key_names(&ir),
["f", "g", "h", "i", "y"].map(String::from).into_iter().collect(),
"GDI 键名集应含外层 var 与块函数泄漏名,不含三常量名 NaN"
);
assert_eq!(
define_global_prop_keys(&ir),
["f", "g", "h", "i"].map(String::from).into_iter().collect(),
"A 侧全局写键名集不含 NaN(不可写内置守卫)"
);
assert!(count_op(&ir.insts, OpCode::STORE_VAR) >= 1, "外层 var 写回 STORE_VAR 应存在");
}
/// 面 4:三级闭包链的 cell 分配与 upvalue 捕获(cell 下标按名排序分配)。
#[test]
fn closure_captures_cell_and_upvalue() {
let src = "function f() { var x = 1; var s = 0; return function g() { x = x + 1; \
return function h() { s = s + x; return x; }; }; }";
let ir = emit_ir(src);
let f = &ir.nested[0];
assert!(f.cells_needed >= 1);
assert!(count_op(&f.insts, OpCode::MAKE_CELL) >= 1);
let g = &f.nested[0];
let g_cells: HashMap<&str, u8> = g.upvalue_captures.iter().map(|c| (c.name.as_str(), c.cell_idx)).collect();
let mut expected: HashMap<&str, u8> = HashMap::new();
expected.insert("s", 0);
expected.insert("x", 1);
assert_eq!(g_cells, expected, "g 的捕获应按名排序分配 cell 下标");
assert!(count_op(&g.insts, OpCode::LOAD_UPVALUE) >= 1);
assert!(count_op(&g.insts, OpCode::STORE_UPVALUE) >= 1);
let h = &g.nested[0];
let h_names: HashSet<&str> = h.upvalue_captures.iter().map(|c| c.name.as_str()).collect();
let mut expected_names: HashSet<&str> = HashSet::new();
expected_names.insert("s");
expected_names.insert("x");
assert_eq!(h_names, expected_names);
assert!(
h.upvalue_captures.iter().all(|c| c.parent_uv_idx.is_some()),
"链式捕获应经父闭包 upvalue 数组取 cell"
);
}
/// 面 13:直接子函数(深度 0)形参默认值对父级绑定的直接引用须纳入父捕获集
/// (父建 cell、子建 upvalue),否则逃逸闭包缺参调用经 LOAD_VAR 读父帧死寄存器取陈旧值。
#[test]
fn nested_fn_default_ref_captures_parent_binding() {
let src = "function f() { var x = 1; var g = function(a = x) { return a; }; return g; }";
let ir = emit_ir(src);
let f = &ir.nested[0];
assert!(f.cells_needed >= 1, "父函数应为默认值引用的 x 建 cell");
assert!(count_op(&f.insts, OpCode::MAKE_CELL) >= 1, "父函数入口应发 MAKE_CELL");
let g = &f.nested[0];
let g_names: HashSet<&str> = g.upvalue_captures.iter().map(|c| c.name.as_str()).collect();
assert!(g_names.contains("x"), "子函数形参默认值引用的 x 应成为子 upvalue");
}
/// 面 5:对象 rest 展开与数组 holes 的现状形态:REST_OBJECT 的 ext 键是排除键
/// (显式绑定的键名);解构赋值经 for-of 迭代面(FOR_OF_INIT)逐元素绑定,
/// 空洞元素被消费但不绑定;常量池含 Undefined。
#[test]
fn destructured_rest_and_holes() {
let src = "var {a, ...r} = {a: 1, b: 2}; var [x, , z] = [1, 2, 3]; \
var p = [[\"k\", 1]]; for (var [k, v] of p) {}";
let ir = emit_ir(src);
let rest = ir
.insts
.iter()
.find(|i| i.op == OpCode::REST_OBJECT)
.expect("REST_OBJECT 应存在");
assert_eq!(rest.ext.len(), 1);
assert_eq!(
ir.constants[rest.ext[0] as usize],
Constant::String("a".to_string()),
"REST_OBJECT 排除键应为显式绑定的键名"
);
assert!(count_op(&ir.insts, OpCode::FOR_OF_INIT) >= 1);
assert!(ir.constants.iter().any(|c| matches!(c, Constant::Undefined)), "常量池应含 Undefined");
}
/// 面 6:模板字面量的 TEMPLATE_STR 面与 quasis 池键(键经 string_forge 编码,
/// 纯 ASCII 键按值还原即原形态)。全字面量模板已折单常量(见字符串域折叠
/// 测试),此处用非字面量内插表达式保持 TEMPLATE_STR 路径。
#[test]
fn template_literal_pool_keys() {
let src = "\"use strict\";\nvar x = 1;\nconst t = `a${x}b`;\nconst tag = (s) => s[0];\ntag`x${2}y`";
let ir = emit_ir(src);
assert!(count_op(&ir.insts, OpCode::TEMPLATE_STR) >= 1);
let pool = pool_strings(&ir);
assert!(pool.contains("a") && pool.contains("b"), "quasis 池键按值应含 a/b: {pool:?}");
}
/// 面 7:类构造器/派生标志、home object 与私有名指令面。
#[test]
fn class_nested_flags() {
let src = "class B { m() { return 1; } } class A extends B { #p = 2; static s = 3; \
m() { return this.#p; } }";
let ir = emit_ir(src);
let (mut base, mut derived, mut home) = (false, false, false);
class_flags_scan(&ir, &mut base, &mut derived, &mut home);
assert!(base, "基类构造器应标 is_class_constructor");
assert!(derived, "extends 构造器应标 is_derived_constructor");
assert!(home, "原型方法应标 needs_home_object");
assert!(tree_count(&ir, OpCode::INIT_PRIVATE) >= 1, "私有字段初始化面应存在");
assert!(tree_count(&ir, OpCode::GET_PRIVATE) >= 1, "私有成员读面应存在");
}
/// 面 9:对象字面量方法挂 home object(模块标 needs_home_object 且树内存在
/// SET_HOME_OBJECT),计算型 super 成员发 SUPER_GET_PROP 且 this 装载先于键表达式。
#[test]
fn object_method_home_object_and_computed_super() {
let src = "var o = { m() { return super.x; }, g() { return super[k]; } };";
let ir = emit_ir(src);
let (mut _base, mut _derived, mut home) = (false, false, false);
class_flags_scan(&ir, &mut _base, &mut _derived, &mut home);
assert!(home, "对象方法应标 needs_home_object");
assert!(tree_count(&ir, OpCode::SET_HOME_OBJECT) >= 2, "对象方法应挂 SET_HOME_OBJECT");
assert!(tree_count(&ir, OpCode::SUPER_GET_PROP) >= 2, "super 读应发 SUPER_GET_PROP");
// 计算型 super 成员:this 装载(LOAD_VAR This)先于键表达式求值(规范序)。
let m_body = ir
.nested
.iter()
.find(|f| f.function_name.as_deref() == Some("g"))
.expect("方法 g 应存在");
let this_pos = m_body
.insts
.iter()
.position(|i| i.op == OpCode::LOAD_VAR && matches!(i.a, Operand::This))
.expect("应存在 LOAD_VAR This");
let super_pos = m_body
.insts
.iter()
.position(|i| i.op == OpCode::SUPER_GET_PROP)
.expect("应存在 SUPER_GET_PROP");
assert!(this_pos < super_pos, "this 装载应先于 SUPER_GET_PROP");
}
/// 面 8:严格模式标志在函数体内与脚本顶层的继承口径。
#[test]
fn strict_flag_inherited() {
let ir = emit_ir("function f() { \"use strict\"; } function g() { return 1; }");
let by_name = |n: &str| -> &IRFunction {
ir.nested
.iter()
.find(|f| f.function_name.as_deref() == Some(n))
.unwrap_or_else(|| panic!("nested 缺 {n}"))
};
assert!(by_name("f").is_strict, "函数体内 use strict 应置位");
assert!(!by_name("g").is_strict, "无 directive 函数不应继承严格");
let top = emit_ir("\"use strict\"; var x = 1;");
assert!(top.is_strict, "脚本顶层 use strict 应置位");
}
/// 面 9:未捕获 `var` 名在函数入口补发 undefined 定义(预声明只登记槽位,
/// 声明点对已绑定名跳过 undefined 写,入口缺定义会使首次写入前读取取到残留)。
#[test]
fn var_entry_init_emits_undefined_store() {
let ir = emit_ir("function f() { var x; return typeof x; }");
let f = &ir.nested[0];
assert_eq!(count_op(&f.insts, OpCode::STORE_VAR), 1, "未捕获 var 应在函数入口恰写一次 undefined");
}
/// 面 10:入口补写的过滤面——形参、`arguments`、捕获名不触发补写,
/// 多个未捕获 var 名各自补一条。
#[test]
fn var_entry_init_filters_params_arguments_captured() {
let param = emit_ir("function f(a) { var x; return typeof a + typeof x; }");
assert_eq!(count_op(&param.nested[0].insts, OpCode::STORE_VAR), 1, "形参名不补写,仅未捕获 var 补写");
let arguments = emit_ir("function f() { var x; return arguments.length + typeof x; }");
assert_eq!(
count_op(&arguments.nested[0].insts, OpCode::STORE_VAR),
1,
"arguments 名不补写,未捕获 var 补写"
);
let captured = emit_ir("function f() { var x; return function g() { return typeof x; }; }");
assert_eq!(
count_op(&captured.nested[0].insts, OpCode::STORE_VAR),
0,
"捕获名由入口 MAKE_CELL(undefined) 实例化,不再补 STORE_VAR"
);
let multi = emit_ir("function f() { var a, b; return typeof a + typeof b; }");
assert_eq!(count_op(&multi.nested[0].insts, OpCode::STORE_VAR), 2, "多 var 名各自补一条入口定义");
}
/// 面 11:source_encoded 口径下正则源池键为 marker 形态(与静态口径的明文键不同形)。
#[test]
fn source_encoded_regex_pool_key() {
let src = "var r = /\\ud800/g;";
let encode_ir = {
let alloc = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&alloc, src).unwrap();
Emitter::new()
.with_source_encoded(true)
.emit_program(&program, false, false)
.unwrap()
};
let static_ir = emit_ir(src);
let enc = pool_strings(&encode_ir);
assert!(enc.contains("\u{FFFD}d800"), "编码源池键应为 FFFD+hex marker 形态: {enc:?}");
assert!(!enc.contains("\\ud800"));
assert!(pool_strings(&static_ir).contains("\\ud800"), "静态源池键为明文转义文本");
}
/// 面 12:块函数抑制集含解构形参叶名——块函数名撞叶名时不建外层 var 绑定、
/// 声明点不写回(STORE_VAR 少一条写回);非抑制名块函数有入口 undefined 写
/// 与声明点写回。
#[test]
fn block_fn_suppressed_by_destructured_param_leaf() {
// 撞解构叶名:叶名绑定(1)+ 块函数物化自写(1)= 2,无外层 var 写回。
let suppressed = emit_ir("function f({x}) { { function x() { return 1; } } return 2; }");
let f = &suppressed.nested[0];
assert_eq!(count_op(&f.insts, OpCode::STORE_VAR), 2, "撞解构叶名的块函数被抑制:无外层 var 写回");
// 非抑制对照:入口 undefined 写(1)+ 块函数物化自写(1)+ 外层 var 写回(1)= 3。
let control = emit_ir("function f() { { function y() { return 1; } } return 2; }");
let fc = &control.nested[0];
assert_eq!(
count_op(&fc.insts, OpCode::STORE_VAR),
3,
"非抑制块函数应有入口 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)");
}
/// 面 15:字符串域链折叠命中矩阵——全操作数为可折叠字面量且至少一个字符串
/// 字面量时,Addition 链折单字符串常量:ADD/CONCAT_N 计数为 0、池含折值键。
#[test]
fn string_chain_fold_hit() {
// 纯字符串链:池含 "ab"。
let ir = emit_ir("var s = \"a\" + \"b\";");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0, "全字面量字符串链应折叠,ADD 计数为 0");
assert_eq!(count_op(&ir.insts, OpCode::CONCAT_N), 0, "全字面量字符串链应折叠,CONCAT_N 计数为 0");
assert!(pool_strings(&ir).contains("ab"), "池应含折值 ab");
// 字符串加数字加布尔加 null:池含 "a1truenull"。
let ir = emit_ir("var s = \"a\" + 1 + true + null;");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0);
assert_eq!(count_op(&ir.insts, OpCode::CONCAT_N), 0);
assert!(pool_strings(&ir).contains("a1truenull"), "池应含折值 a1truenull");
// 字符串加 BigInt:池含 "x10"。
let ir = emit_ir("var s = \"x\" + 10n;");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0);
assert!(pool_strings(&ir).contains("x10"), "池应含折值 x10");
// 字符串加非整数字面量(Number 臂走 js_number_to_string):池含 "a1.5"。
let ir = emit_ir("var s = \"a\" + 1.5;");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0);
assert!(pool_strings(&ir).contains("a1.5"), "池应含折值 a1.5");
// 字符串加 -0(is_int_literal 为真走 Int 臂):池含 "a0"。
let ir = emit_ir("var s = \"a\" + -0;");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0);
assert!(pool_strings(&ir).contains("a0"), "池应含折值 a0");
// 孤立 surrogate 字符串段:池键为 FFFD+hex4 marker 形态加 "a"。
let ir = emit_ir("var s = \"\\ud800\" + \"a\";");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0);
let pool = pool_strings(&ir);
assert!(pool.contains("\u{FFFD}d800a"), "池键应为 FFFD+hex4 marker 形态加 a: {pool:?}");
// 超平面字符段:经 encode_key 良配对形态入池。
let ir = emit_ir("var s = \"\\u{1F600}\" + \"a\";");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 0);
assert!(pool_strings(&ir).contains("\u{1F600}a"), "池应含超平面字符良配对形态加 a");
}
/// 面 16:字符串域链不折矩阵——含标识符操作数或全数字链不落字符串域,
/// 既有 CONCAT_N/ADD 路径计数不变、池不含折值键。
#[test]
fn string_chain_fold_miss() {
// 标识符操作数(两操作数形态):不折,落回左结合 ADD。
let ir = emit_ir("var x = 1; var s = x + \"a\";");
assert_eq!(count_op(&ir.insts, OpCode::ADD), 1, "含标识符的链应落回 ADD");
assert_eq!(count_op(&ir.insts, OpCode::CONCAT_N), 0);
assert!(!pool_strings(&ir).contains("xa"), "含标识符的链不应折出字符串键");
// 全数字链:数值域守卫排除,不折出字符串键,落回 CONCAT_N。
let ir = emit_ir("var s = 1 + 2 + 3;");
assert_eq!(count_op(&ir.insts, OpCode::CONCAT_N), 1, "全数字链应落回 CONCAT_N");
assert!(!pool_strings(&ir).contains("123"), "全数字链不应折出字符串键 123");
// 字符串夹标识符(三操作数形态):不折,落回 CONCAT_N。
let ir = emit_ir("var x = 1; var s = \"a\" + x + \"b\";");
assert_eq!(count_op(&ir.insts, OpCode::CONCAT_N), 1, "含标识符的链应落回 CONCAT_N");
assert!(!pool_strings(&ir).contains("axb"), "含标识符的链不应折出字符串键");
}
/// 面 17:模板字面量折叠命中矩阵——全内插表达式为可折叠字面量且全部 cooked
/// 为 Some 时折单字符串常量:TEMPLATE_STR 计数为 0、池含折值键。
#[test]
fn template_fold_hit() {
let ir = emit_ir("var t = `a${1}b`;");
assert_eq!(count_op(&ir.insts, OpCode::TEMPLATE_STR), 0, "全字面量模板应折叠,TEMPLATE_STR 计数为 0");
assert!(pool_strings(&ir).contains("a1b"), "池应含折值 a1b");
// 内插 BigInt:池含 "x10y"。
let ir = emit_ir("var t = `x${10n}y`;");
assert_eq!(count_op(&ir.insts, OpCode::TEMPLATE_STR), 0);
assert!(pool_strings(&ir).contains("x10y"), "池应含折值 x10y");
// 无内插表达式的单 quasi 模板:折单串。
let ir = emit_ir("var t = `hello`;");
assert_eq!(count_op(&ir.insts, OpCode::TEMPLATE_STR), 0);
assert!(pool_strings(&ir).contains("hello"), "池应含折值 hello");
// 孤立 surrogate quasi:池键为 marker 形态。
let ir = emit_ir("var t = `\\ud800${1}`;");
assert_eq!(count_op(&ir.insts, OpCode::TEMPLATE_STR), 0);
let pool = pool_strings(&ir);
assert!(pool.contains("\u{FFFD}d8001"), "池键应为 FFFD+hex4 marker 形态加 1: {pool:?}");
}
/// 面 18:模板字面量不折矩阵——内插表达式非字面量时落回 TEMPLATE_STR;
/// 带标签模板走 GET_TEMPLATE_OBJECT 加 CALL 的独立路径,不经折叠。
#[test]
fn template_fold_miss() {
// 表达式非字面量:TEMPLATE_STR 计数 1,池不含折值键。
let ir = emit_ir("var x = 1; var t = `a${x}b`;");
assert_eq!(count_op(&ir.insts, OpCode::TEMPLATE_STR), 1, "非字面量内插应落回 TEMPLATE_STR");
assert!(!pool_strings(&ir).contains("axb"), "不折时池不应含折值键");
// 带标签模板:走 GET_TEMPLATE_OBJECT 加 CALL,不折。
let ir = emit_ir("var tag = (s) => s[0]; var t = tag`a${1}`;");
assert_eq!(count_op(&ir.insts, OpCode::TEMPLATE_STR), 0, "带标签模板不发 TEMPLATE_STR");
assert_eq!(count_op(&ir.insts, OpCode::GET_TEMPLATE_OBJECT), 1, "带标签模板应发 GET_TEMPLATE_OBJECT");
assert_eq!(count_op(&ir.insts, OpCode::CALL), 1, "带标签模板应发 CALL");
}
/// 面 19:字符串域折叠发射确定性——折叠路径纯函数无迭代序依赖,链折叠与
/// 模板折叠源串同进程连续编译指纹全等。
#[test]
fn string_fold_emission_stable() {
assert_emission_stable("var s = \"a\" + 1 + true + null;", false);
assert_emission_stable("var t = `a${1}b`;", false);
}