diff --git a/crates/luars/Cargo.toml b/crates/luars/Cargo.toml index b873da2f..876c9c46 100644 --- a/crates/luars/Cargo.toml +++ b/crates/luars/Cargo.toml @@ -14,7 +14,7 @@ categories = ["development-tools"] default = [] serde = ["dep:serde", "dep:serde_json"] sandbox = [] -shared-string = [] +shared-proto = [] [dependencies] # local diff --git a/crates/luars/src/compiler/expr_parser.rs b/crates/luars/src/compiler/expr_parser.rs index df29e17f..2f2336fc 100644 --- a/crates/luars/src/compiler/expr_parser.rs +++ b/crates/luars/src/compiler/expr_parser.rs @@ -983,7 +983,8 @@ pub fn body(fs: &mut FuncState, v: &mut ExpDesc, is_method: bool) -> Result<(), // lparser.c:1005: Add child proto to parent (addprototype) let proto_idx = fs.chunk.child_protos.len(); - fs.chunk.child_protos.push(std::rc::Rc::new(child_chunk)); + let child_proto = fs.vm.create_proto(child_chunk).unwrap(); + fs.chunk.child_protos.push(child_proto); // lparser.c:722-726: Generate CLOSURE instruction (codeclosure) // static void codeclosure (LexState *ls, expdesc *v) { diff --git a/crates/luars/src/gc/gc_kind.rs b/crates/luars/src/gc/gc_kind.rs index 63a4f89d..5bb3ab5f 100644 --- a/crates/luars/src/gc/gc_kind.rs +++ b/crates/luars/src/gc/gc_kind.rs @@ -9,4 +9,5 @@ pub enum GcObjectKind { Upvalue = 5, Thread = 6, Userdata = 7, + Proto = 8, } diff --git a/crates/luars/src/gc/gc_object.rs b/crates/luars/src/gc/gc_object.rs index 2c504db6..d5f424fd 100644 --- a/crates/luars/src/gc/gc_object.rs +++ b/crates/luars/src/gc/gc_object.rs @@ -1,5 +1,5 @@ use crate::{ - GcObjectKind, LuaFunction, LuaTable, LuaValue, + Chunk, GcObjectKind, LuaFunction, LuaTable, LuaValue, lua_value::{CClosureFunction, LuaString, LuaUpvalue, LuaUserdata, RClosureFunction}, lua_vm::LuaState, }; @@ -20,6 +20,7 @@ pub const WHITE0BIT: u8 = 3; // Object is white (type 0) pub const WHITE1BIT: u8 = 4; // Object is white (type 1) pub const BLACKBIT: u8 = 5; // Object is black pub const FINALIZEDBIT: u8 = 6; // Object has been marked for finalization +pub const SHAREDBIT: u8 = 7; // Object is shared across VMs and never collected // Bit masks pub const WHITEBITS: u8 = (1 << WHITE0BIT) | (1 << WHITE1BIT); @@ -180,6 +181,11 @@ impl GcHeader { (self.marked() & (1 << FINALIZEDBIT)) != 0 } + #[inline(always)] + pub fn is_shared(&self) -> bool { + (self.marked() & (1 << SHAREDBIT)) != 0 + } + #[inline(always)] pub fn set_finalized(&mut self) { self.set_marked_bits(self.marked() | (1 << FINALIZEDBIT)); @@ -190,6 +196,11 @@ impl GcHeader { self.set_marked_bits(self.marked() & !(1 << FINALIZEDBIT)); } + #[inline(always)] + pub fn make_shared(&mut self) { + self.set_marked_bits(self.marked() | (1 << SHAREDBIT)); + } + // ============ Color Transitions ============ #[inline(always)] @@ -227,6 +238,9 @@ impl GcHeader { other_white == 0 || other_white == 1, "other_white must be 0 or 1" ); + if self.is_shared() { + return false; + } (self.marked() & (1 << (WHITE0BIT + other_white))) != 0 } @@ -290,6 +304,15 @@ pub struct Gc { pub data: T, } +impl std::fmt::Debug for Gc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Gc") + .field("header", &self.header) + .field("data", &self.data) + .finish() + } +} + impl Gc { pub fn new(data: T, current_white: u8, size: u32) -> Self { let mut header = GcHeader::with_white(current_white); @@ -312,6 +335,7 @@ pub type GcRClosure = Gc; pub type GcUpvalue = Gc; pub type GcThread = Gc; pub type GcUserdata = Gc; +pub type GcProto = Gc; #[derive(Debug)] pub struct GcPtr { @@ -407,6 +431,7 @@ pub type CClosurePtr = GcPtr; pub type RClosurePtr = GcPtr; pub type UserdataPtr = GcPtr; pub type ThreadPtr = GcPtr; +pub type ProtoPtr = GcPtr; /// Compressed GcObjectPtr — tagged pointer in a single `u64` (8 bytes, was 16). /// @@ -452,6 +477,7 @@ impl GcObjectPtr { const TAG_UPVALUE: u64 = 5; const TAG_THREAD: u64 = 6; const TAG_USERDATA: u64 = 7; + const TAG_PROTO: u64 = 8; #[inline(always)] fn new_tagged(ptr: u64, tag: u64) -> Self { debug_assert!( @@ -565,6 +591,12 @@ impl GcObjectPtr { UserdataPtr::from_raw(self.raw_ptr()) } + #[inline(always)] + pub fn as_proto_ptr(&self) -> ProtoPtr { + debug_assert!(self.tag() == Self::TAG_PROTO as u8); + ProtoPtr::from_raw(self.raw_ptr()) + } + // ============ Pattern matching helpers (for code that still uses if-let) ============ #[inline(always)] @@ -606,6 +638,11 @@ impl GcObjectPtr { pub fn is_userdata(&self) -> bool { self.tag() == Self::TAG_USERDATA as u8 } + + #[inline(always)] + pub fn is_proto(&self) -> bool { + self.tag() == Self::TAG_PROTO as u8 + } } impl From for GcObjectPtr { @@ -664,6 +701,13 @@ impl From for GcObjectPtr { } } +impl From for GcObjectPtr { + #[inline(always)] + fn from(ptr: ProtoPtr) -> Self { + Self::new_tagged(ptr.as_u64(), Self::TAG_PROTO) + } +} + // ============ GC-managed Objects ============ pub enum GcObjectOwner { String(Box), @@ -674,6 +718,7 @@ pub enum GcObjectOwner { Userdata(Box), CClosure(Box), RClosure(Box), + Proto(Box), } impl GcObjectOwner { @@ -694,7 +739,7 @@ impl GcObjectOwner { base + array_bytes + hash_bytes } GcObjectOwner::Function(f) => { - f.data.chunk().proto_data_size as usize + std::mem::size_of_val(f.data.upvalues()) + std::mem::size_of::() + std::mem::size_of_val(f.data.upvalues()) } GcObjectOwner::CClosure(c) => { std::mem::size_of::() @@ -707,6 +752,9 @@ impl GcObjectOwner { GcObjectOwner::Upvalue(_) => 64, // fixed estimate GcObjectOwner::Thread(t) => std::mem::size_of::() + t.data.stack.len() * 16, GcObjectOwner::Userdata(_) => std::mem::size_of::(), + GcObjectOwner::Proto(p) => { + std::mem::size_of::() + p.data.proto_data_size as usize + } } } @@ -726,6 +774,7 @@ impl GcObjectOwner { GcObjectOwner::Upvalue(u) => &u.header, GcObjectOwner::Thread(t) => &t.header, GcObjectOwner::Userdata(u) => &u.header, + GcObjectOwner::Proto(p) => &p.header, }) as _ } @@ -739,6 +788,7 @@ impl GcObjectOwner { GcObjectOwner::Upvalue(u) => &mut u.header, GcObjectOwner::Thread(t) => &mut t.header, GcObjectOwner::Userdata(u) => &mut u.header, + GcObjectOwner::Proto(p) => &mut p.header, }) as _ } @@ -800,6 +850,13 @@ impl GcObjectOwner { } } + pub fn as_proto_ptr(&self) -> Option { + match self { + GcObjectOwner::Proto(p) => Some(ProtoPtr::new(p.as_ref() as *const GcProto)), + _ => None, + } + } + pub fn as_gc_ptr(&self) -> GcObjectPtr { match self { GcObjectOwner::String(s) => { @@ -826,6 +883,9 @@ impl GcObjectOwner { GcObjectOwner::RClosure(r) => { GcObjectPtr::from(RClosurePtr::new(r.as_ref() as *const GcRClosure)) } + GcObjectOwner::Proto(p) => { + GcObjectPtr::from(ProtoPtr::new(p.as_ref() as *const GcProto)) + } } } @@ -878,6 +938,13 @@ impl GcObjectOwner { } } + pub fn as_proto_mut(&mut self) -> Option<&mut Chunk> { + match self { + GcObjectOwner::Proto(p) => Some(&mut p.data), + _ => None, + } + } + pub fn size_of_data(&self) -> usize { self.header().size as usize } diff --git a/crates/luars/src/gc/mod.rs b/crates/luars/src/gc/mod.rs index ea92957b..7a78d44b 100644 --- a/crates/luars/src/gc/mod.rs +++ b/crates/luars/src/gc/mod.rs @@ -43,7 +43,7 @@ use std::collections::HashSet; use crate::{ LuaResult, LuaTable, - lua_value::{Chunk, LuaValue}, + lua_value::LuaValue, lua_vm::{LuaError, LuaState, SafeOption, TmKind}, }; pub use gc_kind::*; @@ -422,6 +422,14 @@ impl GC { l.remove_dead_string(str_ptr); } + fn release_or_detach_object(obj: GcObjectOwner) { + if obj.header().is_shared() { + std::mem::forget(obj); + } else { + drop(obj); + } + } + /// Change to incremental mode (like minor2inc in Lua 5.5) /// /// Port of Lua 5.5 lgc.c minor2inc: @@ -1711,22 +1719,22 @@ impl GC { self.mark_object(l, gc_ptr); } - /// Mark all constants in a chunk and its nested chunks (like Lua 5.5's traverseproto) - fn mark_chunk_constants(&mut self, l: &mut LuaState, chunk: &Chunk) -> usize { - // Mark all constants in this chunk + fn traverse_proto(&mut self, l: &mut LuaState, proto_ptr: ProtoPtr) -> usize { + let gc_proto = proto_ptr.as_mut_ref(); + gc_proto.header.make_black(); + + let chunk = &gc_proto.data; for constant in &chunk.constants { if let Some(gc_ptr) = constant.as_gc_ptr() { self.mark_object(l, gc_ptr); } } - let mut count = chunk.constants.len(); - // Recursively mark constants in child protos (nested functions) - for child_chunk in &chunk.child_protos { - count += self.mark_chunk_constants(l, child_chunk); + for child_proto in &chunk.child_protos { + self.mark_object(l, (*child_proto).into()); } - count + 1 + chunk.constants.len() + chunk.child_protos.len() } // ============ Weak Table Traversal Functions (Port of Lua 5.5) ============ @@ -2009,8 +2017,8 @@ impl GC { self.mark_object(l, (*upval_ptr).into()); } - // Mark all constants in the chunk and nested chunks (like Lua 5.5's traverseproto) - count += self.mark_chunk_constants(l, gc_func.data.chunk()); + self.mark_object(l, gc_func.data.proto().into()); + count += 1; count // Estimate of work done } @@ -2232,6 +2240,8 @@ impl GC { self.traverse_cclosure(l, gc_ptr.as_cclosure_ptr()) } else if gc_ptr.is_rclosure() { self.traverse_rclosure(l, gc_ptr.as_rclosure_ptr()) + } else if gc_ptr.is_proto() { + self.traverse_proto(l, gc_ptr.as_proto_ptr()) } else if gc_ptr.is_userdata() { // Userdata: mark the userdata itself and its metatable if any let ud_ptr = gc_ptr.as_userdata_ptr(); @@ -3390,6 +3400,38 @@ impl GC { ); } } + GcObjectOwner::Proto(p) => { + for (i, constant) in p.data.constants.iter().enumerate() { + check_value( + constant, + &container_kind, + container_ptr, + container_age, + container_marked, + &format!("proto_constant[{}]", i), + ); + } + for (i, child_proto) in p.data.child_protos.iter().enumerate() { + let child_gc: GcObjectPtr = (*child_proto).into(); + if is_dead_ptr(child_gc) { + let ref_header = child_gc.header().unwrap(); + panic!( + "GC INVARIANT VIOLATION: alive {:?} at {:#x} (age={}, marked=0x{:02X}) \ + references DEAD Proto at {:#x} (age={}, marked=0x{:02X}) via child_proto[{}]. \ + current_white={}", + container_kind, + container_ptr, + container_age, + container_marked, + child_gc.header().map(|h| h as *const _ as u64).unwrap_or(0), + ref_header.age(), + ref_header.marked(), + i, + self.current_white, + ); + } + } + } // Strings have no GC references GcObjectOwner::String(_) => {} } @@ -3553,6 +3595,7 @@ impl GC { GcObjectOwner::Thread(b) => b.as_ref() as *const _ as u64, GcObjectOwner::Upvalue(b) => b.as_ref() as *const _ as u64, GcObjectOwner::Userdata(b) => b.as_ref() as *const _ as u64, + GcObjectOwner::Proto(b) => b.as_ref() as *const _ as u64, }; if let Some((thread_name, slot, top, thread_marked)) = stack_ptrs.get(&raw_ptr) { let kind = gc_owner.as_gc_ptr().kind(); @@ -4251,6 +4294,53 @@ impl GC { } } +#[cfg(feature = "shared-proto")] +pub fn share_proto(proto_ptr: ProtoPtr) -> usize { + fn mark_proto(proto_ptr: ProtoPtr) -> usize { + let (child_protos, shared_strings) = { + let gc_proto = proto_ptr.as_mut_ref(); + if gc_proto.header.is_shared() { + return 0; + } + + gc_proto.header.make_shared(); + gc_proto.header.make_black(); + gc_proto.header.make_old(); + + let shared_strings = gc_proto.data.share_constant_strings(); + (gc_proto.data.child_protos.clone(), shared_strings) + }; + + let mut shared_count = 1 + shared_strings; + for child_proto in child_protos { + shared_count += mark_proto(child_proto); + } + shared_count + } + + mark_proto(proto_ptr) +} + +impl Drop for GC { + fn drop(&mut self) { + for obj in self.allgc.take_all() { + Self::release_or_detach_object(obj); + } + for obj in self.survival.take_all() { + Self::release_or_detach_object(obj); + } + for obj in self.old1.take_all() { + Self::release_or_detach_object(obj); + } + for obj in self.old.take_all() { + Self::release_or_detach_object(obj); + } + for obj in self.fixed_list.take_all() { + Self::release_or_detach_object(obj); + } + } +} + /// Result of a GC step #[derive(Debug)] enum StepResult { diff --git a/crates/luars/src/gc/object_allocator.rs b/crates/luars/src/gc/object_allocator.rs index 41f49d80..1a2edf20 100644 --- a/crates/luars/src/gc/object_allocator.rs +++ b/crates/luars/src/gc/object_allocator.rs @@ -15,10 +15,9 @@ use crate::lua_value::{ }; use crate::lua_vm::{CFunction, LuaState}; use crate::{ - GC, GcCClosure, GcFunction, GcObjectOwner, GcRClosure, GcTable, GcThread, GcUpvalue, - GcUserdata, LuaFunction, LuaResult, LuaTable, LuaValue, StringPtr, UpvaluePtr, + GC, GcCClosure, GcFunction, GcObjectOwner, GcProto, GcRClosure, GcTable, GcThread, GcUpvalue, + GcUserdata, LuaFunction, LuaResult, LuaTable, LuaValue, ProtoPtr, StringPtr, UpvaluePtr, }; -use std::rc::Rc; pub type CreateResult = LuaResult; @@ -167,6 +166,16 @@ impl ObjectAllocator { // ==================== Function Operations ==================== + #[inline(always)] + pub fn create_proto(&mut self, gc: &mut GC, chunk: Chunk) -> LuaResult { + let current_white = gc.current_white; + let size = std::mem::size_of::() as u32 + chunk.proto_data_size; + let gc_proto = GcObjectOwner::Proto(Box::new(GcProto::new(chunk, current_white, size))); + let ptr = gc_proto.as_proto_ptr().unwrap(); + gc.trace_object(gc_proto)?; + Ok(ptr) + } + /// Create a Lua function (closure with bytecode chunk) /// Now caches upvalue pointers for direct access /// @@ -174,12 +183,12 @@ impl ObjectAllocator { pub fn create_function( &mut self, gc: &mut GC, - chunk: Rc, + chunk: ProtoPtr, upvalue_store: UpvalueStore, ) -> CreateResult { let current_white = gc.current_white; let upval_size = upvalue_store.len() * std::mem::size_of::(); - let size = chunk.proto_data_size + upval_size as u32; + let size = std::mem::size_of::() as u32 + upval_size as u32; let gc_func = GcObjectOwner::Function(Box::new(GcFunction::new( LuaFunction::new(chunk, upvalue_store), diff --git a/crates/luars/src/gc/string_interner.rs b/crates/luars/src/gc/string_interner.rs index 22d59bd9..c12c766b 100644 --- a/crates/luars/src/gc/string_interner.rs +++ b/crates/luars/src/gc/string_interner.rs @@ -1,60 +1,34 @@ use ahash::RandomState; -#[cfg(feature = "shared-string")] -use std::collections::HashMap; -#[cfg(feature = "shared-string")] -use std::sync::{Arc, LazyLock, Mutex, Weak}; use crate::lua_value::{InlineShortString, LuaStrRepr, LuaString}; use crate::lua_vm::lua_limits::LUAI_MAXSHORTLEN; use crate::{CreateResult, GC, GcObjectOwner, GcString, LuaValue, StringPtr}; -#[cfg(feature = "shared-string")] -static SHARED_SHORT_STRING_POOL: LazyLock> = - LazyLock::new(|| Mutex::new(SharedShortStringPool::default())); - -#[cfg(feature = "shared-string")] -struct SharedShortStringPool { - entries: HashMap, Weak<[u8]>>, - cleanup_threshold: usize, -} - -#[cfg(feature = "shared-string")] -impl Default for SharedShortStringPool { - fn default() -> Self { - Self { - entries: HashMap::new(), - cleanup_threshold: 1024, - } - } -} - -#[cfg(feature = "shared-string")] -impl SharedShortStringPool { - fn intern(&mut self, bytes: &[u8]) -> Arc<[u8]> { - if let Some(existing) = self.entries.get(bytes) - && let Some(shared) = existing.upgrade() - { - return shared; - } - - let shared = Arc::<[u8]>::from(bytes); - self.entries - .insert(shared.as_ref().to_vec(), Arc::downgrade(&shared)); +const STRING_HASH_SEED_1: u64 = 0x243f_6a88_85a3_08d3; +const STRING_HASH_SEED_2: u64 = 0x1319_8a2e_0370_7344; +const STRING_HASH_SEED_3: u64 = 0xa409_3822_299f_31d0; +const STRING_HASH_SEED_4: u64 = 0x082e_fa98_ec4e_6c89; + +#[cfg(feature = "shared-proto")] +pub fn share_lua_value(value: &mut LuaValue) -> bool { + match value.as_string_ptr() { + Some(ptr) => { + let gc_string = ptr.as_mut_ref(); + if gc_string.header.is_shared() { + return false; + } - self.maybe_cleanup(); + gc_string.header.make_shared(); + gc_string.header.make_black(); + gc_string.header.make_old(); - shared - } + if value.is_short_string() { + gc_string.data.ensure_short_id(); + } - fn maybe_cleanup(&mut self) { - if self.entries.len() < self.cleanup_threshold { - return; + true } - - self.entries.retain(|_, weak| weak.strong_count() > 0); - - let grown = self.entries.len().saturating_mul(2).max(1024); - self.cleanup_threshold = grown.next_power_of_two(); + None => false, } } @@ -78,10 +52,10 @@ impl StringSlot { /// Open-addressed intern table for short strings. pub struct StringInterner { slots: Vec, + hasher: RandomState, /// Number of interned strings nuse: usize, ndead: usize, - hashbuilder: RandomState, } impl Default for StringInterner { @@ -99,9 +73,14 @@ impl StringInterner { pub fn new() -> Self { Self { slots: vec![StringSlot::Empty; Self::INITIAL_SIZE], + hasher: RandomState::with_seeds( + STRING_HASH_SEED_1, + STRING_HASH_SEED_2, + STRING_HASH_SEED_3, + STRING_HASH_SEED_4, + ), nuse: 0, ndead: 0, - hashbuilder: RandomState::new(), } } @@ -131,19 +110,7 @@ impl StringInterner { return LuaStrRepr::Smol(InlineShortString::new(bytes)); } - #[cfg(feature = "shared-string")] - { - let shared = SHARED_SHORT_STRING_POOL - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .intern(bytes); - return LuaStrRepr::Shared(shared); - } - - #[cfg(not(feature = "shared-string"))] - { - LuaStrRepr::Heap(Box::<[u8]>::from(bytes)) - } + LuaStrRepr::Heap(Box::<[u8]>::from(bytes)) } #[inline(always)] @@ -290,7 +257,7 @@ impl StringInterner { #[inline(always)] fn hash_bytes(&self, s: &[u8]) -> u64 { - self.hashbuilder.hash_one(s) + self.hasher.hash_one(s) } pub fn remove_dead_intern(&mut self, ptr: StringPtr) { @@ -363,82 +330,18 @@ impl StringInterner { } } -#[cfg(all(test, feature = "shared-string"))] +#[cfg(test)] mod tests { use super::StringInterner; - use std::sync::Arc; + #[cfg(feature = "shared-proto")] + use super::share_lua_value; use crate::GC; use crate::lua_value::LuaStrRepr; use crate::lua_vm::SafeOption; #[test] - fn shared_short_strings_reuse_arc_storage_across_interners() { - let key = "0123456789abcdefghijklmnopqr"; - let mut left_interner = StringInterner::new(); - let mut right_interner = StringInterner::new(); - let mut left_gc = GC::new(SafeOption::default()); - let mut right_gc = GC::new(SafeOption::default()); - - let left = left_interner.intern(key, &mut left_gc).unwrap(); - let right = right_interner.intern(key, &mut right_gc).unwrap(); - - let left_ptr = left.as_string_ptr().unwrap(); - let right_ptr = right.as_string_ptr().unwrap(); - - match (&left_ptr.as_ref().data.str, &right_ptr.as_ref().data.str) { - (LuaStrRepr::Shared(left), LuaStrRepr::Shared(right)) => { - assert_eq!(left.as_ref(), right.as_ref()); - assert_eq!(Arc::as_ptr(left), Arc::as_ptr(right)); - } - (left, right) => panic!("expected short strings, got {left:?} and {right:?}"), - } - } - - #[test] - fn shared_string_storage_boundaries_follow_feature_thresholds() { - let mut interner = StringInterner::new(); - let mut gc = GC::new(SafeOption::default()); - - let smol = interner.intern(&"a".repeat(23), &mut gc).unwrap(); - let shared_24 = interner.intern(&"b".repeat(24), &mut gc).unwrap(); - let shared_60 = interner.intern(&"c".repeat(60), &mut gc).unwrap(); - let heap_61 = interner.intern(&"d".repeat(61), &mut gc).unwrap(); - - assert!(smol.is_short_string()); - assert!(shared_24.is_short_string()); - assert!(shared_60.is_short_string()); - assert!(!heap_61.is_short_string()); - - assert!(matches!( - smol.as_string_ptr().unwrap().as_ref().data.str, - LuaStrRepr::Smol(_) - )); - assert!(matches!( - shared_24.as_string_ptr().unwrap().as_ref().data.str, - LuaStrRepr::Shared(_) - )); - assert!(matches!( - shared_60.as_string_ptr().unwrap().as_ref().data.str, - LuaStrRepr::Shared(_) - )); - assert!(matches!( - heap_61.as_string_ptr().unwrap().as_ref().data.str, - LuaStrRepr::Heap(_) - )); - } -} - -#[cfg(all(test, not(feature = "shared-string")))] -mod non_shared_tests { - use super::StringInterner; - - use crate::GC; - use crate::lua_value::LuaStrRepr; - use crate::lua_vm::SafeOption; - - #[test] - fn non_shared_storage_boundaries_follow_default_thresholds() { + fn short_string_storage_boundaries_stay_local_by_default() { let mut interner = StringInterner::new(); let mut gc = GC::new(SafeOption::default()); @@ -469,4 +372,39 @@ mod non_shared_tests { LuaStrRepr::Heap(_) )); } + + #[cfg(feature = "shared-proto")] + #[test] + fn explicit_share_marks_existing_string_as_shared() { + let key = "0123456789abcdefghijklmnopqr"; + let mut interner = StringInterner::new(); + let mut gc = GC::new(SafeOption::default()); + + let mut value = interner.intern(key, &mut gc).unwrap(); + let ptr_before = value.as_string_ptr().unwrap().as_u64(); + + assert!(share_lua_value(&mut value)); + + let ptr = value.as_string_ptr().unwrap(); + assert_eq!(ptr.as_u64(), ptr_before); + assert!(ptr.as_ref().header.is_shared()); + assert_ne!(ptr.as_ref().data.short_id(), 0); + } + + #[cfg(feature = "shared-proto")] + #[test] + fn shared_and_local_short_strings_keep_same_bytes() { + let key = "0123456789abcdefghijklmnopqr"; + let mut left_interner = StringInterner::new(); + let mut right_interner = StringInterner::new(); + let mut left_gc = GC::new(SafeOption::default()); + let mut right_gc = GC::new(SafeOption::default()); + + let mut shared_value = left_interner.intern(key, &mut left_gc).unwrap(); + let local_value = right_interner.intern(key, &mut right_gc).unwrap(); + + assert!(share_lua_value(&mut shared_value)); + + assert_eq!(shared_value.as_bytes(), local_value.as_bytes()); + } } diff --git a/crates/luars/src/lua_value/chunk_serializer.rs b/crates/luars/src/lua_value/chunk_serializer.rs index d0267eb8..61560b9f 100644 --- a/crates/luars/src/lua_value/chunk_serializer.rs +++ b/crates/luars/src/lua_value/chunk_serializer.rs @@ -3,12 +3,17 @@ use super::{Chunk, LocVar, LuaValue, UpvalueDesc}; use crate::Instruction; -use crate::gc::ObjectAllocator; +use crate::gc::{GcProto, ObjectAllocator}; use crate::lua_vm::LuaVM; use crate::lua_vm::lua_limits::LUAI_MAXSHORTLEN; use std::collections::HashMap; use std::io::{Cursor, Read}; -use std::rc::Rc; + +fn detached_proto(chunk: Chunk) -> crate::ProtoPtr { + let size = std::mem::size_of::() as u32 + chunk.proto_data_size; + let boxed = Box::new(GcProto::new(chunk, 0, size)); + crate::ProtoPtr::new(Box::leak(boxed) as *const GcProto) +} // Magic number for lua-rs bytecode (different from official Lua) const LUARS_MAGIC: &[u8] = b"\x1bLuaRS"; @@ -204,7 +209,7 @@ fn write_chunk( // Write child prototypes write_u32(buf, chunk.child_protos.len() as u32); for child in &chunk.child_protos { - write_chunk(buf, child, strip, pool)?; + write_chunk(buf, &child.as_ref().data, strip, pool)?; } // Write debug info (if not stripped) @@ -278,7 +283,7 @@ fn write_chunk_with_dedup( // Write child prototypes write_u32(buf, chunk.child_protos.len() as u32); for child in &chunk.child_protos { - write_chunk_with_dedup(buf, child, strip, pool, string_table)?; + write_chunk_with_dedup(buf, &child.as_ref().data, strip, pool, string_table)?; } // Write debug info (if not stripped) @@ -353,7 +358,7 @@ fn write_chunk_no_pool_with_dedup( // Write child prototypes write_u32(buf, chunk.child_protos.len() as u32); for child in &chunk.child_protos { - write_chunk_no_pool_with_dedup(buf, child, strip, string_table)?; + write_chunk_no_pool_with_dedup(buf, &child.as_ref().data, strip, string_table)?; } // Write debug info with deduplication @@ -424,7 +429,7 @@ fn write_chunk_no_pool(buf: &mut Vec, chunk: &Chunk, strip: bool) -> Result< // Write child prototypes write_u32(buf, chunk.child_protos.len() as u32); for child in &chunk.child_protos { - write_chunk_no_pool(buf, child, strip)?; + write_chunk_no_pool(buf, &child.as_ref().data, strip)?; } // Write debug info @@ -498,7 +503,7 @@ fn read_chunk(cursor: &mut Cursor<&[u8]>) -> Result { let child_len = read_u32(cursor)? as usize; let mut child_protos = Vec::with_capacity(child_len); for _ in 0..child_len { - child_protos.push(Rc::new(read_chunk(cursor)?)); + child_protos.push(detached_proto(read_chunk(cursor)?)); } // Read debug info @@ -590,7 +595,7 @@ fn read_chunk_with_dedup( let child_len = read_u32(cursor)? as usize; let mut child_protos = Vec::with_capacity(child_len); for _ in 0..child_len { - child_protos.push(Rc::new(read_chunk_with_dedup(cursor, string_table)?)); + child_protos.push(detached_proto(read_chunk_with_dedup(cursor, string_table)?)); } // Read debug info with string deduplication @@ -847,7 +852,8 @@ fn read_chunk_with_vm(cursor: &mut Cursor<&[u8]>, vm: &mut LuaVM) -> Result), /// Heap-backed storage for any non-inline string payload. Heap(Box<[u8]>), } +impl Clone for LuaStrRepr { + fn clone(&self) -> Self { + match self { + Self::Smol(value) => Self::Smol(value.clone()), + Self::Heap(value) => Self::Heap(value.clone()), + } + } +} + impl LuaStrRepr { #[inline(always)] pub fn len(&self) -> usize { match self { Self::Smol(s) => s.len(), - #[cfg(feature = "shared-string")] - Self::Shared(s) => s.len(), Self::Heap(s) => s.len(), } } @@ -112,21 +118,9 @@ impl LuaStrRepr { pub fn as_bytes(&self) -> &[u8] { match self { Self::Smol(s) => s.as_bytes(), - #[cfg(feature = "shared-string")] - Self::Shared(s) => s.as_ref(), Self::Heap(s) => s.as_ref(), } } - - #[cfg(feature = "shared-string")] - #[allow(unused)] - #[inline(always)] - pub(crate) fn shared_ptr(&self) -> Option<*const [u8]> { - match self { - Self::Shared(shared) => Some(Arc::as_ptr(shared)), - _ => None, - } - } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -176,29 +170,50 @@ impl std::fmt::Display for LuaStrRepr { } } -#[derive(Clone)] pub struct LuaString { /// Hash comes first for cache locality: GcHeader(8B) + hash(8B) = 16B, /// both in the same cache line after pointer dereference. /// C Lua has TString.hash at offset 12 — ours is now at offset 8. pub hash: u64, pub utf8: Utf8State, + #[cfg(feature = "shared-proto")] + short_id: AtomicUsize, pub str: LuaStrRepr, } +impl Clone for LuaString { + fn clone(&self) -> Self { + Self { + hash: self.hash, + utf8: self.utf8, + #[cfg(feature = "shared-proto")] + short_id: AtomicUsize::new(self.short_id()), + str: self.str.clone(), + } + } +} + impl std::fmt::Debug for LuaString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LuaString") - .field("str", &self.str) - .field("hash", &self.hash) - .field("utf8", &self.utf8) - .finish() + let mut debug = f.debug_struct("LuaString"); + debug.field("str", &self.str); + debug.field("hash", &self.hash); + debug.field("utf8", &self.utf8); + #[cfg(feature = "shared-proto")] + debug.field("short_id", &self.short_id()); + debug.finish() } } impl LuaString { pub fn new(s: LuaStrRepr, hash: u64, utf8: Utf8State) -> Self { - Self { hash, utf8, str: s } + Self { + hash, + utf8, + #[cfg(feature = "shared-proto")] + short_id: AtomicUsize::new(0), + str: s, + } } #[inline(always)] @@ -241,6 +256,73 @@ impl LuaString { pub fn is_long(&self) -> bool { self.str.len() > StringInterner::SHORT_STRING_LIMIT } + + #[cfg(feature = "shared-proto")] + #[inline(always)] + pub fn short_id(&self) -> usize { + self.short_id.load(Ordering::Relaxed) + } + + #[cfg(not(feature = "shared-proto"))] + #[inline(always)] + pub fn short_id(&self) -> usize { + 0 + } + + #[cfg(feature = "shared-proto")] + pub fn ensure_short_id(&self) -> usize { + let current = self.short_id(); + if current != 0 { + return current; + } + + let allocated = SHARED_SHORT_STRING_ID.fetch_add(1, Ordering::Relaxed); + match self + .short_id + .compare_exchange(0, allocated, Ordering::Relaxed, Ordering::Relaxed) + { + Ok(_) => allocated, + Err(existing) => existing, + } + } + + #[cfg(feature = "shared-proto")] + pub fn merge_short_ids(&self, other: &Self) { + let left = self.short_id(); + let right = other.short_id(); + let merged = left.max(right); + if merged == 0 { + return; + } + self.short_id.store(merged, Ordering::Relaxed); + other.short_id.store(merged, Ordering::Relaxed); + } +} + +#[inline(always)] +pub fn short_string_ptr_eq(left: StringPtr, right: StringPtr) -> bool { + if left == right { + return true; + } + + #[cfg(feature = "shared-proto")] + { + let left_string = &left.as_ref().data; + let right_string = &right.as_ref().data; + let left_id = left_string.short_id(); + let right_id = right_string.short_id(); + + if left_id != 0 && left_id == right_id { + return true; + } + + if left_string.hash == right_string.hash && left_string.str == right_string.str { + left_string.merge_short_ids(right_string); + return true; + } + } + + false } impl Eq for LuaString {} diff --git a/crates/luars/src/lua_value/lua_table/native_table.rs b/crates/luars/src/lua_value/lua_table/native_table.rs index a58ff385..758d5916 100644 --- a/crates/luars/src/lua_value/lua_table/native_table.rs +++ b/crates/luars/src/lua_value/lua_table/native_table.rs @@ -1,11 +1,12 @@ // Native Lua 5.5-style table implementation // Port of ltable.c with minimal abstractions for maximum performance -use crate::gc::GcString; use crate::lua_value::{ LuaValue, lua_value::{LUA_TNIL, LUA_VEMPTY, LUA_VNIL, LUA_VNUMINT, LUA_VSHRSTR, Value, novariant}, + short_string_ptr_eq, }; +use crate::{StringPtr, gc::GcString}; use std::alloc::{self, Layout}; use std::ptr; @@ -259,10 +260,15 @@ impl NativeTable { /// Returns true if found and written. pub unsafe fn get_shortstr_into(&self, key: &LuaValue, dest: *mut LuaValue) -> bool { let mut node = self.mainposition_string(key); - let key_ptr = unsafe { key.value.i }; + let key_ptr = StringPtr::new(unsafe { key.value.ptr as *mut GcString }); unsafe { - if (*node).key_tt == LUA_VSHRSTR && (*node).key_data.i == key_ptr { + if (*node).key_tt == LUA_VSHRSTR + && short_string_ptr_eq( + StringPtr::new((*node).key_data.ptr as *mut GcString), + key_ptr, + ) + { if (*node).val_tt != LUA_VNIL { (*dest).tt = (*node).val_tt; (*dest).value = (*node).val_data; @@ -274,7 +280,12 @@ impl NativeTable { let mut next = (*node).next; while next != 0 { node = node.offset(next as isize); - if (*node).key_tt == LUA_VSHRSTR && (*node).key_data.i == key_ptr { + if (*node).key_tt == LUA_VSHRSTR + && short_string_ptr_eq( + StringPtr::new((*node).key_data.ptr as *mut GcString), + key_ptr, + ) + { if (*node).val_tt != LUA_VNIL { (*dest).tt = (*node).val_tt; (*dest).value = (*node).val_data; @@ -298,11 +309,16 @@ impl NativeTable { } let mut node = self.mainposition_string(key); - let key_ptr = unsafe { key.value.i }; + let key_ptr = StringPtr::new(unsafe { key.value.ptr as *mut GcString }); unsafe { loop { - if (*node).key_tt == LUA_VSHRSTR && (*node).key_data.i == key_ptr { + if (*node).key_tt == LUA_VSHRSTR + && short_string_ptr_eq( + StringPtr::new((*node).key_data.ptr as *mut GcString), + key_ptr, + ) + { if (*node).val_tt != LUA_VNIL { (*node).set_value(value); return true; @@ -326,11 +342,16 @@ impl NativeTable { } let mut node = self.mainposition_string(key); - let key_ptr = unsafe { key.value.i }; + let key_ptr = StringPtr::new(unsafe { key.value.ptr as *mut GcString }); unsafe { loop { - if (*node).key_tt == LUA_VSHRSTR && (*node).key_data.i == key_ptr { + if (*node).key_tt == LUA_VSHRSTR + && short_string_ptr_eq( + StringPtr::new((*node).key_data.ptr as *mut GcString), + key_ptr, + ) + { if (*node).val_tt != LUA_VNIL { (*node).set_value_parts(value, tt); return true; @@ -1454,11 +1475,16 @@ impl NativeTable { return None; } let mut node = self.mainposition_string(key); - let key_ptr = unsafe { key.value.i }; + let key_ptr = StringPtr::new(unsafe { key.value.ptr as *mut GcString }); unsafe { // Unroll first iteration (most common case: found in main position) - if (*node).key_tt == LUA_VSHRSTR && (*node).key_data.i == key_ptr { + if (*node).key_tt == LUA_VSHRSTR + && short_string_ptr_eq( + StringPtr::new((*node).key_data.ptr as *mut GcString), + key_ptr, + ) + { let val = (*node).value(); return if val.is_nil() { None } else { Some(val) }; } @@ -1466,8 +1492,12 @@ impl NativeTable { let mut next = (*node).next; while next != 0 { node = node.offset(next as isize); - // Short strings: pointer comparison only (interned) - if (*node).key_tt == LUA_VSHRSTR && (*node).key_data.i == key_ptr { + if (*node).key_tt == LUA_VSHRSTR + && short_string_ptr_eq( + StringPtr::new((*node).key_data.ptr as *mut GcString), + key_ptr, + ) + { let val = (*node).value(); return if val.is_nil() { None } else { Some(val) }; } @@ -2203,6 +2233,13 @@ impl Drop for NativeTable { mod tests { use super::*; + #[cfg(feature = "shared-proto")] + use crate::gc::share_lua_value; + #[cfg(feature = "shared-proto")] + use crate::lua_vm::SafeOption; + #[cfg(feature = "shared-proto")] + use crate::{GC, StringInterner}; + #[test] fn test_native_table_basic() { let mut t = NativeTable::new(4, 4); @@ -2281,4 +2318,25 @@ mod tests { println!("NativeTable integer ops (20k ops): {:?}", elapsed); println!("Per-op: {:?}", elapsed / 20000); } + + #[cfg(feature = "shared-proto")] + #[test] + fn test_shared_short_string_lookup_with_local_query() { + let key = "0123456789abcdefghijklmnopqr"; + let mut shared_interner = StringInterner::new(); + let mut local_interner = StringInterner::new(); + let mut shared_gc = GC::new(SafeOption::default()); + let mut local_gc = GC::new(SafeOption::default()); + + let mut shared_key = shared_interner.intern(key, &mut shared_gc).unwrap(); + let local_key = local_interner.intern(key, &mut local_gc).unwrap(); + let value = LuaValue::integer(123); + + assert!(share_lua_value(&mut shared_key)); + + let mut table = NativeTable::new(0, 4); + table.raw_set(&shared_key, value); + + assert_eq!(table.raw_get(&local_key), Some(value)); + } } diff --git a/crates/luars/src/lua_value/lua_value.rs b/crates/luars/src/lua_value/lua_value.rs index bc02b019..a6412b84 100644 --- a/crates/luars/src/lua_value/lua_value.rs +++ b/crates/luars/src/lua_value/lua_value.rs @@ -28,7 +28,7 @@ // - Bits 0-3: 基础类型 (LUA_TNIL, LUA_TBOOLEAN, LUA_TNUMBER, etc.) // - Bits 4-5: variant bits (区分子类型,如integer/float, short/long string) // - Bit 6: BIT_ISCOLLECTABLE (标记是否是GC对象) -use crate::lua_value::{CClosureFunction, LuaUserdata, RClosureFunction}; +use crate::lua_value::{CClosureFunction, LuaUserdata, RClosureFunction, short_string_ptr_eq}; use crate::lua_vm::{CFunction, LuaState}; use crate::{ CClosurePtr, FunctionPtr, GcCClosure, GcFunction, GcObjectPtr, GcRClosure, GcString, GcTable, @@ -1139,7 +1139,11 @@ impl PartialEq for LuaValue { return true; } return match tt { - LUA_VSHRSTR => false, // different pointers, different interned strings + LUA_VSHRSTR => { + let left = StringPtr::new(unsafe { self.value.ptr as *mut GcString }); + let right = StringPtr::new(unsafe { other.value.ptr as *mut GcString }); + short_string_ptr_eq(left, right) + } LUA_VLNGSTR => { let s1 = unsafe { &*(self.value.ptr as *const GcString) }; let s2 = unsafe { &*(other.value.ptr as *const GcString) }; @@ -1414,6 +1418,38 @@ mod tests { assert_ne!(LuaValue::integer(42), LuaValue::integer(43)); } + #[cfg(feature = "shared-proto")] + #[test] + fn test_shared_short_string_equals_local_short_string() { + use crate::gc::share_lua_value; + use crate::lua_vm::SafeOption; + use crate::{GC, StringInterner}; + + let key = "0123456789abcdefghijklmnopqr"; + let mut left_interner = StringInterner::new(); + let mut right_interner = StringInterner::new(); + let mut left_gc = GC::new(SafeOption::default()); + let mut right_gc = GC::new(SafeOption::default()); + + let mut shared_value = left_interner.intern(key, &mut left_gc).unwrap(); + let local_value = right_interner.intern(key, &mut right_gc).unwrap(); + + assert!(share_lua_value(&mut shared_value)); + assert_eq!(shared_value, local_value); + assert_eq!(local_value, shared_value); + + let local_ptr = local_value.as_string_ptr().unwrap(); + assert_eq!( + local_ptr.as_ref().data.short_id(), + shared_value + .as_string_ptr() + .unwrap() + .as_ref() + .data + .short_id() + ); + } + #[test] fn test_type_tags() { assert_eq!(novariant(LUA_VNUMINT), LUA_TNUMBER); diff --git a/crates/luars/src/lua_value/mod.rs b/crates/luars/src/lua_value/mod.rs index 3eb4570e..fb2238e9 100644 --- a/crates/luars/src/lua_value/mod.rs +++ b/crates/luars/src/lua_value/mod.rs @@ -11,7 +11,6 @@ pub mod userdata_trait; use self::lua_value::Value; use std::any::Any; use std::fmt; -use std::rc::Rc; pub use lua_string::*; pub use userdata_builder::UserDataBuilder; @@ -31,7 +30,7 @@ pub use lua_value::{ }; use crate::lua_vm::CFunction; -use crate::{Instruction, RefUserData, TablePtr, UpvaluePtr}; +use crate::{Instruction, ProtoPtr, RefUserData, TablePtr, UpvaluePtr}; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct LuaValuePtr { @@ -387,13 +386,13 @@ pub struct Chunk { pub needs_vararg_table: bool, // Whether function needs vararg table (PF_VATAB in Lua 5.5) pub use_hidden_vararg: bool, // Whether function uses hidden vararg args (PF_VAHID in Lua 5.5) pub max_stack_size: usize, - pub child_protos: Vec>, // Nested function prototypes + pub child_protos: Vec, // Nested function prototypes pub upvalue_descs: Vec, // Upvalue descriptors - pub source_name: Option, // Source file/chunk name for debugging - pub line_info: Vec, // Line number for each instruction (for debug) - pub linedefined: usize, // Line where function starts (0 for main) - pub lastlinedefined: usize, // Line where function ends (0 for main) - pub proto_data_size: u32, // Cached size for GC (code+constants+children+lines) + pub source_name: Option, // Source file/chunk name for debugging + pub line_info: Vec, // Line number for each instruction (for debug) + pub linedefined: usize, // Line where function starts (0 for main) + pub lastlinedefined: usize, // Line where function ends (0 for main) + pub proto_data_size: u32, // Cached size for GC (code+constants+children+lines) } impl Default for Chunk { @@ -429,10 +428,32 @@ impl Chunk { use std::mem::size_of; let instr_size = self.code.len() * size_of::(); let const_size = self.constants.len() * size_of::(); - let child_size = self.child_protos.len() * size_of::(); + let child_size = self.child_protos.len() * size_of::(); let line_size = self.line_info.len() * size_of::(); self.proto_data_size = (instr_size + const_size + child_size + line_size) as u32; } + + #[cfg(feature = "shared-proto")] + pub fn share_constant_strings(&mut self) -> usize { + let mut shared_count = 0; + + for constant in &mut self.constants { + shared_count += usize::from(crate::gc::share_lua_value(constant)); + } + + shared_count + } + + #[cfg(feature = "shared-proto")] + pub fn share_proto_strings(&mut self) -> usize { + let mut shared_count = self.share_constant_strings(); + + for child in &mut self.child_protos { + shared_count += child.as_mut_ref().data.share_proto_strings(); + } + + shared_count + } } /// Inline storage for upvalue pointers — avoids heap allocation for 0-1 upvalues. @@ -488,12 +509,12 @@ impl UpvalueStore { } pub struct LuaFunction { - chunk: Rc, + chunk: ProtoPtr, upvalue_store: UpvalueStore, } impl LuaFunction { - pub fn new(chunk: Rc, upvalue_store: UpvalueStore) -> Self { + pub fn new(chunk: ProtoPtr, upvalue_store: UpvalueStore) -> Self { LuaFunction { chunk, upvalue_store, @@ -503,7 +524,12 @@ impl LuaFunction { /// Get the chunk if this is a Lua function #[inline(always)] pub fn chunk(&self) -> &Chunk { - &self.chunk + &self.chunk.as_ref().data + } + + #[inline(always)] + pub fn proto(&self) -> ProtoPtr { + self.chunk } /// Get upvalue pointers as a slice. diff --git a/crates/luars/src/lua_vm/execute/closure.rs b/crates/luars/src/lua_vm/execute/closure.rs index 79d6f123..97ac1593 100644 --- a/crates/luars/src/lua_vm/execute/closure.rs +++ b/crates/luars/src/lua_vm/execute/closure.rs @@ -20,10 +20,10 @@ pub fn push_closure( if bx >= current_chunk.child_protos.len() { return Err(LuaError::RuntimeError); } - let proto = current_chunk.child_protos[bx].clone(); + let proto = current_chunk.child_protos[bx]; // Get upvalue descriptors - let upvalue_descs = &proto.upvalue_descs; + let upvalue_descs = &proto.as_ref().data.upvalue_descs; let num_upvalues = upvalue_descs.len(); // Build UpvalueStore — avoid heap allocation for 0-1 upvalues diff --git a/crates/luars/src/lua_vm/lua_limits.rs b/crates/luars/src/lua_vm/lua_limits.rs index 8fcfdf44..a303f4fa 100644 --- a/crates/luars/src/lua_vm/lua_limits.rs +++ b/crates/luars/src/lua_vm/lua_limits.rs @@ -42,9 +42,6 @@ pub const CSTACKERR: usize = 30; /// Maximum length for "short" strings (interned in hash table). /// Matches Lua 5.5's LUAI_MAXSHORTLEN. -#[cfg(feature = "shared-string")] -pub const LUAI_MAXSHORTLEN: usize = 60; -#[cfg(not(feature = "shared-string"))] pub const LUAI_MAXSHORTLEN: usize = 40; // ===== Compiler ===== diff --git a/crates/luars/src/lua_vm/lua_state.rs b/crates/luars/src/lua_vm/lua_state.rs index 924ef75f..d50bfc6e 100644 --- a/crates/luars/src/lua_vm/lua_state.rs +++ b/crates/luars/src/lua_vm/lua_state.rs @@ -4,7 +4,6 @@ use std::future::Future; use std::pin::Pin; -use std::rc::Rc; use crate::lua_value::userdata_trait::UserDataTrait; use crate::lua_value::{LuaUserdata, LuaValue, LuaValueKind, LuaValuePtr, UpvalueStore}; @@ -19,8 +18,8 @@ use crate::lua_vm::{SANDBOX_TIMEOUT_CHECK_INTERVAL, SandboxRuntimeLimits}; #[cfg(feature = "sandbox")] use crate::platform_time::unix_nanos; use crate::{ - AsyncReturnValue, Chunk, CreateResult, GcObjectPtr, LuaAnyRef, LuaFunctionRef, LuaRegistrable, - LuaTableRef, LuaVM, StringPtr, TablePtr, ThreadPtr, UpvaluePtr, + AsyncReturnValue, CreateResult, GcObjectPtr, LuaAnyRef, LuaFunctionRef, LuaRegistrable, + LuaTableRef, LuaVM, ProtoPtr, StringPtr, TablePtr, ThreadPtr, UpvaluePtr, }; /// Execution state for a Lua thread/coroutine @@ -2010,7 +2009,7 @@ impl LuaState { /// Create function closure #[inline] - pub fn create_function(&mut self, chunk: Rc, upvalues: UpvalueStore) -> CreateResult { + pub fn create_function(&mut self, chunk: ProtoPtr, upvalues: UpvalueStore) -> CreateResult { self.vm_mut().create_function(chunk, upvalues) } @@ -2250,7 +2249,7 @@ impl LuaState { /// Execute a pre-compiled chunk, returning results. /// /// This is a convenience proxy for `LuaVM::execute`. - pub fn execute_chunk(&mut self, chunk: Rc) -> LuaResult> { + pub fn execute_chunk(&mut self, chunk: ProtoPtr) -> LuaResult> { self.vm_mut().execute_chunk(chunk) } diff --git a/crates/luars/src/lua_vm/mod.rs b/crates/luars/src/lua_vm/mod.rs index 5abf94e6..b29efbaf 100644 --- a/crates/luars/src/lua_vm/mod.rs +++ b/crates/luars/src/lua_vm/mod.rs @@ -42,9 +42,57 @@ pub use execute::TmKind; pub use execute::{get_metamethod_event, get_metatable}; pub use opcode::{Instruction, OpCode}; use std::future::Future; -use std::rc::Rc; #[cfg(feature = "sandbox")] use std::time::Duration; +#[cfg(feature = "shared-proto")] +use std::{cell::RefCell, collections::HashMap, path::PathBuf, time::SystemTime}; + +#[cfg(feature = "shared-proto")] +#[derive(Clone)] +struct SharedFileProtoEntry { + proto: crate::ProtoPtr, + len: u64, + modified: Option, + version: LuaLanguageLevel, +} + +#[cfg(feature = "shared-proto")] +thread_local! { + static SHARED_FILE_PROTO_CACHE: RefCell> = + RefCell::new(HashMap::new()); +} + +struct FileChunkLayout { + skip_offset: usize, + text_start: usize, + is_binary: bool, +} + +fn inspect_file_chunk_layout(file_bytes: &[u8]) -> FileChunkLayout { + let mut skip_offset = 0; + + if file_bytes.first() == Some(&b'#') { + if let Some(pos) = file_bytes.iter().position(|&b| b == b'\n') { + skip_offset = pos + 1; + } else { + skip_offset = file_bytes.len(); + } + } + + if file_bytes[skip_offset..].starts_with(&[0xEF, 0xBB, 0xBF]) { + skip_offset += 3; + } + + FileChunkLayout { + skip_offset, + text_start: if file_bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { + 3 + } else { + 0 + }, + is_binary: file_bytes.get(skip_offset) == Some(&0x1B), + } +} #[cfg(feature = "sandbox")] #[derive(Debug, Clone, PartialEq, Eq)] @@ -658,7 +706,7 @@ impl LuaVM { } /// Execute a chunk in the main thread - pub fn execute_chunk(&mut self, chunk: Rc) -> LuaResult> { + pub fn execute_chunk(&mut self, chunk: crate::ProtoPtr) -> LuaResult> { // Main chunk needs _ENV upvalue pointing to global table // This matches Lua 5.4+ behavior where all chunks have _ENV as upvalue[0] let env_upval = self.create_upvalue_closed(self.global)?; @@ -666,10 +714,42 @@ impl LuaVM { self.execute_function(func, vec![]) } + #[inline] + pub(crate) fn prepare_loaded_chunk(&mut self, chunk: Chunk) -> crate::ProtoPtr { + #[cfg(feature = "shared-proto")] + { + let proto = self + .create_proto(chunk) + .expect("failed to allocate proto for loaded chunk"); + crate::gc::share_proto(proto); + proto + } + + #[cfg(not(feature = "shared-proto"))] + self.create_proto(chunk) + .expect("failed to allocate proto for loaded chunk") + } + + #[inline] + pub(crate) fn create_loaded_function( + &mut self, + chunk: Chunk, + upvalues: UpvalueStore, + ) -> LuaResult { + let chunk = self.prepare_loaded_chunk(chunk); + self.create_function(chunk, upvalues) + } + + #[inline] + pub(crate) fn execute_loaded_chunk(&mut self, chunk: Chunk) -> LuaResult> { + let chunk = self.prepare_loaded_chunk(chunk); + self.execute_chunk(chunk) + } + #[cfg(feature = "sandbox")] fn execute_chunk_with_env( &mut self, - chunk: Rc, + chunk: crate::ProtoPtr, env: LuaValue, ) -> LuaResult> { let env_upval = self.create_upvalue_closed(env)?; @@ -679,7 +759,7 @@ impl LuaVM { pub fn execute(&mut self, source: &str) -> LuaResult> { let chunk = self.compile(source)?; - self.execute_chunk(Rc::new(chunk)) + self.execute_loaded_chunk(chunk) } #[cfg(feature = "sandbox")] @@ -693,7 +773,8 @@ impl LuaVM { let limits = config.runtime_limits(); self.main_state() .with_sandbox_runtime_limits(limits, |state| { - state.vm_mut().execute_chunk_with_env(Rc::new(chunk), env) + let chunk = state.vm_mut().prepare_loaded_chunk(chunk); + state.vm_mut().execute_chunk_with_env(chunk, env) }) } @@ -713,7 +794,7 @@ impl LuaVM { pub fn load(&mut self, source: &str) -> LuaResult { let chunk = self.compile(source)?; let env_upval = self.create_upvalue_closed(self.global)?; - self.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upval)) + self.create_loaded_function(chunk, UpvalueStore::from_single(env_upval)) } #[cfg(feature = "sandbox")] @@ -721,7 +802,7 @@ impl LuaVM { let chunk = self.compile(source)?; let env = self.create_sandbox_env(config)?; let env_upval = self.create_upvalue_closed(env)?; - self.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upval)) + self.create_loaded_function(chunk, UpvalueStore::from_single(env_upval)) } /// Compile source code with a chunk name and return a callable function value. @@ -730,7 +811,7 @@ impl LuaVM { pub fn load_with_name(&mut self, source: &str, chunk_name: &str) -> LuaResult { let chunk = self.compile_with_name(source, chunk_name)?; let env_upval = self.create_upvalue_closed(self.global)?; - self.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upval)) + self.create_loaded_function(chunk, UpvalueStore::from_single(env_upval)) } #[cfg(feature = "sandbox")] @@ -743,7 +824,7 @@ impl LuaVM { let chunk = self.compile_with_name(source, chunk_name)?; let env = self.create_sandbox_env(config)?; let env_upval = self.create_upvalue_closed(env)?; - self.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upval)) + self.create_loaded_function(chunk, UpvalueStore::from_single(env_upval)) } /// Read a file, compile it, and execute it. @@ -756,11 +837,8 @@ impl LuaVM { /// let results = vm.dofile("scripts/init.lua")?; /// ``` pub fn dofile(&mut self, path: &str) -> LuaResult> { - let source = std::fs::read_to_string(path) - .map_err(|e| self.error(format!("cannot open {}: {}", path, e)))?; - let chunk_name = format!("@{}", path); - let chunk = self.compile_with_name(&source, &chunk_name)?; - self.execute_chunk(Rc::new(chunk)) + let proto = self.load_proto_from_file(path)?; + self.execute_chunk(proto) } /// Call a function value with arguments (synchronous). @@ -1164,6 +1242,69 @@ impl LuaVM { Ok(chunk) } + pub(crate) fn load_proto_from_file(&mut self, path: &str) -> LuaResult { + use crate::lua_value::chunk_serializer; + + let resolved_path = std::fs::canonicalize(path) + .map_err(|e| self.error(format!("cannot open {}: {}", path, e)))?; + + #[cfg(feature = "shared-proto")] + { + let metadata = std::fs::metadata(&resolved_path) + .map_err(|e| self.error(format!("cannot open {}: {}", path, e)))?; + let len = metadata.len(); + let modified = metadata.modified().ok(); + let version = self.version; + if let Some(proto) = SHARED_FILE_PROTO_CACHE.with(|cache| { + let cache = cache.borrow(); + cache.get(&resolved_path).and_then(|entry| { + (entry.len == len && entry.modified == modified && entry.version == version) + .then_some(entry.proto) + }) + }) { + return Ok(proto); + } + } + + let file_bytes = std::fs::read(&resolved_path) + .map_err(|e| self.error(format!("cannot open {}: {}", path, e)))?; + let layout = inspect_file_chunk_layout(&file_bytes); + let chunk_name = format!("@{}", resolved_path.display()); + + let chunk = if layout.is_binary { + chunk_serializer::deserialize_chunk_with_strings_vm( + &file_bytes[layout.skip_offset..], + self, + ) + .map_err(|e| self.error(format!("binary load error: {}", e)))? + } else { + let code_str = String::from_utf8(file_bytes[layout.text_start..].to_vec()) + .map_err(|_| self.error("source file is not valid UTF-8".to_string()))?; + self.compile_with_name(&code_str, &chunk_name)? + }; + + let proto = self.prepare_loaded_chunk(chunk); + + #[cfg(feature = "shared-proto")] + { + let metadata = std::fs::metadata(&resolved_path) + .map_err(|e| self.error(format!("cannot open {}: {}", path, e)))?; + SHARED_FILE_PROTO_CACHE.with(|cache| { + cache.borrow_mut().insert( + resolved_path, + SharedFileProtoEntry { + proto, + len: metadata.len(), + modified: metadata.modified().ok(), + version: self.version, + }, + ); + }); + } + + Ok(proto) + } + pub fn get_global(&mut self, name: &str) -> LuaResult> { let key = self.create_string(name)?; Ok(self.raw_get(&self.global, &key)) @@ -1430,8 +1571,7 @@ impl LuaVM { ) -> LuaResult { // Main chunk needs _ENV upvalue pointing to global table let env_upval = self.create_upvalue_closed(self.global)?; - let func_val = - self.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upval))?; + let func_val = self.create_loaded_function(chunk, UpvalueStore::from_single(env_upval))?; let thread_val = self.create_thread(func_val)?; let vm_ptr = self as *mut LuaVM; Ok(async_thread::AsyncThread::new(thread_val, vm_ptr, args)) @@ -1523,7 +1663,7 @@ impl LuaVM { let chunk = self.compile(async_thread::ASYNC_CALL_RUNNER)?; let env_upval = self.create_upvalue_closed(self.global)?; let runner_func = - self.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upval))?; + self.create_loaded_function(chunk, UpvalueStore::from_single(env_upval))?; let thread_val = self.create_thread(runner_func)?; let vm_ptr = self as *mut LuaVM; async_thread::AsyncCallHandle::new(thread_val, vm_ptr, func) @@ -1682,9 +1822,18 @@ impl LuaVM { self.object_allocator.create_userdata(&mut self.gc, data) } + #[inline(always)] + pub fn create_proto(&mut self, chunk: Chunk) -> LuaResult { + self.object_allocator.create_proto(&mut self.gc, chunk) + } + /// Create a function in object pool #[inline(always)] - pub fn create_function(&mut self, chunk: Rc, upvalues: UpvalueStore) -> CreateResult { + pub fn create_function( + &mut self, + chunk: crate::ProtoPtr, + upvalues: UpvalueStore, + ) -> CreateResult { self.object_allocator .create_function(&mut self.gc, chunk, upvalues) } diff --git a/crates/luars/src/stdlib/basic/mod.rs b/crates/luars/src/stdlib/basic/mod.rs index 1c164a6f..bb772bfa 100644 --- a/crates/luars/src/stdlib/basic/mod.rs +++ b/crates/luars/src/stdlib/basic/mod.rs @@ -5,8 +5,6 @@ pub mod parse_number; mod require; -use std::rc::Rc; - use crate::gc::{code_param, decode_param}; use crate::lib_registry::LibraryModule; use crate::lua_value::{LuaValue, LuaValueKind, UpvalueStore}; @@ -1374,7 +1372,9 @@ fn lua_load(l: &mut LuaState) -> LuaResult { } } - let func = l.create_function(Rc::new(chunk), UpvalueStore::from_vec(upvalues))?; + let func = l + .vm_mut() + .create_loaded_function(chunk, UpvalueStore::from_vec(upvalues))?; l.push_value(func)?; Ok(1) } @@ -1390,8 +1390,6 @@ fn lua_load(l: &mut LuaState) -> LuaResult { /// loadfile([filename [, mode [, env]]]) - Load a file as a chunk fn lua_loadfile(l: &mut LuaState) -> LuaResult { - use crate::lua_value::chunk_serializer; - let filename = l .get_arg(1) .ok_or_else(|| l.error("bad argument #1 to 'loadfile' (value expected)".to_string()))?; @@ -1459,39 +1457,9 @@ fn lua_loadfile(l: &mut LuaState) -> LuaResult { } } - let chunkname = format!("@{}", filename_str); - - let chunk_result = if is_binary { - // Deserialize binary bytecode (skip shebang/BOM) - let vm = l.vm_mut(); - chunk_serializer::deserialize_chunk_with_strings_vm(&file_bytes[skip_offset..], vm) - .map_err(|e| format!("binary load error: {}", e)) - } else { - // For text, strip BOM but keep shebang (tokenizer handles it) - let text_start = if file_bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { - 3 - } else { - 0 - }; - // Source files must be valid UTF-8 - let code_str = match String::from_utf8(file_bytes[text_start..].to_vec()) { - Ok(s) => s, - Err(_) => { - // loadfile returns nil + error message on failure - let err_msg = l.create_string("source file is not valid UTF-8")?; - l.push_value(LuaValue::nil())?; - l.push_value(err_msg)?; - return Ok(2); - } - }; - let vm = l.vm_mut(); - vm.compile_with_name(&code_str, &chunkname) - .map_err(|e| vm.get_error_message(e)) - }; - - match chunk_result { - Ok(chunk) => { - let upvalue_count = chunk.upvalue_count; + match l.vm_mut().load_proto_from_file(&filename_str) { + Ok(proto) => { + let upvalue_count = proto.as_ref().data.upvalue_count; let mut upvalues = Vec::with_capacity(upvalue_count); for i in 0..upvalue_count { @@ -1509,8 +1477,9 @@ fn lua_loadfile(l: &mut LuaState) -> LuaResult { } } - let func = - l.create_function(std::rc::Rc::new(chunk), UpvalueStore::from_vec(upvalues))?; + let func = l + .vm_mut() + .create_function(proto, UpvalueStore::from_vec(upvalues))?; l.push_value(func)?; Ok(1) } @@ -1541,56 +1510,13 @@ fn lua_dofile(l: &mut LuaState) -> LuaResult { return Err(l.error("dofile: reading from stdin not yet implemented".to_string())); }; - // Load from file as bytes - let file_bytes = match std::fs::read(&filename_str) { - Ok(b) => b, - Err(e) => { - return Err(l.error(format!("cannot open {}: {}", filename_str, e))); - } - }; - - // Determine content after skipping shebang/BOM for binary detection - let mut skip_offset = 0; - if file_bytes.first() == Some(&b'#') { - if let Some(pos) = file_bytes.iter().position(|&b| b == b'\n') { - skip_offset = pos + 1; - } else { - skip_offset = file_bytes.len(); - } - } - if file_bytes[skip_offset..].starts_with(&[0xEF, 0xBB, 0xBF]) { - skip_offset += 3; - } - - let is_binary = file_bytes.get(skip_offset) == Some(&0x1B); - - // Compile/load the code - let chunkname = format!("@{}", filename_str); - let chunk = if is_binary { - use crate::lua_value::chunk_serializer; - let vm = l.vm_mut(); - chunk_serializer::deserialize_chunk_with_strings_vm(&file_bytes[skip_offset..], vm) - .map_err(|e| l.error(format!("binary load error: {}", e)))? - } else { - let text_start = if file_bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { - 3 - } else { - 0 - }; - let code_str = String::from_utf8(file_bytes[text_start..].to_vec()) - .map_err(|_| l.error("source file is not valid UTF-8".to_string()))?; - l.vm_mut() - .compile_with_name(&code_str, &chunkname) - .map_err(|e| l.error(format!("error loading {}: {}", filename_str, e)))? - }; - + let proto = l.vm_mut().load_proto_from_file(&filename_str)?; let global = l.vm_mut().global; // Create function with _ENV upvalue (global table) let env_upvalue = l.create_upvalue_closed(global)?; - let func = l.create_function( - std::rc::Rc::new(chunk), - UpvalueStore::from_single(env_upvalue), - )?; + let func = l + .vm_mut() + .create_function(proto, UpvalueStore::from_single(env_upvalue))?; // Use call_stack_based which supports yields (equivalent to lua_callk in C Lua). // C Lua does lua_settop(L, 1) to keep only the filename, then pushes the chunk at slot 2. diff --git a/crates/luars/src/stdlib/package.rs b/crates/luars/src/stdlib/package.rs index 3727fade..15993458 100644 --- a/crates/luars/src/stdlib/package.rs +++ b/crates/luars/src/stdlib/package.rs @@ -1,8 +1,6 @@ // Package library // Implements: config, cpath, loaded, loadlib, path, preload, searchers, searchpath -use std::rc::Rc; - use crate::lib_registry::LibraryModule; use crate::lua_value::{LuaValue, UpvalueStore}; use crate::lua_vm::{LuaResult, LuaState}; @@ -189,27 +187,12 @@ fn lua_file_loader(l: &mut LuaState) -> LuaResult { return Err(l.error("file path must be a string".to_string())); }; - if std::fs::metadata(filepath_str).is_err() { - return Ok(0); - } - - // Read the file - let source = match std::fs::read_to_string(filepath_str) { - Ok(s) => s, - Err(e) => { - return Err(l.error(format!("cannot open file '{}': {}", filepath_str, e))); - } - }; - let vm = l.vm_mut(); - - // Compile it using VM's string pool with chunk name - let chunkname = format!("@{}", filepath_str); - let chunk = vm.compile_with_name(&source, &chunkname)?; + let proto = vm.load_proto_from_file(filepath_str)?; // Create a function from the chunk with _ENV upvalue let env_upvalue = vm.create_upvalue_closed(vm.global)?; - let func = vm.create_function(Rc::new(chunk), UpvalueStore::from_single(env_upvalue))?; + let func = vm.create_function(proto, UpvalueStore::from_single(env_upvalue))?; // Call the function to execute the module and get its return value // The module should return its exports (usually a table) diff --git a/crates/luars/src/test/test_api_proposals.rs b/crates/luars/src/test/test_api_proposals.rs index 379d1ed1..77bdee99 100644 --- a/crates/luars/src/test/test_api_proposals.rs +++ b/crates/luars/src/test/test_api_proposals.rs @@ -162,6 +162,119 @@ fn test_load_does_not_execute() { assert!(x.is_none()); } +#[cfg(feature = "shared-proto")] +#[test] +fn test_load_marks_chunk_short_strings_shared() { + let mut vm = LuaVM::new(SafeOption::default()); + vm.open_stdlib(Stdlib::All).unwrap(); + + let func = vm + .load("local key = '0123456789abcdefghijklmnopqr'; return key") + .unwrap(); + + let function = func.as_function_ptr().unwrap(); + let chunk = function.as_ref().data.chunk(); + let key = chunk + .constants + .iter() + .copied() + .find(|value| value.as_str() == Some("0123456789abcdefghijklmnopqr")) + .unwrap(); + + assert!(key.is_short_string()); + assert!(key.as_string_ptr().unwrap().as_ref().header.is_shared()); + assert!(function.as_ref().data.proto().as_ref().header.is_shared()); +} + +#[cfg(feature = "shared-proto")] +#[test] +fn test_shared_proto_survives_vm_drop() { + let proto = { + let mut vm = LuaVM::new(SafeOption::default()); + vm.open_stdlib(Stdlib::All).unwrap(); + + let func = vm + .load("local key = '0123456789abcdefghijklmnopqr'; return key") + .unwrap(); + + func.as_function_ptr().unwrap().as_ref().data.proto() + }; + + assert!(proto.as_ref().header.is_shared()); + + let key = proto + .as_ref() + .data + .constants + .iter() + .copied() + .find(|value| value.as_str() == Some("0123456789abcdefghijklmnopqr")) + .unwrap(); + + assert!(key.as_string_ptr().unwrap().as_ref().header.is_shared()); +} + +#[cfg(feature = "shared-proto")] +#[test] +fn test_shared_proto_reuses_same_file_across_vms() { + use std::io::Write; + + let path = std::env::temp_dir().join("lua_rs_shared_proto_cache.lua"); + { + let mut file = std::fs::File::create(&path).unwrap(); + writeln!(file, "return 'shared-proto-cache'").unwrap(); + } + + let proto1 = { + let mut vm = LuaVM::new(SafeOption::default()); + vm.open_stdlib(Stdlib::All).unwrap(); + vm.load_proto_from_file(path.to_str().unwrap()).unwrap() + }; + + let proto2 = { + let mut vm = LuaVM::new(SafeOption::default()); + vm.open_stdlib(Stdlib::All).unwrap(); + vm.load_proto_from_file(path.to_str().unwrap()).unwrap() + }; + + assert_eq!(proto1, proto2); + + std::fs::remove_file(&path).ok(); +} + +#[cfg(feature = "shared-proto")] +#[test] +fn test_shared_proto_reloads_when_file_changes() { + use std::io::Write; + + let path = std::env::temp_dir().join("lua_rs_shared_proto_reload.lua"); + { + let mut file = std::fs::File::create(&path).unwrap(); + writeln!(file, "return 1").unwrap(); + } + + let proto1 = { + let mut vm = LuaVM::new(SafeOption::default()); + vm.open_stdlib(Stdlib::All).unwrap(); + vm.load_proto_from_file(path.to_str().unwrap()).unwrap() + }; + + { + let mut file = std::fs::File::create(&path).unwrap(); + writeln!(file, "return 123456789").unwrap(); + } + + let proto2 = { + let mut vm = LuaVM::new(SafeOption::default()); + vm.open_stdlib(Stdlib::All).unwrap(); + vm.load_proto_from_file(path.to_str().unwrap()).unwrap() + }; + + assert_ne!(proto1, proto2); + + std::fs::remove_file(&path).ok(); +} + // ============================ // P4: register_type_of on LuaVM (tested implicitly via existing userdata tests) // ============================ diff --git a/crates/luars_interpreter/src/bin/bytecode_dump.rs b/crates/luars_interpreter/src/bin/bytecode_dump.rs index c55001ac..df6da2e7 100644 --- a/crates/luars_interpreter/src/bin/bytecode_dump.rs +++ b/crates/luars_interpreter/src/bin/bytecode_dump.rs @@ -707,11 +707,12 @@ fn dump_chunk( // Recursively dump child protos if !chunk.child_protos.is_empty() { for child in chunk.child_protos.iter() { + let child_chunk = &child.as_ref().data; dump_chunk( - child, + child_chunk, filename, - child.linedefined, - child.lastlinedefined, + child_chunk.linedefined, + child_chunk.lastlinedefined, false, vm, ); diff --git a/crates/luars_interpreter/src/lib.rs b/crates/luars_interpreter/src/lib.rs index 3a16a083..8a453aca 100644 --- a/crates/luars_interpreter/src/lib.rs +++ b/crates/luars_interpreter/src/lib.rs @@ -5,7 +5,6 @@ use luars::stdlib; use std::env; use std::fs; use std::io::{self, BufRead, Read, Write}; -use std::rc::Rc; const VERSION: &str = "Lua-RS 5.5 (compatible)"; const COPYRIGHT: &str = "Copyright (C) 2026 lua-rs CppCXY"; @@ -133,8 +132,8 @@ fn require_module(vm: &mut LuaVM, module: &str) -> Result<(), String> { let code = format!("{} = require('{}')", module, module); match vm.compile(&code) { Ok(chunk) => { - vm.execute_chunk(Rc::new(chunk)) - .map_err(|e| format!("{}", e))?; + let proto = vm.create_proto(chunk).unwrap(); + vm.execute_chunk(proto).map_err(|e| format!("{}", e))?; Ok(()) } Err(e) => Err(format!("failed to load module '{}': {}", module, e)), @@ -147,7 +146,8 @@ fn execute_file(vm: &mut LuaVM, filename: &str) -> Result<(), String> { match vm.compile_with_name(&code, &format!("@{}", filename)) { Ok(chunk) => { - match vm.execute_chunk(Rc::new(chunk)) { + let proto = vm.create_proto(chunk).unwrap(); + match vm.execute_chunk(proto) { Ok(_) => Ok(()), Err(e) => { // execute_chunk already generates traceback before unwinding, @@ -169,8 +169,8 @@ fn execute_stdin(vm: &mut LuaVM) -> Result<(), String> { match vm.compile(&code) { Ok(chunk) => { - vm.execute_chunk(Rc::new(chunk)) - .map_err(|e| format!("{}", e))?; + let proto = vm.create_proto(chunk).unwrap(); + vm.execute_chunk(proto).map_err(|e| format!("{}", e))?; Ok(()) } Err(e) => Err(format!("stdin: {}", e)), @@ -226,7 +226,8 @@ fn run_repl(vm: &mut LuaVM) { // Try to compile and execute match vm.compile(&code_to_run) { Ok(chunk) => { - match vm.execute_chunk(Rc::new(chunk)) { + let proto = vm.create_proto(chunk).unwrap(); + match vm.execute_chunk(proto) { Ok(results) => { // Print non-nil first result if let Some(first) = results.into_iter().next() @@ -411,7 +412,8 @@ fn lua_main() -> i32 { resolved.replace('\\', "\\\\").replace('\'', "\\'") ); if let Ok(chunk) = vm.compile(&code) { - let _ = vm.execute_chunk(Rc::new(chunk)); + let proto = vm.create_proto(chunk).unwrap(); + let _ = vm.execute_chunk(proto); } } // Override package.cpath from LUA_CPATH_5_5 or LUA_CPATH @@ -426,7 +428,8 @@ fn lua_main() -> i32 { resolved.replace('\\', "\\\\").replace('\'', "\\'") ); if let Ok(chunk) = vm.compile(&code) { - let _ = vm.execute_chunk(Rc::new(chunk)); + let proto = vm.create_proto(chunk).unwrap(); + let _ = vm.execute_chunk(proto); } } } @@ -447,7 +450,8 @@ fn lua_main() -> i32 { // Execute string match vm.compile(&init) { Ok(chunk) => { - if let Err(e) = vm.execute_chunk(Rc::new(chunk)) { + let proto = vm.create_proto(chunk).unwrap(); + if let Err(e) = vm.execute_chunk(proto) { let error_msg = vm.get_error_message(e); eprintln!("lua: {}", error_msg); return 1; @@ -465,7 +469,8 @@ fn lua_main() -> i32 { if opts.warnings_on && let Ok(chunk) = vm.compile("warn('@on')") { - let _ = vm.execute_chunk(Rc::new(chunk)); + let proto = vm.create_proto(chunk).unwrap(); + let _ = vm.execute_chunk(proto); } // Setup arg table @@ -489,7 +494,8 @@ fn lua_main() -> i32 { for code in &opts.execute_strings { match vm.compile(code) { Ok(chunk) => { - if let Err(e) = vm.execute_chunk(Rc::new(chunk)) { + let proto = vm.create_proto(chunk).unwrap(); + if let Err(e) = vm.execute_chunk(proto) { let error_msg = vm.get_error_message(e); eprintln!("lua: Runtime Error: {}", error_msg); return 1; @@ -518,7 +524,8 @@ fn lua_main() -> i32 { dir = dir.replace('\\', "/") ); if let Ok(chunk) = vm.compile(&set_path) { - let _ = vm.execute_chunk(Rc::new(chunk)); + let proto = vm.create_proto(chunk).unwrap(); + let _ = vm.execute_chunk(proto); } } if let Err(e) = execute_file(&mut vm, filename) {