Files
endorphin-tessera-gui/src/model/binding.rs
T

238 lines
10 KiB
Rust

//! 属性绑定:在只读计算中收集依赖,将结果写回绑定目标。
//! 缓存记录读取版本而不是复制业务模型;依赖变化才重新计算。托管连接对象追踪其拥有的引用。
//!
//! Target-owned managed bindings. Dependency versions are borrowed identities;
//! the host's weak Block registry never roots an otherwise unreachable graph.
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use anyhow::{Result, bail};
use endor_core::{NativeContext, ObjectPayload, ObjectRef, Value, Vm, VmError, WeakObjectRef};
use crate::{block, event_dispatch::Subscription, ui_bridge::attach_desktop_type};
/// The output value is borrowed for equality only, including object handles.
/// It never owns a reference or dereferences a possibly reclaimed cached object.
#[derive(Debug, Clone)]
/// 上次绑定计算的读取观察,用于判断是否需要重新执行源计算。
struct Cached {
// 每项保存读取对象/属性的版本观察;刷新时向对象存储核对,而非复制业务值。
reads: Vec<endor_core::ReadDependency>,
// 只借用上次结果的身份做相等比较;不能解引用其中可能已被回收的对象。
output: Value,
}
#[derive(Debug, Clone)]
/// 托管属性连接,强持有计算/目标所需引用,并保留只读计算的版本缓存。
pub struct Binding {
// 目标属性名;目标 Block 通过 bindings 列表拥有本连接。
field: String,
// Some 表示连接有效且拥有计算函数;取消/替换后置 None,让 trace 停止保活函数。
pub compute: Option<ObjectRef>,
// payload 克隆共享同一缓存,刷新局部副本也能更新缓存;首次计算前为 None。
reads: Arc<Mutex<Option<Cached>>>,
}
impl ObjectPayload for Binding {
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
fn reference_assignment_only(&self) -> bool {
true
}
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
fn as_any(&self) -> &dyn std::any::Any {
self
}
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
fn kind_name(&self) -> &'static str {
"tessera-binding"
}
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
if let Some(compute) = self.compute {
visit(compute);
}
}
}
/// 检查绑定目标与可调用计算,建立托管连接及其引用关系。
pub fn connect(
cx: &mut NativeContext<'_>,
target: ObjectRef,
field: &str,
compute: ObjectRef,
desktop: ObjectRef,
) -> Result<ObjectRef, VmError> {
// 在触碰连接列表前拒绝非法名称,尤其禁止绑定运行时类型属性。
if field.is_empty() || field.len() > 128 || field == "type" {
return Err(VmError::InvalidArgument("invalid binding property"));
}
let (reference, data) = block::resolve(cx.store(), target)?;
let mut data = data.clone();
// 只允许属性存储里已存在的字段;方法名即使可读取,也不能作为绑定写入目标。
let properties = data
.properties
.ok_or(VmError::InvalidArgument("missing property storage"))?;
cx.store().get_attr(properties, field)?;
if !cx.store().object(compute)?.payload().is_function() {
return Err(VmError::InvalidArgument(
"binding compute must be a Function",
));
}
// 刷新统一以零参数调用 compute,因此必须有参数元数据并且接受零实参。
if !cx
.store()
.call_arity(compute)?
.ok_or(VmError::InvalidArgument(
"binding compute requires positional metadata",
))?
.accepts(0)
{
return Err(VmError::InvalidArgument(
"binding compute requires zero unbound parameters",
));
}
// 同一字段只保留一个有效计算。其他字段连接沿用;旧同字段连接失效并清缓存,
// 使外部仍持有的 Subscription 不会继续驱动被替换的计算。
let mut retained = vec![];
for connection in &data.bindings {
let mut binding = cx
.store()
.payload::<Binding>(*connection, "Binding")?
.clone();
if binding.compute.is_some() && binding.field != field {
retained.push(*connection);
} else if binding.compute.is_some() {
binding.compute = None;
*binding
.reads
.lock()
.map_err(|_| VmError::InvalidArgument("binding cache poisoned"))? = None;
cx.replace_payload(*connection, binding)?;
}
}
if retained.len() >= 128 {
return Err(VmError::ExecutionLimit("128 bindings per Block"));
}
let connection = cx.allocate(Binding {
field: field.into(),
compute: Some(compute),
reads: Arc::new(Mutex::new(None)),
})?;
retained.push(connection);
data.bindings = retained;
// text 改由通用绑定负责后,清除旧 textOf 推导入口,避免两套机制竞争同一属性。
if field == "text" {
data.text_binding = None;
}
cx.replace_payload(reference, data)?;
// 返回的取消令牌仅弱引用连接;连接生命期由目标 Block 的 bindings 决定。
let token = cx.allocate(Subscription {
connection: cx.store().downgrade(connection)?,
})?;
attach_desktop_type(cx, token, "Subscription", desktop)?;
Ok(token)
}
/// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。
pub fn refresh(
vm: &mut Vm,
nodes: &[WeakObjectRef],
host_desktops: &HashSet<ObjectRef>,
) -> Result<usize> {
// work 跨所有轮次累计。属性 A 的写入可能使已检查的 B 失效,所以不能只扫描一次。
let mut work = 0;
loop {
let mut evaluated = false;
// 每轮按解析后的真实 Block 身份去重,多个包装对象共享数据时只处理一次。
let mut seen = HashSet::new();
for weak in nodes {
let Some(target) = vm.upgrade(*weak)? else {
continue;
};
// 用闭包暂存失败,让下方 release 无论处理成功与否都能释放 upgrade 的强引用。
let result: Result<()> = (|| {
let (reference, data) = block::resolve(vm.object_store(), target)?;
if !seen.insert(reference) {
return Ok(());
}
let connections = data.bindings.clone();
for connection in connections {
let binding = vm.payload::<Binding>(connection, "Binding")?.clone();
let Some(compute) = binding.compute else {
continue;
};
let cached = binding
.reads
.lock()
.map_err(|_| anyhow::anyhow!("binding cache poisoned"))?
.clone();
// 两项都成立才能跳过:读取依赖版本未变,且目标仍等于上次输出。
// 后一项让外部直接改写目标后,绑定仍能恢复其计算值。
if cached.as_ref().is_some_and(|cached| {
cached
.reads
.iter()
.all(|read| vm.object_store().read_dependency_valid(read))
&& vm
.get_attr(target, &binding.field)
.is_ok_and(|value| value == cached.output)
}) {
continue;
}
// 防止相互写入的绑定持续振荡;预算跨轮累计,超限向宿主报告不稳定。
work += 1;
if work > 10_000 {
bail!("property bindings did not stabilize within the execution budget");
}
evaluated = true;
// 计算阶段禁止写入,精确记录本次实际读取;单次调用另设帧数与指令上限。
let (value, reads) = vm
.call_read_only_precise_with_limits(
compute,
&[],
endor_core::ExecutionLimits {
max_frames: 128,
max_instructions: 100_000,
},
)
.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
let applied: Result<()> = (|| {
// 宿主桌面对象不纳入此缓存依赖;保留业务读取,避免宿主发布状态驱动重算。
let reads = reads
.dependencies
.into_iter()
.filter(|read| !host_desktops.contains(&read.object()))
.collect();
let (_, data) = block::resolve(vm.object_store(), target)?;
// 先在数据副本上验证属性类型/范围,验证通过后才写回托管属性。
block::apply_visual_property(&mut data.clone(), &binding.field, &value)?;
if vm.get_attr(target, &binding.field)? != value {
vm.set_attr(target, &binding.field, value.clone())?;
}
*binding
.reads
.lock()
.map_err(|_| anyhow::anyhow!("binding cache poisoned"))? =
Some(Cached {
reads,
output: value.clone(),
});
Ok(())
})();
// 缓存不拥有输出对象;释放调用返回值后再传播应用错误,避免失败路径漏引用。
vm.release_value(value)?;
applied?;
}
Ok(())
})();
vm.release(target)?;
result?;
}
// 整轮无需计算才达到固定点;即使某次计算输出未变,也再扫描一轮确认依赖有效。
if !evaluated {
return Ok(work);
}
}
}