feat: track borrowed name-level dependencies across read-only calls

This commit is contained in:
2026-10-24 01:39:49 +08:00
parent 89b817447e
commit a775d5c827
9 changed files with 221 additions and 42 deletions
+8 -7
View File
@@ -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)?;
+1
View File
@@ -103,6 +103,7 @@ pub enum ExecutionProgress {
Complete {
value: Value,
reads: Vec<ObjectRef>,
dependencies: Vec<crate::ReadDependency>,
instructions: usize,
},
}
+2 -2
View File
@@ -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;
+3 -3
View File
@@ -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));
}
+143 -20
View File
@@ -41,6 +41,8 @@ struct ReadOnly {
protected: BTreeSet<ObjectRef>,
reads: Mutex<BTreeSet<ObjectRef>>,
regions: Mutex<Vec<BTreeMap<ObjectRef, bool>>>,
dependencies: Mutex<BTreeMap<ReadKey, ReadDependency>>,
dependency_regions: Mutex<Vec<BTreeMap<ReadKey, ReadDependency>>>,
}
#[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<crate::Value>,
},
}
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<ReadAccess>,
pub dependencies: Vec<ReadDependency>,
}
#[derive(Debug)]
pub struct ReadSet {
pub objects: Vec<ObjectRef>,
pub dependencies: Vec<ReadDependency>,
}
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<Vec<ObjectRef>, VmError> {
Ok(self.end_read_only_precise(scope)?.objects)
}
pub(crate) fn end_read_only_precise(&mut self, scope: usize) -> Result<ReadSet, VmError> {
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<usize, VmError> {
@@ -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<Vec<ReadAccess>, VmError> {
Ok(self.end_read_tracking_precise(token)?.accesses)
}
pub(crate) fn end_read_tracking_precise(&self, token: usize) -> Result<TrackedReads, VmError> {
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<crate::Value>,
) {
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()
+1 -1
View File
@@ -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,
+27 -2
View File
@@ -43,6 +43,29 @@ impl ObjectStore {
pub fn end_read_only(&mut self, scope: usize) -> Result<Vec<ObjectRef>, VmError> {
self.heap.end_read_only(scope)
}
pub fn end_read_only_precise(&mut self, scope: usize) -> Result<crate::ReadSet, VmError> {
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<crate::TrackedReads, VmError> {
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
+34 -5
View File
@@ -20,9 +20,9 @@ impl ObjectStore {
"function defaults or closure do not match code",
));
}
self.payload::<EndorDictObject>(function.globals(), "dict")?;
self.dictionary_kind(function.globals())?;
if let Some(builtins) = function.builtins() {
self.payload::<EndorDictObject>(builtins, "dict")?;
self.dictionary_kind(builtins)?;
}
for cell in function.closure() {
self.payload::<EndorCellObject>(*cell, "cell")?;
@@ -67,10 +67,10 @@ impl ObjectStore {
defaults: Vec<Value>,
closure: Vec<ObjectRef>,
) -> Result<ObjectRef, VmError> {
self.payload::<EndorDictObject>(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::<EndorDictObject>(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::<EndorDictObject>()
.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<Option<Value>, 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<Option<Value>, VmError> {
let dict = self.payload::<EndorDictObject>(dictionary, "dict")?;
self.key_hash(key)?;
+2 -2
View File
@@ -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 {