feat: track borrowed name-level dependencies across read-only calls
This commit is contained in:
@@ -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)?;
|
||||
|
||||
@@ -103,6 +103,7 @@ pub enum ExecutionProgress {
|
||||
Complete {
|
||||
value: Value,
|
||||
reads: Vec<ObjectRef>,
|
||||
dependencies: Vec<crate::ReadDependency>,
|
||||
instructions: usize,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user