diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index bd5d229..684817e 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -131,7 +131,7 @@ impl Vm { let prepared = self.prepare_call(function, arguments.to_vec(), &mut executed, limits); match prepared { Ok(PreparedCall::Value(value)) => { - let reads = match self.objects.end_read_only(scope) { + let reads = match self.objects.end_read_only_precise(scope) { Ok(reads) => reads, Err(error) => { self.drop_root(value); @@ -140,7 +140,8 @@ impl Vm { }; Ok(ExecutionProgress::Complete { value, - reads, + dependencies: reads.dependencies, + reads: reads.objects, instructions: executed, }) } @@ -216,11 +217,12 @@ impl Vm { while let Some(frame) = call.frames.pop() { self.clear_frame(frame); } - let reads = self.objects.end_read_only(call.scope); + let reads = self.objects.end_read_only_precise(call.scope); match (result, reads) { (Ok(Some(value)), Ok(reads)) => Ok(ExecutionProgress::Complete { value, - reads, + dependencies: reads.dependencies, + reads: reads.objects, instructions: call.executed, }), (Ok(Some(value)), Err(error)) => { @@ -1077,12 +1079,11 @@ impl Vm { } Opcode::LoadGlobal => { let name = &frame.executable.layout().names[argument]; - let key = Value::Text(name.clone()); - let mut value = self.dict_get(frame.globals, &key)?; + let mut value = self.objects.dict_get_name(frame.globals, name)?; if value.is_none() && let Some(builtins) = frame.builtins { - value = self.dict_get(builtins, &key)?; + value = self.objects.dict_get_name(builtins, name)?; } let value = value.ok_or_else(|| VmError::NameNotFound(name.clone()))?; self.push_borrowed(frame, value)?; diff --git a/crates/core/src/frame.rs b/crates/core/src/frame.rs index 36875c2..3a84503 100644 --- a/crates/core/src/frame.rs +++ b/crates/core/src/frame.rs @@ -103,6 +103,7 @@ pub enum ExecutionProgress { Complete { value: Value, reads: Vec, + dependencies: Vec, instructions: usize, }, } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 06c8911..8b747f9 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -58,7 +58,7 @@ pub use objects::{ EndorNativeCall, EndorNativeFunctionObject, EndorNumber, EndorNumberObject, EndorObject, EndorRational, EndorReal, EndorStringObject, EndorTracebackEntry, Instruction, NativeCall, NativeCallable, NativeContext, NativeFunction, NativeResult, ObjectChange, ObjectHeader, - ObjectPayload, ObjectRef, Opcode, ReadAccess, Record, SourceSignature, Value, VmError, - WeakObjectRef, + ObjectPayload, ObjectRef, Opcode, ReadAccess, ReadDependency, ReadSet, Record, SourceSignature, + TrackedReads, Value, VmError, WeakObjectRef, }; pub use vm::Vm; diff --git a/crates/objects/src/effects.rs b/crates/objects/src/effects.rs index 2e7516a..cdd89e9 100644 --- a/crates/objects/src/effects.rs +++ b/crates/objects/src/effects.rs @@ -131,12 +131,12 @@ impl ObjectStore { Fact::Unknown }), Opcode::LoadGlobal => { - let key = Value::Text(code.layout().names[argument].clone()); - let mut value = self.dict_get(function.globals(), &key)?; + let name = &code.layout().names[argument]; + let mut value = self.dict_get_name(function.globals(), name)?; if value.is_none() && let Some(builtins) = function.builtins() { - value = self.dict_get(builtins, &key)?; + value = self.dict_get_name(builtins, name)?; } stack.push(value.map(Fact::value).unwrap_or(Fact::Unknown)); } diff --git a/crates/objects/src/heap.rs b/crates/objects/src/heap.rs index 4d0da5d..cafdb27 100644 --- a/crates/objects/src/heap.rs +++ b/crates/objects/src/heap.rs @@ -41,6 +41,8 @@ struct ReadOnly { protected: BTreeSet, reads: Mutex>, regions: Mutex>>, + dependencies: Mutex>, + dependency_regions: Mutex>>, } #[derive(Debug)] @@ -63,6 +65,49 @@ pub struct ReadAccess { pub content_read: bool, } +/// Borrowed observations; Object handles never retain the observed graph. +/// Named values are immutable comparison snapshots, not managed owning edges. +#[derive(Debug, Clone, PartialEq)] +pub enum ReadDependency { + Object { + object: ObjectRef, + version: u64, + }, + DictionaryKind { + object: ObjectRef, + }, + Name { + object: ObjectRef, + name: String, + value: Option, + }, +} +impl ReadDependency { + pub fn object(&self) -> ObjectRef { + match self { + Self::Object { object, .. } + | Self::DictionaryKind { object } + | Self::Name { object, .. } => *object, + } + } +} +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum ReadKey { + Object(ObjectRef), + DictionaryKind(ObjectRef), + Name(ObjectRef, String), +} +#[derive(Debug)] +pub struct TrackedReads { + pub accesses: Vec, + pub dependencies: Vec, +} +#[derive(Debug)] +pub struct ReadSet { + pub objects: Vec, + pub dependencies: Vec, +} + struct ValidationScope<'a> { heap: &'a Heap, thread: ThreadId, @@ -128,23 +173,34 @@ impl Heap { protected, reads: Mutex::new(BTreeSet::new()), regions: Mutex::new(Vec::new()), + dependencies: Mutex::new(BTreeMap::new()), + dependency_regions: Mutex::new(Vec::new()), }); self.read_only.len() } pub(crate) fn end_read_only(&mut self, scope: usize) -> Result, VmError> { + Ok(self.end_read_only_precise(scope)?.objects) + } + pub(crate) fn end_read_only_precise(&mut self, scope: usize) -> Result { if scope == 0 || scope != self.read_only.len() { return Err(VmError::InvalidArgument("read-only scope nesting mismatch")); } - Ok(self - .read_only - .pop() - .unwrap() - .reads - .into_inner() - .expect("read tracker poisoned") - .into_iter() - .collect()) + let scope = self.read_only.pop().unwrap(); + Ok(ReadSet { + objects: scope + .reads + .into_inner() + .expect("read tracker poisoned") + .into_iter() + .collect(), + dependencies: scope + .dependencies + .into_inner() + .expect("read tracker poisoned") + .into_values() + .collect(), + }) } pub(crate) fn begin_read_tracking(&self) -> Result { @@ -153,6 +209,11 @@ impl Heap { ))?; let mut regions = scope.regions.lock().expect("read tracker poisoned"); regions.push(BTreeMap::new()); + scope + .dependency_regions + .lock() + .expect("read tracker poisoned") + .push(BTreeMap::new()); Ok(regions.len()) } @@ -168,22 +229,32 @@ impl Heap { &self, token: usize, ) -> Result, VmError> { + Ok(self.end_read_tracking_precise(token)?.accesses) + } + pub(crate) fn end_read_tracking_precise(&self, token: usize) -> Result { let scope = self.read_only.last().ok_or(VmError::InvalidArgument( "read tracking requires a read-only scope", ))?; let mut regions = scope.regions.lock().expect("read tracker poisoned"); - if token == 0 || token != regions.len() { + let mut precise = scope + .dependency_regions + .lock() + .expect("read tracker poisoned"); + if token == 0 || token != regions.len() || token != precise.len() { return Err(VmError::InvalidArgument("read tracking nesting mismatch")); } - Ok(regions - .pop() - .unwrap() - .into_iter() - .map(|(object, content_read)| ReadAccess { - object, - content_read, - }) - .collect()) + Ok(TrackedReads { + accesses: regions + .pop() + .unwrap() + .into_iter() + .map(|(object, content_read)| ReadAccess { + object, + content_read, + }) + .collect(), + dependencies: precise.pop().unwrap().into_values().collect(), + }) } pub(crate) fn is_read_only(&self) -> bool { @@ -209,7 +280,45 @@ impl Heap { compute() } - pub(crate) fn record_read(&self, reference: ObjectRef) { + pub(crate) fn record_read(&self, reference: ObjectRef, version: u64) { + if self.read_only.is_empty() { + return; + } + self.record_observation(reference, ReadKey::Object(reference), || { + ReadDependency::Object { + object: reference, + version, + } + }); + } + pub(crate) fn record_dictionary_kind(&self, reference: ObjectRef) { + self.record_observation(reference, ReadKey::DictionaryKind(reference), || { + ReadDependency::DictionaryKind { object: reference } + }); + } + pub(crate) fn record_name( + &self, + reference: ObjectRef, + name: &str, + value: &Option, + ) { + if self.read_only.is_empty() { + return; + } + self.record_observation(reference, ReadKey::Name(reference, name.into()), || { + ReadDependency::Name { + object: reference, + name: name.into(), + value: value.clone(), + } + }); + } + fn record_observation( + &self, + reference: ObjectRef, + key: ReadKey, + build: impl Fn() -> ReadDependency, + ) { if self.read_only.is_empty() { return; } @@ -220,6 +329,20 @@ impl Heap { .contains_key(&std::thread::current().id()); for scope in &self.read_only { if scope.protected.contains(&reference) { + scope + .dependencies + .lock() + .expect("read tracker poisoned") + .entry(key.clone()) + .or_insert_with(&build); + for region in scope + .dependency_regions + .lock() + .expect("read tracker poisoned") + .iter_mut() + { + region.entry(key.clone()).or_insert_with(&build); + } scope .reads .lock() diff --git a/crates/objects/src/lib.rs b/crates/objects/src/lib.rs index 9535f73..2bcea71 100644 --- a/crates/objects/src/lib.rs +++ b/crates/objects/src/lib.rs @@ -33,7 +33,7 @@ pub use builtins::{ pub use changes::{ChangeBatch, ChangeKey, ObjectChange}; pub use code::{CodeLayout, EndorCode, EndorCodeObject, SourceSignature}; pub use error::VmError; -pub use heap::{ObjectRef, ReadAccess, WeakObjectRef}; +pub use heap::{ObjectRef, ReadAccess, ReadDependency, ReadSet, TrackedReads, WeakObjectRef}; pub use instruction::{DecodedInstruction, Instruction, Opcode}; pub use native::{ CallArity, CallEffect, CallSignature, CallType, ContextNativeFunction, NativeCallable, diff --git a/crates/objects/src/store.rs b/crates/objects/src/store.rs index 68219e4..359fd2d 100644 --- a/crates/objects/src/store.rs +++ b/crates/objects/src/store.rs @@ -43,6 +43,29 @@ impl ObjectStore { pub fn end_read_only(&mut self, scope: usize) -> Result, VmError> { self.heap.end_read_only(scope) } + pub fn end_read_only_precise(&mut self, scope: usize) -> Result { + self.heap.end_read_only_precise(scope) + } + pub fn read_dependency_valid(&self, dependency: &crate::ReadDependency) -> bool { + match dependency { + crate::ReadDependency::Object { object, version } => { + self.change_version(*object).ok() == Some(*version) + } + crate::ReadDependency::DictionaryKind { object } => { + self.dictionary_kind(*object).is_ok() + } + crate::ReadDependency::Name { + object, + name, + value, + } => self + .dict_get_name(*object, name) + .is_ok_and(|current| current == *value), + } + } + pub fn end_read_tracking_precise(&self, token: usize) -> Result { + self.heap.end_read_tracking_precise(token) + } /// Track one subcall inside the active read-only scope. Regions nest in /// LIFO order and do not clear the enclosing call's read dependencies. /// Handles returned by end_read_tracking are borrowed, never owning roots. @@ -214,8 +237,10 @@ impl ObjectStore { } pub fn object(&self, reference: ObjectRef) -> Result<&EndorObject, VmError> { - self.heap.record_read(reference); - self.heap.get(reference) + let object = self.heap.get(reference)?; + self.heap + .record_read(reference, object.header().change_version()); + Ok(object) } /// Borrowed storage path: depend on Contents for intermediate objects and diff --git a/crates/objects/src/store_builtins.rs b/crates/objects/src/store_builtins.rs index 84e30ab..d6ddcf8 100644 --- a/crates/objects/src/store_builtins.rs +++ b/crates/objects/src/store_builtins.rs @@ -20,9 +20,9 @@ impl ObjectStore { "function defaults or closure do not match code", )); } - self.payload::(function.globals(), "dict")?; + self.dictionary_kind(function.globals())?; if let Some(builtins) = function.builtins() { - self.payload::(builtins, "dict")?; + self.dictionary_kind(builtins)?; } for cell in function.closure() { self.payload::(*cell, "cell")?; @@ -67,10 +67,10 @@ impl ObjectStore { defaults: Vec, closure: Vec, ) -> Result { - self.payload::(globals, "dict")?; - let builtins = match self.dict_get(globals, &Value::Text("__builtins__".into()))? { + self.dictionary_kind(globals)?; + let builtins = match self.dict_get_name(globals, "__builtins__")? { Some(Value::Object(reference)) => { - self.payload::(reference, "dict")?; + self.dictionary_kind(reference)?; Some(reference) } None => None, @@ -101,6 +101,35 @@ impl ObjectStore { .entries()) } + pub(crate) fn dictionary_kind( + &self, + dictionary: ObjectRef, + ) -> Result<&EndorDictObject, VmError> { + let dict = self + .heap + .get(dictionary)? + .payload() + .as_any() + .downcast_ref::() + .ok_or(VmError::ObjectKindMismatch("dict"))?; + self.heap.record_dictionary_kind(dictionary); + Ok(dict) + } + /// Name lookup records both present and missing text keys. General dict_get + /// and iteration continue to observe the entire dictionary contents. + pub fn dict_get_name( + &self, + dictionary: ObjectRef, + name: &str, + ) -> Result, VmError> { + let dict = self.dictionary_kind(dictionary)?; + let value = self + .dict_position(dict, &Value::Text(name.into()))? + .map(|index| dict.entries[index].1.clone()); + self.heap.record_name(dictionary, name, &value); + Ok(value) + } + pub fn dict_get(&self, dictionary: ObjectRef, key: &Value) -> Result, VmError> { let dict = self.payload::(dictionary, "dict")?; self.key_hash(key)?; diff --git a/crates/objects/src/types.rs b/crates/objects/src/types.rs index 4d64c90..2232e09 100644 --- a/crates/objects/src/types.rs +++ b/crates/objects/src/types.rs @@ -1266,9 +1266,9 @@ impl ObjectStore { return Ok(primitive); } let value = self - .dict_get(data.globals(), &Value::Text(name.into()))? + .dict_get_name(data.globals(), name)? .or(match data.builtins() { - Some(builtins) => self.dict_get(builtins, &Value::Text(name.into()))?, + Some(builtins) => self.dict_get_name(builtins, name)?, None => None, }); if let Some(Value::Object(definition)) = value {