Files
OxideJS-New/crates/oxide_vm/tests/member_write_ic_tests.rs
T

244 lines
9.1 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.
//! member 复合写(obj.x += 1 / obj.x++ 等)接入 IC 写命中的行为测试。
//!
//! 覆盖三组语义:①写侧命中(depth==0 槽直写,读写对称);②写新属性走
//! CreateDataProperty 快路径(shape 转换,命中无法覆盖);③继承属性 shadow /
//! 原型 setter / 只读继承属性(写侧 depth==0 限制与原型链检查兜底,防原型污染)。
//!
//! 计数断言基于单次 run 内 miss/hit 对比(run() 每次冷启动,IC 学习不跨 run)。
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
/// 编译并单次运行,返回 (结果, hits, misses)。
fn run_once(source: &str) -> (String, u64, u64) {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let result = vm.run(&module).expect("run");
// 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。
let r = vm.lookup_str(result).unwrap_or_else(|| format!("{result}"));
(r, vm.ic_hit_count(), vm.ic_miss_count())
}
/// 单态 member 复合写:写侧命中直写(读侧命中不计数,pre-existing 口径——
/// 计数仅来自写侧与独立 IC_GET/IC_SET 分发站点)。
#[test]
fn member_compound_monomorphic_write_hits() {
let (r, hits, misses) = run_once(
r#"var t = { x: 0 };
for (var i = 0; i < 10000; i++) { t.x += 1; }
t.x"#,
);
assert_eq!(r, "10000");
assert_eq!(misses, 1, "仅末尾 `t.x` IC_GET 站点学习 miss,循环写侧零 miss");
assert_eq!(hits, 10000, "循环 10000 次写侧全命中(实际 {hits})");
}
/// 4 shape 轮换 member 复合写:写侧复用读侧学习的 4 槽条目,循环内零写 miss。
#[test]
fn member_compound_poly4_read_write_hits() {
let (r, hits, misses) = run_once(
r#"var objs = [{x:0}, {x:0,y:1}, {x:0,z:1}, {x:0,y:1,z:2}];
for (var i = 0; i < 10000; i++) {
var t = objs[i % 4];
t.x += 1;
}
objs[0].x + objs[1].x + objs[2].x + objs[3].x"#,
);
assert_eq!(r, "10000", "四对象 x 各增 2500 次");
assert_eq!(misses, 4, "末尾 4 个 `objs[i].x` IC_GET 站点各学习 1 次");
assert_eq!(hits, 10000, "循环写侧 10000 全命中(实际 {hits})");
}
/// 继承属性 shadow(核心正确性):写侧只接受 depth==0 槽,原型不被污染。
#[test]
fn inherited_prop_shadows_receiver_not_proto() {
let (r, _hits, _misses) = run_once(
r#"var p = { x: 1 };
var o = Object.create(p);
o.x += 1;
o.x + ":" + p.x"#,
);
assert_eq!(r, "2:1", "o 获得 own x=2,p.x 保持 1(未直写原型)");
}
/// IC_SET 独立赋值路径的继承 shadow:`Object.create(p).x = 5` 同样不写原型。
#[test]
fn ic_set_inherited_shadow_receiver_only() {
let (r, _hits, _misses) = run_once(
r#"var p = { x: 1 };
var o = Object.create(p);
o.x = 5;
p.x"#,
);
assert_eq!(r, "1", "p.x 保持 1,o 得到 own x=5");
}
/// 原型 accessor setter:IC_SET 与 member 复合写都触发 setter,接收者不建新槽。
#[test]
fn proto_setter_called_both_write_paths() {
let (r, _hits, _misses) = run_once(
r#"var calls = 0;
var p = { set x(v) { calls++; } };
var o = Object.create(p);
o.x = 5;
o.x += 2;
calls + ":" + Object.keys(o).length"#,
);
assert_eq!(r, "2:0", "setter 被调 2 次(IC_SET 与 member 各 1),o 无 own 属性");
}
/// 原型只读 data:两条写路径都按 ordinary_set 抛 TypeError。
#[test]
fn readonly_inherited_data_raises() {
let (r, _hits, _misses) = run_once(
r#"var p = {};
Object.defineProperty(p, 'x', { value: 1, writable: false });
var o = Object.create(p);
var msg = '';
try { o.x = 5; } catch (e) { msg = e.message; }
try { o.x += 1; } catch (e) { msg += '|' + e.message; }
msg"#,
);
assert_eq!(
r, "cannot assign to read-only property|cannot assign to read-only property",
"两条路径都拒绝写只读继承属性"
);
}
/// 写新属性(shape 转换):快路径建 shape + 写回,循环内同 shape 写全命中。
#[test]
fn write_new_prop_then_hit() {
let (r, hits, misses) = run_once(
r#"var o = {};
o.y = 0;
for (var i = 0; i < 10000; i++) { o.y += 1; }
o.y"#,
);
assert_eq!(r, "10000");
assert_eq!(misses, 2, "`o.y = 0` 站点 1 次 + 末尾 `o.y` IC_GET 站点 1 次");
assert_eq!(hits, 10000, "compound 写侧 10000 全命中(实际 {hits})");
}
/// ic_cache.js 同款模式(批量建对象 + 分批写 y/z 新属性):快路径建 shape 正确。
#[test]
fn ic_cache_style_new_prop_writes() {
let (r, _hits, _misses) = run_once(
r#"var objs = [];
for (var i = 0; i < 100; i++) {
objs.push({ x: i });
if (i % 3 === 0) objs[i].y = i;
if (i % 5 === 0) objs[i].z = i;
}
var yok = 0, zok = 0;
for (var i = 0; i < 100; i++) {
if (i % 3 === 0 && objs[i].y === i) yok++;
if (i % 5 === 0 && objs[i].z === i) zok++;
}
yok + ":" + zok"#,
);
assert_eq!(r, "34:20", "y/z 全部写入成功(含 i%15==0 的 y+z 同对象场景)");
}
/// has_prop_meta 对象 member 写走 ordinary_set(不缓存,accessor 语义保留)。
#[test]
fn meta_object_member_write_uses_ordinary_set() {
let (r, _hits, _misses) = run_once(
r#"var store = 1;
var o = { get a() { return store; }, set a(v) { store = v; } };
o.a += 5;
store"#,
);
assert_eq!(r, "6", "accessor 对象走 setter,store 更新为 6");
}
/// 命中率口径:member 写 hit/miss 计入后 ic_hit_rate 反映写侧全量命中。
#[test]
fn member_write_counts_in_ic_hit_rate() {
let allocator = Allocator::default();
let program = oxide_parser::parse(
&allocator,
r#"var t = { x: 0 };
for (var i = 0; i < 1000; i++) { t.x += 1; }
t.x"#,
)
.expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let result = vm.run(&module).expect("run");
let r = vm.lookup_str(result).unwrap_or_else(|| format!("{result}"));
assert_eq!(r, "1000");
// 循环写侧 1000 命中 + 末尾 IC_GET 1 miss → 命中率 ~0.999(写侧计入后趋近真实)。
assert!(vm.ic_hit_rate() > 0.99, "member 写侧命中计入命中率(实际 {:.4})", vm.ic_hit_rate());
}
/// `__proto__` 赋值 + 继承成员复合写:原型链建立正常,shadow 语义不变(回归锚定)。
#[test]
fn proto_assignment_then_compound_shadow() {
let (r, _hits, _misses) = run_once(
r#"var p = { x: 1 };
var o = {};
o.__proto__ = p;
o.x += 1;
o.x + ":" + p.x"#,
);
assert_eq!(r, "2:1", "__proto__ 赋值建链后复合写 shadow 到 o,p 不被污染");
}
/// 数组 length 复合写分流 ordinary_set(ArraySetLength):`arr.length += 1` 更新逻辑
/// 长度而非建影子槽,后续 push / 索引写读正常(P1-1 回归锚定)。
#[test]
fn array_length_compound_write_uses_array_set_length() {
let (r, _hits, _misses) = run_once(
r#"var arr = [1, 2, 3];
arr.length += 1;
var l1 = arr.length;
arr.push(5);
arr[1] = 9;
l1 + ":" + arr.length + ":" + arr.join(",")"#,
);
assert_eq!(r, "4:5:1,9,3,,5", "length 复合写后 l1=4,push 后 length=5,元素区正确");
}
/// 数组 length 复合写后再直接赋值 `arr.length = N`:两写路径均走 ArraySetLength,
/// 元素区按新长度伸缩,不残留影子槽(P1-1 双路径对称回归锚定)。
#[test]
fn array_length_compound_then_direct_assign() {
let (r, _hits, _misses) = run_once(
r#"var arr = [1, 2, 3];
arr.length += 1;
arr.length = 5;
var l = arr.length;
arr[4] = 7;
l + ":" + arr.join(",")"#,
);
assert_eq!(r, "5:1,2,3,,7", "length=5 后逻辑长度与元素区同步伸缩");
}
/// 数组写新命名属性(IC_SET 快路径建 shape 槽):命名槽在元素区之后,元素写入不错位。
#[test]
fn array_named_prop_create_then_element_write() {
let (r, _hits, _misses) = run_once(
r#"var arr = [1];
arr.foo = 1;
arr[0] = 9;
arr.foo"#,
);
assert_eq!(r, "1", "元素区写入后命名属性 foo 不错位(槽在元素区之后)");
}
/// 数组命名属性 member 复合写命中 shadow 槽 + 元素增长后读回正确(P2-1)。
#[test]
fn array_named_prop_compound_hit_after_element_growth() {
let (r, _hits, _misses) = run_once(
r#"var arr = [1];
arr.foo = 2;
arr.foo += 3;
arr[0] = 9;
arr.foo"#,
);
assert_eq!(r, "5", "member 复合写命中 shadow 槽(2+3=5),元素增长后仍不错位");
}