diff --git a/src/render/source_layout.rs b/src/render/source_layout.rs index db8f519..8191875 100644 --- a/src/render/source_layout.rs +++ b/src/render/source_layout.rs @@ -4,6 +4,11 @@ //! //! Bottom-up measurement and top-down arrangement share one read-only VM call. //! The host scratch plan contains derived data only, never a second business heap. +//! +//! ui_bridge::begin_layout_frame 先建立临时 Mount 上下文和检查点,再调用 start;本模块只拥有求解草稿与 VM 续体。 +//! Prepare 建立循环,Advance 依次安排 Step → 源方法/原生结果 → Collect,整轮结束由 Settle 决定继续还是停止。 +//! Progress::Complete 只证明本轮布局驱动完成,桥接层仍需投影、提交几何及生命周期,运行时还需绘制并验证全窗口稳定。 +//! Work::cancel 结束只读调用并释放入口根;恢复 Mount 检查点属于桥接层,不能把本模块取消误认为业务状态回滚。 use crate::{ document::Frame, layout::{self, LayoutRuntime, Size}, @@ -289,6 +294,7 @@ impl NativeCallable for Step { // 先安装本轮节点、子测量和临时 bounds,让源代码调用基类或 Mount.bounds 时观察当前阶段数据。 let context = mount.layout.ok_or(invalid("missing pre-layout context"))?; { + // 只更新当前出现的临时协议输入;不写 Plan 的兄弟位置,也不修改共享 Block 的业务属性。 let mut context = context .lock() .map_err(|_| invalid("layout context poisoned"))?; @@ -448,6 +454,7 @@ impl NativeCallable for Collect { plan.measured_limits[self.index][0] = Some(size.width); } if !native.is_some_and(|native| native.height == size.height) { + // 高度独立比较,源方法只改高时也要保留纵向内容上限,不能只根据宽度是否不同决定两轴。 plan.measured_limits[self.index][1] = Some(size.height); } } @@ -465,9 +472,11 @@ impl NativeCallable for Collect { if let Some((pending, reads)) = tracked { // 若观察了 Mount 内容/几何等不稳定宿主状态,无法证明跨轮可复用,保守地删除该测量缓存。 let mut volatile = false; + // 类型字典用于协议名义检查,不是源业务状态;它和 desktop 都要从易变性判断与缓存依赖中排除。 let registry = desktop_type_registry(cx, self.desktop)?; // accesses 提供内容读取细节,dependencies 则用于后面保留可校验依赖,两者用途不同。 for read in &reads.accesses { + // access 记录哪个对象被怎样读取,后面仅对真实 Mount 内容读取禁用缓存,不因普通身份比较一概禁用。 let object = read.object; // 当前桌面和类型注册表属于宿主内部检查,跳过这类访问而不把它们认成易变业务输入。 if object == self.desktop || object == registry { @@ -477,6 +486,7 @@ impl NativeCallable for Collect { // source. Object-granular tracking cannot prove those // accesses stable, so conservatively bypass its cache. if cx.store().object(object)?.payload().as_any().is::() { + // Mount 身份可用于查父关系,但内容读取可能涉及本轮派生几何;content_read 才决定此次测量是否易变。 volatile |= read.content_read; continue; } @@ -524,6 +534,7 @@ impl NativeCallable for Collect { return Err(invalid("Arrangement.children must be an ObjectList")); }; let items = object_list::items(cx.store(), list)?; + // 固定当前父出现的直接孩子列表,源结果按 slot 查回这个列表,再转换为全局出现下标。 let children = plan.children[self.index].clone(); // 安排结果必须覆盖全部孩子,不能省略零尺寸项;结合下面 slot 唯一检查保证逐一对应。 if items.len() != children.len() { @@ -541,6 +552,7 @@ impl NativeCallable for Collect { .collect(); // 记录已安排 slot 防止重复,项数相等加逐项唯一性共同保证每个孩子恰好分配一次。 let mut assigned = BTreeSet::new(); + // 列表每项都需满足 Bounds 名义类型,不能只有 x/y/width/height 同名字段就当作合法安排。 let expected = desktop_type(cx.store(), "Bounds", self.desktop)?; // 源返回列表可以重新排序,实际孩子用 slot 查表确定,不按列表位置直接对应。 for item in items { @@ -549,6 +561,7 @@ impl NativeCallable for Collect { return Err(invalid("Arrangement child must be Bounds")); } let Value::Number(slot) = cx.store().get_attr(*item, "slot")? else { + // Bounds 名义类型合法仍不足以接受任意 slot 值,缺失或非 Number 要在建立孩子映射之前拒绝。 return Err(invalid("Arrangement child requires a slot")); }; // slot 先要求可转为 i64 整数,再拒绝负值转为 u64;超出 i64 范围的值也不能作为此处身份。 @@ -596,6 +609,7 @@ impl NativeCallable for Collect { plan.allocations[child][0] = Some(bounds[2]); } if bounds[3] != plan.sizes[child].height { + // 高度差异单独形成精确纵向分配,和上面的宽度约束可同时存在,也可只约束其中一轴。 plan.allocations[child][1] = Some(bounds[3]); } } @@ -631,6 +645,7 @@ impl NativeCallable for Settle { let mut next = plan.constraints.clone(); // 按父先子后传播下一轮约束,next[parent] 先确定后才推导直接孩子。 for index in 0..plan.nodes.len() { + // 克隆直接孩子索引以结束对 plan.children 的借用,随后要在同一 Plan 内更新约束与显式分配。 let children = plan.children[index].clone(); let [_, _, width, height] = plan.bounds[index].ok_or(invalid("unarranged occurrence"))?; @@ -641,6 +656,7 @@ impl NativeCallable for Settle { available[2] = width; } if let Some(height) = plan.measured_limits[index][1] { + // 源测量给出的纵向内容限制只覆盖最大高,最小约束仍从 next 继承,再由孩子约束策略统一处理。 available[3] = height; } // 父级实际分配与测量不同,孩子的可用宽度应跟随分配,否则换行仍按旧宽度计算。 @@ -648,6 +664,7 @@ impl NativeCallable for Settle { available[2] = width; } if height != plan.sizes[index].height { + // 最终分配高度与测高不同,则孩子预算跟随实际分配;不把此值写回业务对象 height。 available[3] = height; } // 根据父盒模型与实际宽高计算孩子公共约束基础,随后再考虑各个出现的独立尺寸规则。 @@ -666,6 +683,7 @@ impl NativeCallable for Settle { Size { width, height }, child_index, ); + // 从自然约束起步,再逐轴套入显式精确分配;natural 留作原生 flex 分配失效时恢复基线。 let mut constraints = natural; // 显式轴分配同时设置最小与最大,区别于仅给孩子一个可自由缩小的最大可用宽度。 if let Some(width) = plan.allocations[child][0] { @@ -673,11 +691,13 @@ impl NativeCallable for Settle { constraints[2] = width; } if let Some(height) = plan.allocations[child][1] { + // 显式高同时成为 minHeight/maxHeight;这比仅给上限更强,使下一轮按安排分配重新测量。 constraints[1] = height; constraints[3] = height; } // 线性布局才有主/交叉轴;交叉轴范围变化可触发文字重排,进而改变主轴自然尺寸。 if let Some(main) = layout::main_axis(&plan.nodes[index]) { + // 轴索引只有 0/1;Row 主轴横向、Column 主轴纵向,所以用 1-main 得到另一轴而非孩子顺序。 let cross = 1 - main; // 交叉轴变化可能改变换行及主轴自然高度,清 preferred 让其重新测量;原生派生主轴分配也需暂时撤销。 if constraints[cross] != plan.constraints[child][cross] @@ -752,6 +772,7 @@ impl NativeCallable for Settle { for weak in self.mounts.iter() { let mount = cx.upgrade(*weak)?.ok_or(invalid("layout Mount expired"))?; if let Some(context) = &cx.store().payload::(mount, "Mount")?.layout { + // 仍有临时上下文才清 bounds;其余输入留给下一轮 Step 更新,不在这里调用源 measure。 context .lock() .map_err(|_| invalid("layout context poisoned"))? @@ -788,6 +809,7 @@ impl NativeCallable for Prepare { }, vec![self.desktop], ))?; + // 固定循环以 Advance 返回的 Bool 决定继续,不为每个节点展开另一份巨大的驱动字节码。 let function = crate::vm_sequence::repeat_call(cx, advance, "source layout", self.desktop)?; Ok(NativeResult::Invoke { function, @@ -836,6 +858,7 @@ impl NativeCallable for Advance { } .invoke(cx, &[])?; let NativeResult::Return(Value::Bool(repeat)) = settled else { + // Settle 是宿主内部零参计算,必须同步返回 Bool;其他结果不能误作“已经稳定”而提前交付。 return Err(invalid("invalid settling result")); }; // 需要继续时重置同一个游标,后续仍回到原 repeat_call 帧执行整轮;false 则结束布局。 @@ -933,6 +956,7 @@ impl Work { /// 恢复当前 Work 拥有的 VM 续体,继续同一轮求解而不是重放 measure/arrange。 // ui_bridge::resume_layout_frame 调用,沿原只读续体继续;slices 统计包含 start 的第一片。 pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result { + // resume 消费原 Work,计数包含最初 start;非法量子/恢复错误同样交 accept 取消原只读续体,不重建求解。 self.slices += 1; let progress = vm.resume_read_only_call(slice); self.accept(vm, progress) @@ -963,12 +987,14 @@ impl Work { dependencies, .. }) => { + // VM 调用域已经完成,先释放其入口根;finish 消费同一个 Work,并只读取最终 Plan 汇总布局草稿。 vm.release(self.prepare)?; finish(vm, self, value, dependencies) } Err(error) => { // An invalid quantum can fail without discarding the VM's // continuation. No failed job may strand a read-only scope. + // 在取消调用可能清理 VM 执行上下文之前提取原始 traceback,避免最后只报告清理阶段位置。 let diagnostic = crate::endor_host::runtime_error(vm, error); self.cancel(vm)?; Err(diagnostic) @@ -996,6 +1022,7 @@ pub fn start( // 分字段借用草稿以准备本轮,cache 命中统计重置但条目先保留供下方筛选。 let frame = &mut state.frame; let runtime = &mut state.runtime; + // 与 frame/runtime 借用同一 Draft 的独立字段,重置的是本轮源测量统计,不是清掉全部仍可复用的条目。 let cache = &mut state.cache; cache.hits = 0; cache.misses = 0; @@ -1008,12 +1035,14 @@ pub fn start( // 扫描每个出现的实际 measure/arrange,实现全部为原生还不够,下面 flex/拉伸也会要求收敛驱动。 let mut custom = false; for weak in &mounts { + // 扫描只为选择驱动路径,临时升级保证方法身份检查期间 Mount 不被回收;下面无论检查结果如何都 release。 let mount = vm .upgrade(*weak)? .ok_or_else(|| anyhow::anyhow!("layout Mount expired"))?; // 方法检查错误先保存为 Result,下面总会尝试 release 临时升级的 Mount,再传播检查结果。 let result = (|| { let block = vm.payload::(mount, "Mount")?.block; + // 从真实业务对象取得类型,不能使用 UiNode 种类推断源覆盖是否存在。 let definition = vm .object_store() .type_of(block)? @@ -1024,6 +1053,7 @@ pub fn start( return Ok::<_, VmError>(true); }; if !crate::native_visual::is_intrinsic(vm.object_store(), method, name, desktop)? { + // 任一协议为源覆盖就需要 VM 驱动,不能因另一协议仍是原生而整体走直接布局。 return Ok(true); } } diff --git a/src/runtime/endor_host.rs b/src/runtime/endor_host.rs index 0561f05..be3c12a 100644 --- a/src/runtime/endor_host.rs +++ b/src/runtime/endor_host.rs @@ -6,6 +6,10 @@ //! UI 类型与刷新由 bridge 适配;此模块负责源码预算、真实会话、执行结果和挂起刷新的生命周期。 //! compile/evaluate 是一次性入口,关闭 Candidate 后只返回纯数据;生产持久路径使用增量入口并保存续体。 //! 本模块仍直接依赖 UI Frame/桥接,不是已经可以独立搬走的通用 Nexus runtime,迁移需先抽出宿主接口。 +//! +//! 增量刷新有两层“继续”:ReplSession 保存全窗口 RefreshWork,bridge 保存当前窗口的布局或绘制续体。 +//! API worker 下一量子恢复前者,step_refresh 再选择同一窗口恢复后者;两层都不重放已经执行的 Console/事件副作用。 +//! 尺寸目标确实变化才由 retarget_refresh 取消当前视觉续体并重启稳定轮;取消派生几何不会撤销已发生的业务写入。 use anyhow::{Result, bail}; use endor_core::{ExecutionLimits, ObjectRef, Value, Vm, VmError}; use frontend::{ @@ -241,6 +245,7 @@ fn compile_plan_with_globals( assigned(s, globals); } if let Some(other) = &b.otherwise { + // else 中的潜在绑定也要让后续片段可编译引用,但这里不预先在 globals 字典创建实际值。 for s in &other.statements { assigned(s, globals); } @@ -426,6 +431,7 @@ impl ReplSession { viewport: Option, ) -> Result { if source.len() > MAX_SOURCE_BYTES { + // 完整 PUT 候选在创建 VM 前限制原始 UTF-8 字节数,解析/代码生成尚未开始。 bail!("source exceeds the 1 MiB limit"); } // 来源标注把 API namespace/提交号接入 Aster 错误来源,不改变用户原文。 @@ -573,6 +579,7 @@ impl ReplSession { ); } if source.len() > MAX_SOURCE_BYTES { + // 单次 Console 输入也受原始字节预算约束,不能仅靠下面累计 transcript 预算替代。 bail!("input exceeds 1 MiB"); } // 持久 Console 限制累计输入次数、源码文本和稍后的 AST 总节点数,用户需显式重置才能重新获得预算。 @@ -592,6 +599,7 @@ impl ReplSession { budget.visit_ast_root(ast)?; } if self.ast_nodes + budget.nodes > MAX_AST_NODES { + // 此处限制整个持久会话累计结构量;超限发生在记输入号、追加源码和执行用户语句之前。 bail!("console session exceeds 10000 AST nodes; reset the session to continue"); } // new 已建立的字典继续原地使用,保存对象身份和闭包,不能通过历史源码重放重建它。 @@ -633,6 +641,7 @@ impl ReplSession { .push_str(&format!("// Console input {}\n", self.inputs)); self.source.push_str(source.text()); if !self.source.ends_with('\n') { + // transcript 的尾部补换行只服务下一条输入的浏览行号,当前执行仍使用独立 SourceUnit 原文。 self.source.push('\n'); } // 本轮输入的代码树先使用局部行号,再用 offset 平移到累计源码中的对应位置。 @@ -715,6 +724,7 @@ impl ReplSession { self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; if let Some(node) = self.nodes.last_mut() { + // 标记本次已记录的输入节点,保留先前 Sources 记录,不把运行失败误记为从未执行。 node.name .push_str(" (error; earlier statements may remain live)"); } @@ -825,7 +835,7 @@ impl ReplSession { Err(error) => { self.faulted = true; // 错误时必须显式取消 paint/layout 的 owning VM 续体并恢复检查点,不能仅丢弃 Rust 工作结构。 - // 进入 paint 后只恢复其原续体和配对文档,不重复布局,也不重跑输入业务语句。 + // 这里是失败收尾,取消已有 paint 并丢弃配对草稿;只有正常下一量子的 step_refresh 才会恢复 paint。 if let Some(paint) = work.paint.take() { self.bridge.cancel_paint_frame( &mut self.candidate.vm, @@ -834,6 +844,7 @@ impl ReplSession { )?; } if let Some(pending) = work.pending.take() { + // 若失败发生在布局阶段,取消这份原窗口检查点配套的 Work;不会恢复执行源语句或发布其草稿。 self.bridge .abort_layout_frame(&mut self.candidate.vm, *pending)?; } @@ -1057,6 +1068,7 @@ impl ReplSession { return Err(error); } if let Err(error) = self.bridge.note_refresh_restart(cancelled, cancelled_paint) { + // 统计写入失败也表示桥接状态不可可靠访问,不能保留看似可续跑的刷新并继续执行源程序。 self.faulted = true; return Err(error); } @@ -1110,6 +1122,7 @@ impl ReplSession { let mut changed = false; // 仅更新桥接仍注册的窗口;旧 GUI 尺寸记录不应在新域里隐式创建窗口资源。 for (id, viewport) in viewports { + // viewports 来自宿主尺寸记录,只有桥接当前仍注册的 ID 才有权改输入;其余记录不会分配新窗口。 if ids.contains(id) { // 每次选明确窗口,使 ui.viewport 等隐式上下文读到对应窗口的尺寸输入。 self.bridge.select_window(*id)?; @@ -1268,6 +1281,7 @@ impl ReplSession { )?; } if let Some(pending) = work.pending.take() { + // 与 paint 清理互斥的布局续体还带 Mount 检查点,交桥接层完整取消而非直接丢弃 Rust 句柄。 self.bridge .cancel_layout_frame(&mut self.candidate.vm, *pending)?; } @@ -1344,6 +1358,7 @@ impl ReplSession { bail!("event belongs to another runtime session"); } if !self.accepts_tab_drop(input) { + // 拖放还需验证目标端最新页面序列,身份不符时不先执行源 moveToWindow 再尝试修复位置。 bail!("drop targets an obsolete presentation"); } // 将 ui.window/viewport 等隐式上下文切到事件所属窗口,分发后统一恢复主窗口。 @@ -1373,6 +1388,7 @@ impl ReplSession { self.bridge.discard_pending(&mut self.candidate.vm)?; } if !self.has_pending_refresh() { + // 分派后的只读刷新若仍挂起,临时根归原调用持有;只有空闲边界才回收,不干扰下一量子恢复。 self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; }