From 9db28b22d6e0981c9822bf505d80f7d79ff380e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Wed, 17 Jun 2026 09:38:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Map/Set=20GC=20?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E4=B8=8E=20CodeForge=20=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 + crates/oxide_compiler/src/compiler.rs | 2 +- crates/oxide_compiler/src/hash.rs | 216 +++++++++++------- .../tests/structural_hash_tests.rs | 20 ++ crates/oxide_kernel/src/code_forge.rs | 36 ++- crates/oxide_types/src/object.rs | 13 ++ crates/oxide_vm/src/builtins/map.rs | 110 +++++++-- crates/oxide_vm/src/builtins/set.rs | 102 +++++++-- crates/oxide_vm/src/session_arena.rs | 10 + crates/oxide_vm/src/session_gc.rs | 97 +++++++- 10 files changed, 482 insertions(+), 126 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d8faf..6928c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: dtolnay/rust-toolchain@stable with: diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 5ea52fc..3f8e9b3 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -4,7 +4,7 @@ use crate::module::CompiledModule; use crate::opcode::{self, OpCode}; use crate::symbol_table::{Binding, SymbolTable}; -pub use crate::hash::structural_hash; +pub use crate::hash::{compiled_module_hash, structural_hash}; pub use crate::module::Constant; use crate::symbol_table::ScopeKind; pub use oxide_parser::VariableDeclarationKind; diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index 56cda01..9d14dc5 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -1,5 +1,6 @@ use oxide_parser::{ - ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey, SimpleAssignmentTarget, Statement, + BindingPattern, ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey, + SimpleAssignmentTarget, Statement, }; use std::hash::Hash; @@ -24,103 +25,132 @@ macro_rules! hash_match { } pub fn structural_hash(program: &oxide_parser::Program) -> u64 { + hash_program(program, false) +} + +pub fn compiled_module_hash(program: &oxide_parser::Program) -> u64 { + hash_program(program, true) +} + +fn hash_program(program: &oxide_parser::Program, include_binding_names: bool) -> u64 { use std::hash::Hasher; let mut h = rustc_hash::FxHasher::default(); for stmt in &program.body { - hash_statement(stmt, &mut h); + hash_statement(stmt, &mut h, include_binding_names); } h.finish() } -fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { +fn hash_binding_pattern(pattern: &BindingPattern, h: &mut rustc_hash::FxHasher) { + if let BindingPattern::BindingIdentifier(ident) = pattern { + ident.name.as_str().hash(h); + } +} + +fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { hash_match!(HashDomain::Statement, stmt, h, { Statement::ExpressionStatement(es) => { - hash_expression(&es.expression, h); + hash_expression(&es.expression, h, include_binding_names); } Statement::VariableDeclaration(decl) => { std::mem::discriminant(&decl.kind).hash(h); (decl.declarations.len() as u32).hash(h); for d in &decl.declarations { + if include_binding_names { + hash_binding_pattern(&d.id, h); + } if let Some(init) = &d.init { - hash_expression(init, h); + hash_expression(init, h, include_binding_names); } } } Statement::ReturnStatement(ret) => { if let Some(arg) = &ret.argument { - hash_expression(arg, h); + hash_expression(arg, h, include_binding_names); } } Statement::IfStatement(ifs) => { - hash_expression(&ifs.test, h); - hash_statement(&ifs.consequent, h); + hash_expression(&ifs.test, h, include_binding_names); + hash_statement(&ifs.consequent, h, include_binding_names); if let Some(alt) = &ifs.alternate { - hash_statement(alt, h); + hash_statement(alt, h, include_binding_names); } } Statement::WhileStatement(wh) => { - hash_expression(&wh.test, h); - hash_statement(&wh.body, h); + hash_expression(&wh.test, h, include_binding_names); + hash_statement(&wh.body, h, include_binding_names); } Statement::ForStatement(fr) => { if let Some(init) = &fr.init { if let Some(expr) = init.as_expression() { - hash_expression(expr, h); + hash_expression(expr, h, include_binding_names); } else if let ForStatementInit::VariableDeclaration(decl) = init { std::mem::discriminant(&decl.kind).hash(h); (decl.declarations.len() as u32).hash(h); for d in &decl.declarations { + if include_binding_names { + hash_binding_pattern(&d.id, h); + } if let Some(init_expr) = &d.init { - hash_expression(init_expr, h); + hash_expression(init_expr, h, include_binding_names); } } } } if let Some(test) = &fr.test { - hash_expression(test, h); + hash_expression(test, h, include_binding_names); } if let Some(update) = &fr.update { - hash_expression(update, h); + hash_expression(update, h, include_binding_names); } - hash_statement(&fr.body, h); + hash_statement(&fr.body, h, include_binding_names); } Statement::BlockStatement(block) => { for s in &block.body { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } Statement::BreakStatement(_) => {} Statement::ContinueStatement(_) => {} Statement::DoWhileStatement(dw) => { - hash_statement(&dw.body, h); - hash_expression(&dw.test, h); + hash_statement(&dw.body, h, include_binding_names); + hash_expression(&dw.test, h, include_binding_names); } Statement::ForInStatement(fi) => { - hash_expression(&fi.right, h); - hash_statement(&fi.body, h); + hash_expression(&fi.right, h, include_binding_names); + hash_statement(&fi.body, h, include_binding_names); } Statement::SwitchStatement(sw) => { - hash_expression(&sw.discriminant, h); + hash_expression(&sw.discriminant, h, include_binding_names); for case in &sw.cases { if let Some(test) = &case.test { - hash_expression(test, h); + hash_expression(test, h, include_binding_names); } for s in &case.consequent { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } } Statement::FunctionDeclaration(fd) => { - if let Some(id) = &fd.id { + if include_binding_names { + if let Some(id) = &fd.id { + id.name.as_str().hash(h); + } + } else if let Some(id) = &fd.id { id.name.as_str().hash(h); } (fd.params.items.len() as u32).hash(h); + if include_binding_names { + for param in &fd.params.items { + hash_binding_pattern(¶m.pattern, h); + } + } if let Some(body) = &fd.body { for s in &body.statements { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } } @@ -130,32 +160,34 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { } class.super_class.is_some().hash(h); if let Some(super_class) = &class.super_class { - hash_expression(super_class, h); + hash_expression(super_class, h, include_binding_names); } for element in &class.body.body { - hash_class_element(element, h); + hash_class_element(element, h, include_binding_names); } } Statement::ThrowStatement(ts) => { - hash_expression(&ts.argument, h); + hash_expression(&ts.argument, h, include_binding_names); } Statement::TryStatement(ts) => { for s in &ts.block.body { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } if let Some(catch) = &ts.handler { if let Some(param) = &catch.param { - if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { + if include_binding_names { + hash_binding_pattern(¶m.pattern, h); + } else if let oxide_parser::BindingPattern::BindingIdentifier(bi) = ¶m.pattern { bi.name.as_str().hash(h); } } for s in &catch.body.body { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } if let Some(finally) = &ts.finalizer { for s in &finally.body { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } } @@ -163,7 +195,7 @@ fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher) { }); } -fn hash_property_key(key: &PropertyKey, h: &mut rustc_hash::FxHasher) { +fn hash_property_key(key: &PropertyKey, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { hash_match!(HashDomain::PropertyKey, key, h, { PropertyKey::StaticIdentifier(ident) => { ident.name.as_str().hash(h); @@ -181,80 +213,89 @@ fn hash_property_key(key: &PropertyKey, h: &mut rustc_hash::FxHasher) { pi.name.as_str().hash(h); } _ => { - hash_expression(key.to_expression(), h); + hash_expression(key.to_expression(), h, include_binding_names); } }); } -fn hash_class_element(element: &ClassElement, h: &mut rustc_hash::FxHasher) { +fn hash_class_element(element: &ClassElement, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { hash_match!(HashDomain::ClassElement, element, h, { ClassElement::MethodDefinition(method) => { method.r#static.hash(h); method.computed.hash(h); std::mem::discriminant(&method.kind).hash(h); - hash_property_key(&method.key, h); + hash_property_key(&method.key, h, include_binding_names); if method.computed { - hash_property_key(&method.key, h); + hash_property_key(&method.key, h, include_binding_names); } (method.value.params.items.len() as u32).hash(h); + if include_binding_names { + for param in &method.value.params.items { + hash_binding_pattern(¶m.pattern, h); + } + } if let Some(body) = &method.value.body { for stmt in &body.statements { - hash_statement(stmt, h); + hash_statement(stmt, h, include_binding_names); } } } ClassElement::PropertyDefinition(prop) => { prop.r#static.hash(h); prop.computed.hash(h); - hash_property_key(&prop.key, h); + hash_property_key(&prop.key, h, include_binding_names); if let Some(value) = &prop.value { - hash_expression(value, h); + hash_expression(value, h, include_binding_names); } } ClassElement::StaticBlock(block) => { for stmt in &block.body { - hash_statement(stmt, h); + hash_statement(stmt, h, include_binding_names); } } _ => {} }); } -fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { +fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { hash_match!(HashDomain::Expression, expr, h, { Expression::BinaryExpression(bin) => { std::mem::discriminant(&bin.operator).hash(h); - hash_expression(&bin.left, h); - hash_expression(&bin.right, h); + hash_expression(&bin.left, h, include_binding_names); + hash_expression(&bin.right, h, include_binding_names); } Expression::UnaryExpression(un) => { std::mem::discriminant(&un.operator).hash(h); - hash_expression(&un.argument, h); + hash_expression(&un.argument, h, include_binding_names); } Expression::CallExpression(call) => { (call.arguments.len() as u32).hash(h); - hash_expression(&call.callee, h); + hash_expression(&call.callee, h, include_binding_names); for arg in &call.arguments { if let Some(expr) = arg.as_expression() { - hash_expression(expr, h); + hash_expression(expr, h, include_binding_names); } } } Expression::LogicalExpression(log) => { std::mem::discriminant(&log.operator).hash(h); - hash_expression(&log.left, h); - hash_expression(&log.right, h); + hash_expression(&log.left, h, include_binding_names); + hash_expression(&log.right, h, include_binding_names); } Expression::ConditionalExpression(cond) => { - hash_expression(&cond.test, h); - hash_expression(&cond.consequent, h); - hash_expression(&cond.alternate, h); + hash_expression(&cond.test, h, include_binding_names); + hash_expression(&cond.consequent, h, include_binding_names); + hash_expression(&cond.alternate, h, include_binding_names); } Expression::PrivateInExpression(pin) => { pin.left.name.as_str().hash(h); - hash_expression(&pin.right, h); + hash_expression(&pin.right, h, include_binding_names); + } + Expression::Identifier(ident) => { + if include_binding_names { + ident.name.as_str().hash(h); + } } - Expression::Identifier(_) => {} Expression::NumericLiteral(num) => { num.value.to_bits().hash(h); } @@ -267,39 +308,52 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { Expression::AssignmentExpression(assign) => { std::mem::discriminant(&assign.operator).hash(h); if let Some(target) = assign.left.as_simple_assignment_target() { - hash_simple_assignment_target(target, h); + hash_simple_assignment_target(target, h, include_binding_names); } - hash_expression(&assign.right, h); + hash_expression(&assign.right, h, include_binding_names); } Expression::UpdateExpression(update) => { std::mem::discriminant(&update.operator).hash(h); update.prefix.hash(h); - hash_simple_assignment_target(&update.argument, h); + hash_simple_assignment_target(&update.argument, h, include_binding_names); } Expression::TemplateLiteral(tl) => { (tl.quasis.len() as u32).hash(h); for expr in &tl.expressions { - hash_expression(expr, h); + hash_expression(expr, h, include_binding_names); } } Expression::TaggedTemplateExpression(tt) => { - hash_expression(&tt.tag, h); + hash_expression(&tt.tag, h, include_binding_names); (tt.quasi.quasis.len() as u32).hash(h); for expr in &tt.quasi.expressions { - hash_expression(expr, h); + hash_expression(expr, h, include_binding_names); } } Expression::ArrowFunctionExpression(arrow) => { (arrow.params.items.len() as u32).hash(h); + if include_binding_names { + for param in &arrow.params.items { + hash_binding_pattern(¶m.pattern, h); + } + } for s in &arrow.body.statements { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } Expression::FunctionExpression(fe) => { (fe.params.items.len() as u32).hash(h); + if include_binding_names { + if let Some(id) = &fe.id { + id.name.as_str().hash(h); + } + for param in &fe.params.items { + hash_binding_pattern(¶m.pattern, h); + } + } if let Some(body) = &fe.body { for s in &body.statements { - hash_statement(s, h); + hash_statement(s, h, include_binding_names); } } } @@ -309,29 +363,29 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { } class.super_class.is_some().hash(h); if let Some(super_class) = &class.super_class { - hash_expression(super_class, h); + hash_expression(super_class, h, include_binding_names); } for element in &class.body.body { - hash_class_element(element, h); + hash_class_element(element, h, include_binding_names); } } Expression::ObjectExpression(obj) => { (obj.properties.len() as u32).hash(h); for prop in &obj.properties { - hash_object_property_kind(prop, h); + hash_object_property_kind(prop, h, include_binding_names); } } Expression::NewExpression(ne) => { - hash_expression(&ne.callee, h); + hash_expression(&ne.callee, h, include_binding_names); (ne.arguments.len() as u32).hash(h); for arg in &ne.arguments { if let Some(expr) = arg.as_expression() { - hash_expression(expr, h); + hash_expression(expr, h, include_binding_names); } } } Expression::PrivateFieldExpression(member) => { - hash_expression(&member.object, h); + hash_expression(&member.object, h, include_binding_names); member.field.name.as_str().hash(h); } Expression::Super(_) => {} @@ -344,31 +398,37 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { }); } -fn hash_object_property_kind(prop: &ObjectPropertyKind, h: &mut rustc_hash::FxHasher) { +fn hash_object_property_kind(prop: &ObjectPropertyKind, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { hash_match!(HashDomain::ObjectPropertyKind, prop, h, { ObjectPropertyKind::ObjectProperty(p) => { std::mem::discriminant(&p.kind).hash(h); p.method.hash(h); p.computed.hash(h); - hash_property_key(&p.key, h); - hash_expression(&p.value, h); + hash_property_key(&p.key, h, include_binding_names); + hash_expression(&p.value, h, include_binding_names); } ObjectPropertyKind::SpreadProperty(_) => {} }); } -fn hash_simple_assignment_target(target: &SimpleAssignmentTarget, h: &mut rustc_hash::FxHasher) { +fn hash_simple_assignment_target( + target: &SimpleAssignmentTarget, h: &mut rustc_hash::FxHasher, include_binding_names: bool, +) { hash_match!(HashDomain::SimpleAssignmentTarget, target, h, { - SimpleAssignmentTarget::AssignmentTargetIdentifier(_) => {} + SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => { + if include_binding_names { + ident.name.as_str().hash(h); + } + } SimpleAssignmentTarget::StaticMemberExpression(member) => { - hash_expression(&member.object, h); + hash_expression(&member.object, h, include_binding_names); } SimpleAssignmentTarget::ComputedMemberExpression(member) => { - hash_expression(&member.object, h); - hash_expression(&member.expression, h); + hash_expression(&member.object, h, include_binding_names); + hash_expression(&member.expression, h, include_binding_names); } SimpleAssignmentTarget::PrivateFieldExpression(member) => { - hash_expression(&member.object, h); + hash_expression(&member.object, h, include_binding_names); member.field.name.as_str().hash(h); } _ => {} diff --git a/crates/oxide_compiler/tests/structural_hash_tests.rs b/crates/oxide_compiler/tests/structural_hash_tests.rs index f7358e1..50035ff 100644 --- a/crates/oxide_compiler/tests/structural_hash_tests.rs +++ b/crates/oxide_compiler/tests/structural_hash_tests.rs @@ -16,6 +16,12 @@ fn parse_to_hash(source: &str) -> u64 { oxide_compiler::compiler::structural_hash(&program) } +fn parse_to_compiled_hash(source: &str) -> u64 { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + oxide_compiler::compiler::compiled_module_hash(&program) +} + #[test] fn structural_hash_hit() { let forge = CodeForge::new(); @@ -43,6 +49,20 @@ fn structural_hash_identifier_renaming_remains_normalized() { assert_eq!(hash_a, hash_b); } +#[test] +fn compiled_module_hash_distinguishes_same_shape_different_identifier_loads() { + let hash_a = parse_to_compiled_hash("var x = 1; var y = 2; x"); + let hash_b = parse_to_compiled_hash("var x = 1; var y = 2; y"); + assert_ne!(hash_a, hash_b); +} + +#[test] +fn compiled_module_hash_distinguishes_assignment_targets() { + let hash_a = parse_to_compiled_hash("var x = 1; x = 2; x"); + let hash_b = parse_to_compiled_hash("var x = 1; y = 2; x"); + assert_ne!(hash_a, hash_b); +} + #[test] fn structural_hash_different_init_values() { let hash_a = parse_to_hash("var x = 1 + 2;"); diff --git a/crates/oxide_kernel/src/code_forge.rs b/crates/oxide_kernel/src/code_forge.rs index 78eac9b..3aef38d 100644 --- a/crates/oxide_kernel/src/code_forge.rs +++ b/crates/oxide_kernel/src/code_forge.rs @@ -7,7 +7,7 @@ use oxide_compiler::module::CompiledModule; use crate::{kernel_debug, kernel_info}; -pub use oxide_compiler::compiler::structural_hash; +pub use oxide_compiler::compiler::{compiled_module_hash, structural_hash}; /// Shared compiled-module cache. /// @@ -29,16 +29,16 @@ impl CodeForge { pub fn get_or_compile( &self, program: &oxide_parser::Program, compiler: &Compiler, ) -> Result, String> { - let hash = structural_hash(program); + let hash = compiled_module_hash(program); if let Some(module) = self.map.get(&hash) { - kernel_info!("CodeForge hit hash={:#x}", hash); + kernel_info!("CodeForge hit compiled_hash={:#x}", hash); return Ok(Arc::clone(&module)); } let module = compiler.compile(program)?; let module = Arc::new(module); - kernel_debug!("CodeForge miss hash={:#x}", hash); + kernel_debug!("CodeForge miss compiled_hash={:#x}", hash); Ok(Arc::clone(self.map.entry(hash).or_insert(module).value())) } @@ -73,6 +73,17 @@ mod tests { assert_eq!(structural_hash(&p1), structural_hash(&p2)); } + #[test] + fn test_compiled_module_hash_distinguishes_identifier_loads() { + let a1 = Allocator::default(); + let a2 = Allocator::default(); + let p1 = oxide_parser::parse(&a1, "var x = 1; var y = 2; x").expect("parse failed"); + let p2 = oxide_parser::parse(&a2, "var x = 1; var y = 2; y").expect("parse failed"); + + assert_eq!(structural_hash(&p1), structural_hash(&p2)); + assert_ne!(compiled_module_hash(&p1), compiled_module_hash(&p2)); + } + #[test] fn test_structural_hash_different_ops() { let a1 = Allocator::default(); @@ -110,4 +121,21 @@ mod tests { assert_ne!(m1.bytecode, m2.bytecode); } + + #[test] + fn test_cache_miss_same_shape_different_binding_program() { + let forge = CodeForge::new(); + let compiler = Compiler::new(); + + let a1 = Allocator::default(); + let p1 = oxide_parser::parse(&a1, "var x = 1; var y = 2; x").expect("parse failed"); + let a2 = Allocator::default(); + let p2 = oxide_parser::parse(&a2, "var x = 1; var y = 2; y").expect("parse failed"); + + let m1 = forge.get_or_compile(&p1, &compiler).expect("compile p1"); + let m2 = forge.get_or_compile(&p2, &compiler).expect("compile p2"); + + assert!(!Arc::ptr_eq(&m1, &m2)); + assert_ne!(m1.bytecode, m2.bytecode); + } } diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 7e0c1f8..cc88b12 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -183,6 +183,7 @@ impl PropIndex for i32 { /// _pad: u8 /// hash_props: *mut u8 (8 bytes, points to Box>) /// prop_meta: *mut u8 (8 bytes, points to Box>>) +/// native_data: *mut u8 (8 bytes, opaque VM-owned native/exotic payload) /// proto: JsValue (8 bytes) /// generation: u32 (4 bytes + 4 pad) /// native_fn: Option (16 bytes — Option optimization NOT available for @@ -199,6 +200,7 @@ pub struct JsObject { _pad: u8, hash_props: *mut u8, prop_meta: *mut u8, + native_data: *mut u8, proto: JsValue, generation: u32, _pad2: [u8; 4], @@ -264,6 +266,7 @@ impl JsObject { _pad: 0, hash_props: std::ptr::null_mut(), prop_meta: std::ptr::null_mut(), + native_data: std::ptr::null_mut(), proto, generation: 1, _pad2: [0; 4], @@ -284,6 +287,7 @@ impl JsObject { _pad: 0, hash_props: std::ptr::null_mut(), prop_meta: std::ptr::null_mut(), + native_data: std::ptr::null_mut(), proto, generation: 1, _pad2: [0; 4], @@ -344,6 +348,7 @@ impl JsObject { _pad: self._pad, hash_props, prop_meta, + native_data: self.native_data, proto: self.proto, generation: self.generation, _pad2: self._pad2, @@ -363,6 +368,14 @@ impl JsObject { self.prop_meta } + pub fn native_data(&self) -> *mut u8 { + self.native_data + } + + pub fn set_native_data(&mut self, ptr: *mut u8) { + self.native_data = ptr; + } + pub fn rewrite_object_values(&mut self, mut rewrite: F) where F: FnMut(JsValue) -> JsValue, diff --git a/crates/oxide_vm/src/builtins/map.rs b/crates/oxide_vm/src/builtins/map.rs index cee4604..7efa54d 100644 --- a/crates/oxide_vm/src/builtins/map.rs +++ b/crates/oxide_vm/src/builtins/map.rs @@ -6,8 +6,6 @@ use crate::builtins::set::SetKey; use crate::native::NativeResult; use crate::vm::Vm; -const MAP_PROP_INDEX: u8 = 0; - macro_rules! native_try { ($expr:expr) => { match $expr { @@ -17,7 +15,9 @@ macro_rules! native_try { }; } -/// Retrieve the `IndexMap` pointer stored at `MAP_PROP_INDEX` of a Map object. +type MapInner = indexmap::IndexMap; + +/// Retrieve the `IndexMap` pointer stored in a Map object's native-data slot. /// /// # Safety contract maintained by callers /// @@ -27,7 +27,7 @@ macro_rules! native_try { /// is created in `new_map_inner()` and is never freed until the process exits (intentional /// leak — lifetime is tied to the epoch). Only one live `*mut` alias exists at a time /// per Map object because native calls are single-threaded. -fn get_map_inner(vm: &mut Vm, this_val: JsValue) -> Result<*mut indexmap::IndexMap, JsValue> { +fn get_map_inner(vm: &mut Vm, this_val: JsValue) -> Result<*mut MapInner, JsValue> { if !this_val.is_object() { return Err(crate::builtins::error::create_type_error(vm, "called on non-Map object")); } @@ -45,40 +45,108 @@ fn get_map_inner(vm: &mut Vm, this_val: JsValue) -> Result<*mut indexmap::IndexM "Map.prototype method called on incompatible receiver", )); } - let inner_val = map_obj.get_prop_at(MAP_PROP_INDEX); - if !inner_val.is_object() { - return Err(crate::builtins::error::create_type_error(vm, "Map internal state invalid")); - } - // SAFETY: inner_val holds the raw pointer written by `alloc_map` as - // `JsValue::object(Box::into_raw(Box::new(IndexMap::new())) as *mut u8)`. - // The pointer is a valid, heap-allocated `Box>` and is - // never freed during the Map object's lifetime (see module safety contract above). + // SAFETY: native_data holds the raw pointer written by `alloc_map`. + // The pointer is a valid, heap-allocated `Box>`. // Alignment: IndexMap requires at most 8-byte alignment; the global allocator satisfies this. - let inner_ptr = inner_val.as_js_object_ptr() as *mut indexmap::IndexMap; + let inner_ptr = map_obj.native_data() as *mut MapInner; if inner_ptr.is_null() { return Err(crate::builtins::error::create_type_error(vm, "Map internal state invalid")); } Ok(inner_ptr) } -/// Allocate a new `IndexMap` on the global heap. -/// The caller is responsible for storing the pointer in the Map `JsObject` at `MAP_PROP_INDEX` -/// via `JsValue::object(ptr as *mut u8)` and ensuring it is never double-freed. -fn new_map_inner() -> *mut indexmap::IndexMap { - Box::into_raw(Box::new(indexmap::IndexMap::new())) +fn new_map_inner() -> *mut MapInner { + Box::into_raw(Box::new(MapInner::new())) } fn alloc_map(vm: &mut Vm) -> *mut JsObject { let map_proto = vm.session().builtin_world().map_proto.as_ptr() as *mut JsObject; let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(map_proto)); obj.set_map(true); - // SAFETY: new_map_inner() returns a valid Box pointer; stored as a JsValue - // object tag so get_map_inner() can retrieve and cast it back (same provenance). let inner = new_map_inner(); - obj.set_prop_at(MAP_PROP_INDEX, JsValue::object(inner as *mut u8)); + obj.set_native_data(inner as *mut u8); vm.epoch().alloc(obj) } +pub(crate) fn map_native_edges(obj: &JsObject) -> Vec { + if !obj.is_map() { + return Vec::new(); + } + let inner = obj.native_data() as *const MapInner; + if inner.is_null() { + return Vec::new(); + } + unsafe { + (*inner) + .iter() + .flat_map(|(key, value)| [key.0, *value]) + .filter(|value| value.is_object()) + .collect() + } +} + +pub(crate) fn clone_map_native_with_rewrite(src: &JsObject, dst: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + if !src.is_map() { + return; + } + let inner = src.native_data() as *const MapInner; + if inner.is_null() { + dst.set_native_data(std::ptr::null_mut()); + return; + } + let mut cloned = MapInner::new(); + unsafe { + for (key, value) in (*inner).iter() { + let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { *key }; + let new_value = if value.is_object() { rewrite(*value) } else { *value }; + cloned.insert(new_key, new_value); + } + } + dst.set_native_data(Box::into_raw(Box::new(cloned)) as *mut u8); +} + +pub(crate) fn rewrite_map_native(obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + if !obj.is_map() { + return; + } + let inner = obj.native_data() as *mut MapInner; + if inner.is_null() { + return; + } + unsafe { + let mut rewritten = MapInner::with_capacity((*inner).len()); + for (key, value) in (*inner).iter() { + let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { *key }; + let new_value = if value.is_object() { rewrite(*value) } else { *value }; + rewritten.insert(new_key, new_value); + } + *inner = rewritten; + } +} + +pub(crate) fn drop_map_native(obj: &mut JsObject) -> u64 { + if !obj.is_map() { + return 0; + } + let inner = obj.native_data() as *mut MapInner; + if inner.is_null() { + return 0; + } + unsafe { + let boxed = Box::from_raw(inner); + let bytes = std::mem::size_of::() + boxed.capacity() * std::mem::size_of::<(SetKey, JsValue)>(); + drop(boxed); + obj.set_native_data(std::ptr::null_mut()); + bytes as u64 + } +} + pub fn map_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult { let map_obj = alloc_map(vm); NativeResult::Ok(JsValue::from_js_object(map_obj)) diff --git a/crates/oxide_vm/src/builtins/set.rs b/crates/oxide_vm/src/builtins/set.rs index 92c25ee..07e6d90 100644 --- a/crates/oxide_vm/src/builtins/set.rs +++ b/crates/oxide_vm/src/builtins/set.rs @@ -7,8 +7,6 @@ use oxide_types::value::JsValue; use crate::native::NativeResult; use crate::vm::Vm; -const SET_PROP_INDEX: u8 = 0; - macro_rules! native_try { ($expr:expr) => { match $expr { @@ -59,7 +57,9 @@ impl Hash for SetKey { } } -/// Retrieve the `IndexSet` pointer stored at `SET_PROP_INDEX` of a Set object. +type SetInner = indexmap::IndexSet; + +/// Retrieve the `IndexSet` pointer stored in a Set object's native-data slot. /// /// # Safety contract maintained by callers /// @@ -68,7 +68,7 @@ impl Hash for SetKey { /// native builtin is executing. The `Box` is allocated in `new_set_inner()` /// and is never freed during the Set's lifetime. Only one live `*mut` alias exists per /// Set object at a time because native calls are single-threaded. -fn get_set_inner(vm: &mut Vm, this_val: JsValue) -> Result<*mut indexmap::IndexSet, JsValue> { +fn get_set_inner(vm: &mut Vm, this_val: JsValue) -> Result<*mut SetInner, JsValue> { if !this_val.is_object() { return Err(crate::builtins::error::create_type_error(vm, "called on non-Set object")); } @@ -85,40 +85,100 @@ fn get_set_inner(vm: &mut Vm, this_val: JsValue) -> Result<*mut indexmap::IndexS "Set.prototype.add called on incompatible receiver", )); } - let inner_val = set_obj.get_prop_at(SET_PROP_INDEX); - if !inner_val.is_object() { - return Err(crate::builtins::error::create_type_error(vm, "Set internal state invalid")); - } - // SAFETY: inner_val holds the raw pointer written by `alloc_set` as - // `JsValue::object(Box::into_raw(Box::new(IndexSet::new())) as *mut u8)`. - // The pointer is a valid, heap-allocated `Box>` and is never freed - // during the Set object's lifetime (see module safety contract above). + // SAFETY: native_data holds the raw pointer written by `alloc_set`. + // The pointer is a valid, heap-allocated `Box>`. // Alignment: IndexSet requires at most 8-byte alignment; the global allocator satisfies this. - let inner_ptr = inner_val.as_js_object_ptr() as *mut indexmap::IndexSet; + let inner_ptr = set_obj.native_data() as *mut SetInner; if inner_ptr.is_null() { return Err(crate::builtins::error::create_type_error(vm, "Set internal state invalid")); } Ok(inner_ptr) } -/// Allocate a new `IndexSet` on the global heap. -/// The caller is responsible for storing the pointer in the Set `JsObject` at `SET_PROP_INDEX` -/// via `JsValue::object(ptr as *mut u8)` and ensuring it is never double-freed. -fn new_set_inner() -> *mut indexmap::IndexSet { - Box::into_raw(Box::new(indexmap::IndexSet::new())) +fn new_set_inner() -> *mut SetInner { + Box::into_raw(Box::new(SetInner::new())) } fn alloc_set(vm: &mut Vm) -> *mut JsObject { let set_proto = vm.session().builtin_world().set_proto.as_ptr() as *mut JsObject; let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(set_proto)); obj.set_set(true); - // SAFETY: new_set_inner() returns a valid Box pointer; stored as a JsValue - // object tag so get_set_inner() can retrieve and cast it back (same provenance). let inner = new_set_inner(); - obj.set_prop_at(SET_PROP_INDEX, JsValue::object(inner as *mut u8)); + obj.set_native_data(inner as *mut u8); vm.epoch().alloc(obj) } +pub(crate) fn set_native_edges(obj: &JsObject) -> Vec { + if !obj.is_set() { + return Vec::new(); + } + let inner = obj.native_data() as *const SetInner; + if inner.is_null() { + return Vec::new(); + } + unsafe { (*inner).iter().map(|key| key.0).filter(|value| value.is_object()).collect() } +} + +pub(crate) fn clone_set_native_with_rewrite(src: &JsObject, dst: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + if !src.is_set() { + return; + } + let inner = src.native_data() as *const SetInner; + if inner.is_null() { + dst.set_native_data(std::ptr::null_mut()); + return; + } + let mut cloned = SetInner::new(); + unsafe { + for key in (*inner).iter() { + let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { *key }; + cloned.insert(new_key); + } + } + dst.set_native_data(Box::into_raw(Box::new(cloned)) as *mut u8); +} + +pub(crate) fn rewrite_set_native(obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + if !obj.is_set() { + return; + } + let inner = obj.native_data() as *mut SetInner; + if inner.is_null() { + return; + } + unsafe { + let mut rewritten = SetInner::with_capacity((*inner).len()); + for key in (*inner).iter() { + let new_key = if key.0.is_object() { SetKey(rewrite(key.0)) } else { *key }; + rewritten.insert(new_key); + } + *inner = rewritten; + } +} + +pub(crate) fn drop_set_native(obj: &mut JsObject) -> u64 { + if !obj.is_set() { + return 0; + } + let inner = obj.native_data() as *mut SetInner; + if inner.is_null() { + return 0; + } + unsafe { + let boxed = Box::from_raw(inner); + let bytes = std::mem::size_of::() + boxed.capacity() * std::mem::size_of::(); + drop(boxed); + obj.set_native_data(std::ptr::null_mut()); + bytes as u64 + } +} + pub fn set_constructor(vm: &mut Vm, _args: &[u8]) -> NativeResult { let set_obj = alloc_set(vm); NativeResult::Ok(JsValue::from_js_object(set_obj)) diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 62ddcd7..b2d08e8 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use oxide_types::object::JsObject; use oxide_types::value::JsValue; +use crate::builtins::{map, set}; use crate::vm::Vm; impl Vm { @@ -41,6 +42,15 @@ impl Vm { let dst_ref = unsafe { &mut *dst }; dst_ref.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, forwarding)); + if src_ref.is_map() { + map::clone_map_native_with_rewrite(src_ref, dst_ref, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); + } else if src_ref.is_set() { + set::clone_set_native_with_rewrite(src_ref, dst_ref, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); + } dst } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index b992ac7..757f227 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -5,6 +5,7 @@ use std::time::Instant; use oxide_types::object::{JsObject, PropMetaEntry}; use oxide_types::value::JsValue; +use crate::builtins::{map, set}; use crate::vm::Vm; pub struct SessionGc { @@ -80,6 +81,12 @@ impl SessionGc { if obj.home_object().is_object() { edges.push(obj.home_object()); } + if obj.is_map() { + edges.extend(map::map_native_edges(obj)); + } + if obj.is_set() { + edges.extend(set::set_native_edges(obj)); + } edges } @@ -161,6 +168,9 @@ impl SessionGc { std::mem::drop(vec); } + freed_bytes += map::drop_map_native(obj); + freed_bytes += set::drop_set_native(obj); + freed_bytes } } @@ -186,8 +196,15 @@ impl SessionGc { let is_live = unsafe { (*old_ptr).is_gc_marked() }; if is_live { survivors += 1; - let clone = unsafe { &*old_ptr }.clone_for_session_epoch(); + let old_ref = unsafe { &*old_ptr }; + let clone = old_ref.clone_for_session_epoch(); let new_ptr = new_arena.alloc(clone) as *mut JsObject; + let new_ref = unsafe { &mut *new_ptr }; + if old_ref.is_map() { + map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value); + } else if old_ref.is_set() { + set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value); + } forwarding.insert(old_ptr, new_ptr); freed_bytes += Self::drop_session_object_heap_data(old_ptr); } else { @@ -208,6 +225,11 @@ impl SessionGc { } value }); + if obj.is_map() { + map::rewrite_map_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); + } else if obj.is_set() { + set::rewrite_set_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); + } } rewrite_vm_roots(vm, &forwarding); @@ -353,6 +375,8 @@ mod tests { use oxide_types::object::JsObject; use super::*; + use crate::builtins::{map, set}; + use crate::native::NativeResult; use crate::vm::{CallFrame, FrameContinuation}; fn plain_object(vm: &mut Vm) -> *mut JsObject { @@ -480,6 +504,14 @@ mod tests { Vm::with_kernel_core(core) } + fn native_ok(result: NativeResult) -> JsValue { + match result { + NativeResult::Ok(value) => value, + NativeResult::Err(err) => panic!("native error: {err}"), + NativeResult::TailCall { .. } => panic!("unexpected native bytecode call"), + } + } + #[test] fn reset_maybe_collect_collects_after_threshold() { let mut vm = vm_with_low_threshold(); @@ -536,4 +568,67 @@ mod tests { assert!(!std::ptr::eq(new_value.as_js_object_ptr(), old_ptr)); assert_eq!(unsafe { (*new_value.as_js_object_ptr()).get_prop_at(0) }, JsValue::int(42)); } + + #[test] + fn map_native_storage_is_not_a_normal_object_edge() { + let mut vm = Vm::new(); + let map_value = native_ok(map::map_constructor(&mut vm, &[])); + let map_obj = unsafe { &*map_value.as_js_object_ptr() }; + let native_ptr = map_obj.native_data() as *mut JsObject; + + assert!(map_obj.hash_props_vec().is_none()); + assert!(!map_obj.native_data().is_null()); + assert!(!SessionGc::new() + .object_edges(map_obj) + .iter() + .any(|edge| std::ptr::eq(edge.as_js_object_ptr(), native_ptr))); + } + + #[test] + fn session_gc_traces_map_object_key_and_value() { + let mut vm = vm_with_low_threshold(); + let map_value = native_ok(map::map_constructor(&mut vm, &[])); + let key = JsValue::from_js_object(plain_object(&mut vm)); + let value = JsValue::from_js_object(plain_object(&mut vm)); + vm.regs[0] = map_value; + vm.regs[1] = key; + vm.regs[2] = value; + native_ok(map::map_set(&mut vm, &[0, 1, 2])); + + let map_session = vm.promote_object(map_value.as_js_object_ptr()); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(map_session); + let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + gc.collect(&mut vm); + vm.session_gc = gc; + + let live_map = unsafe { &*vm.regs[0].as_js_object_ptr() }; + let edges = map::map_native_edges(live_map); + assert_eq!(edges.len(), 2); + assert!(edges.iter().all(|value| vm.is_session_ptr(value.as_js_object_ptr()))); + assert_eq!(vm.session_object_ptrs.len(), 3); + } + + #[test] + fn session_gc_traces_set_object_key() { + let mut vm = vm_with_low_threshold(); + let set_value = native_ok(set::set_constructor(&mut vm, &[])); + let key = JsValue::from_js_object(plain_object(&mut vm)); + vm.regs[0] = set_value; + vm.regs[1] = key; + native_ok(set::set_add(&mut vm, &[0, 1])); + + let set_session = vm.promote_object(set_value.as_js_object_ptr()); + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(set_session); + let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + gc.collect(&mut vm); + vm.session_gc = gc; + + let live_set = unsafe { &*vm.regs[0].as_js_object_ptr() }; + let edges = set::set_native_edges(live_set); + assert_eq!(edges.len(), 1); + assert!(vm.is_session_ptr(edges[0].as_js_object_ptr())); + assert_eq!(vm.session_object_ptrs.len(), 2); + } }