diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 28329a6..ac91802 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -2,6 +2,9 @@ //! HTTP 线程不操作 VM;共享 NamespaceState 只保存最近完整发布文档和资源命令。提交序号用来拒绝已被较新 PUT/DELETE 取代的结果。 //! 关闭流程先请求运行时有界清理,再通过完成通知让原生事件循环退出。 //! +//! main::main 调用 start,shell 持有 RuntimeControl;HTTP 请求经 handle/submit 入队,原生输入经 dispatch_native 入同一队列。 +//! worker 子模块拥有实际 ReplSession;本文件的线程循环只决定继续哪个请求、何时等待、如何响应和退出。 +//! 本地端点、观察器和 EventLoopProxy 都是 Tessera 宿主适配,不能把整个文件直接作为无 GUI 的 Nexus 核心。 mod guide; mod worker; @@ -34,34 +37,47 @@ use crate::{ endor_host, }; -// 只监听 IPv4 回环固定端口,接口用于本进程原生输入和本机 agent,不是对外网络服务器。 +// 只监听 IPv4 回环固定端口,供本机 agent/Console HTTP 调用;原生输入现在直接走同一 worker 队列。 pub const API_ADDRESS: &str = "127.0.0.1:4737"; +// 请求流读取与 endor_host 的源预算一致,Content-Length 检查不能替代流上的硬上限。 const MAX_CODE_SIZE: u64 = endor_host::MAX_SOURCE_BYTES as u64; +// GUI 更新回调只投递原生通知,不携带 VM/托管引用,也不在调用线程直接绘制窗口。 type Notify = Arc; #[derive(Clone, Debug)] /// 运行时清理的摘要与可选错误,供最终原生退出结果报告。 pub struct ShutdownOutcome { + // worker 生成的清理数量/故障摘要,供退出日志与 shell 最终报告使用。 pub summary: String, + // 清理失败时保留文本,不能因为退出事件已经发出就把失败说成正常关闭。 pub error: Option, } +// 完成槽和条件变量共享同一生命周期,退出发起者可有限等待或由 GUI ShutdownComplete 事件唤醒。 type ShutdownCompletion = Arc<(Mutex>, Condvar)>; #[derive(Clone)] /// 跨线程的退出控制句柄,使用有界通知和完成条件变量,不携带 VM。 pub struct RuntimeControl { + // 独立容量一的关闭通道,即使正常输入队列满了也能通知 worker 优先清理。 sender: SyncSender<()>, + // 合并重复退出请求,并让 HTTP/原生输入尽早拒绝新执行工作。 requested: Arc, + // worker 在退出收尾后填写结果,wait_shutdown 不在 GUI 内自行执行清理。 completion: ShutdownCompletion, + // start_server_observed 一次性装入共享执行队列,不能让每个原生窗口自行创建另一个 VM worker。 native_sender: Arc>>, + // 只读当前 namespace/submission 以给原生 job 盖身份,不包含 VM 对象图。 namespace: Arc>, } impl RuntimeControl { - /// Only the native input relay calls this blocking acknowledgement path. - /// Window threads still use a bounded, nonblocking Input sender. + /// events::Client 的后台 relay 调用,向共享 worker 队列发送原生信封并等回执;GUI 仍只非阻塞发给 Client。 pub(crate) fn dispatch_native(&self, input: crate::events::Input) -> Result<()> { + // 检查退出门闩后才创建回执/工作,不让已关闭域继续排入源回调。 anyhow::ensure!(!self.is_exiting(), "application is exiting"); + // 一个输入一个回执,relay 等待不会阻塞 GUI;容量一足以让 worker 写回后继续自身循环。 let (done, completed) = sync_channel(1); + // 短读锁只复制当前接受身份,发送/等待都在锁外,worker 发布时仍可取得写锁。 let job = { + // 原生来源由构造类型决定,HTTP 头或 JSON 不能伪造 RequestReply::Native。 let state = self .namespace .read() @@ -86,7 +102,7 @@ impl RuntimeControl { .map_err(anyhow::Error::msg) } - /// 只发出一次退出请求,唤醒 worker 执行清理,并通过完成句柄通知原生循环。 + /// shell 关闭流程调用,只通知一次,由 owning worker 清理 VM;true 表示已排入或此前已请求,不等于已清理成功。 pub fn request_shutdown(&self) -> bool { // 原子标志合并重复退出请求;已请求直接返回 true,不在 GUI 线程重复排队清理。 if self.requested.swap(true, Ordering::AcqRel) { @@ -100,14 +116,17 @@ impl RuntimeControl { false } } - /// 读取退出标志,使窗口事件和新输入尽早停止进入运行时。 + /// shell/原生 relay 查询退出请求状态,不读取完成槽,也不能由此推断资源已全部清理。 pub fn is_exiting(&self) -> bool { self.requested.load(Ordering::Acquire) } - /// 在明确超时内等待 worker 的清理结果,不能无限阻塞退出。 + /// shell 最终退出收尾调用,有限等待 worker 保存的 ShutdownOutcome;None 为未取得结果,不是成功。 pub fn wait_shutdown(&self, timeout: std::time::Duration) -> Option { + // 同一锁保护可选结果与等待谓词,避免“检查为空后错过通知”的竞争。 let (lock, ready) = &*self.completion; + // 锁异常直接返回 None,不在未知完成状态下继续阻塞。 let result = lock.lock().ok()?; + // wait_timeout_while 反复检查 Option,返回前重新拿锁,克隆纯文本结果交还 shell。 let (result, _) = ready // 条件变量处理虚假唤醒,直到完成槽非空或超时;返回 None 也可能来自锁中毒,不能当作清理成功。 .wait_timeout_while(result, timeout, |result| result.is_none()) @@ -117,38 +136,53 @@ impl RuntimeControl { } /// worker 持有的关闭接收端、控制状态和 GUI 完成通知代理。 struct Shutdown { + // worker 每个控制边界 try_recv,关闭信号优先于下一片 VM 刷新或下一条普通输入。 receiver: Receiver<()>, + // 保存完成槽及已请求门闩,与 shell 手中的句柄是同一共享状态。 control: RuntimeControl, + // 完成后向原生事件循环发消息,worker 不直接调用窗口退出/绘制 API。 proxy: EventLoopProxy, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// 送入执行队列的请求类别,区分完整源码、持久输入、事件与重置。 enum RequestKind { + // 原生或 HTTP 输入,均由已有运行时执行,不启动新的程序堆。 Event, + // 完整程序 PUT:候选成功后才替换旧域和发布文档。 Code, + // 隔离表达式求值,不写活程序或 Console 的 globals。 Expression, + // 在真实持久 globals 上执行一轮交互,运行错误不回滚此前成功语句。 Repl, + // 显式清理/替换 Console 上下文,与完整 DELETE/code 的文档清空不同。 ResetRepl, } -/// 保留原 HTTP 请求与提交身份的执行工作,完成或失败后由 worker 回应。 +/// submit/dispatch_native 产生的纯数据工作信封,保留回应端和接受身份,worker 完成后才回应或确认过期。 struct BuildRequest { + // 原 HTTP 请求的观察包装或原生一次性回执,不能 Clone 成多次响应。 request: RequestReply, + // 原生来源只由进程内构造,HTTP 输入的事件 JSON 仍保存在 source 中由 worker 解码。 native: Option, + // 执行语义标签,不从请求体猜是替换程序还是追加 Console。 kind: RequestKind, + // 已限制字节数并验证 UTF-8 的正文;原生事件无需字符串往返因此为空。 source: String, + // 路由所指 namespace,执行/发布时须仍匹配当前共享状态。 namespace: String, + // 完整提交的代际号,worker 核验后才允许替换发布值,旧结果不能覆盖新 PUT/DELETE。 submission: u64, } -/// 绑定回环 API,启动请求接收与单 VM worker,返回活动日志及一次性退出控制句柄。 +/// main::main 初始化时调用,绑定本机 API 并启动两个线程,返回 shell 使用的活动记录和 RuntimeControl。 pub fn start( namespace: Arc>, proxy: EventLoopProxy, ) -> Result<(Activity, RuntimeControl)> { // 退出控制仅需一个消息槽,正常执行请求另走容量8队列,避免工作排满后无法通知关闭。 let (sender, receiver) = sync_channel(1); + // HTTP、shell 和 worker 共享请求/完成门闩,原生队列指针由实际线程启动函数一次性填入。 let control = RuntimeControl { sender, requested: Arc::new(AtomicBool::new(false)), @@ -156,6 +190,7 @@ pub fn start( native_sender: Arc::new(OnceLock::new()), namespace: namespace.clone(), }; + // worker 专用接收端与通知代理;不复制 Receiver 给多个执行线程抢同一退出消息。 let shutdown = Shutdown { receiver, control: control.clone(), @@ -165,9 +200,12 @@ pub fn start( let server = Server::http(API_ADDRESS) .map_err(|error| anyhow::anyhow!(error)) .with_context(|| format!("bind Tessera API to {API_ADDRESS}"))?; + // 无 VM/文档 payload 的更新通知,GUI 收到后自行读最近完整 NamespaceState。 let notify: Notify = Arc::new(move || { + // 事件循环已退出时通知失败无需再触发 VM 故障,发布值本身仍由共享状态保存。 let _ = proxy.send_event(AppEvent::DocumentUpdated); }); + // Network/Console 观察器共享纯数据有界日志,和 VM 的 globals/堆状态分开。 let activity = Arc::new(Mutex::new(ActivityLog::default())); start_server_observed( Arc::new(server), @@ -182,7 +220,7 @@ pub fn start( } #[cfg(test)] -/// 为本地监听服务建立请求处理线程,并把执行请求转交有界队列。 +/// API 测试调用,使用测试自己绑定的 Server,不安装原生退出/输入端口,复用真实路由与 worker。 fn start_server( server: Arc, namespace: Arc>, @@ -199,7 +237,7 @@ fn start_server( ) } -/// 启动带活动观察的 HTTP 服务,将请求状态变化通知管理界面。 +/// start 与 start_server 共用线程启动器,建立唯一执行队列、VM worker 和只负责路由的 HTTP 接收线程。 fn start_server_observed( server: Arc, namespace: Arc>, @@ -212,18 +250,23 @@ fn start_server_observed( // HTTP 接收与 VM 执行分离,最多8个排队请求;源代码只在单个 endor-worker 中操作 VM。 let (sender, receiver) = sync_channel::(8); if let Some(native_port) = native_port { + // 原生输入接入同一个 sender,不能有绕过串行执行/刷新约束的第二条解释器路径。 native_port .set(sender.clone()) .map_err(|_| anyhow::anyhow!("native input queue already attached"))?; } + // worker 线程与 HTTP 线程各持发布状态 Arc,任何一方都不把读写锁贯穿源代码执行。 let worker_namespace = namespace.clone(); + // worker 的发布通知与 HTTP 观察通知用途不同,均只投递原生事件。 let worker_notify = notify.clone(); + // HTTP 线程仅借门闩判断是否拒绝新执行请求,不持 Shutdown 的独占接收端。 let exiting = shutdown .as_ref() .map(|shutdown| shutdown.control.requested.clone()); thread::Builder::new() .name("tessera-endor-worker".into()) .spawn(move || { + // 所有实际 VM 会话只在这个线程构建/操作,Default 此时尚未创建任何用户程序。 let mut worker = Worker::default(); // 分片请求跨循环保留原 HTTP 请求和继续句柄;同一时刻只推进一个有未完成 VM 执行的输入。 let mut pending: Option<(BuildRequest, PendingRequest)> = None; @@ -234,13 +277,16 @@ fn start_server_observed( // 退出优先于下一量子:先撤去待响应请求,再由 shutdown_runtime 清理 VM 续体与生命周期资源。 if let Some((job, input)) = pending.take() { drop(input); + // 客户端可能已断开,尽力回应后仍继续清理,不能因 socket 失败阻止应用退出。 let _ = job .request .respond(error_response(StatusCode(503), "application is exiting")); } + // 清理完成文本含可能的错误,不因收到了退出请求就直接向 GUI 报成功。 let outcome = worker.shutdown_runtime(); // 排队请求全部明确回应503,不能留着客户端无期限等待已经停止的执行线程。 while let Ok(job) = receiver.try_recv() { + // 已接受但未执行的请求明确发 503;发送失败仅表示接收者已离开,不再重新入队。 let _ = job .request .respond(error_response(StatusCode(503), "application is exiting")); @@ -257,12 +303,14 @@ fn start_server_observed( } } eprintln!("{}", outcome.summary); + // 共享完成槽同时服务有限等待和 GUI 退出事件,先存 outcome 再通知。 let (lock, ready) = &*shutdown.control.completion; if let Ok(mut result) = lock.lock() { *result = Some(outcome.clone()); // 先在锁内保存清理结果再唤醒等待者,同时用 AppEvent 通知原生事件循环退出,两条路径复用同一 outcome。 ready.notify_all(); } + // 完成槽已先保存 outcome,GUI 事件投递失败不抹掉可由 wait_shutdown 取得的结果。 let _ = shutdown.proxy.send_event(AppEvent::ShutdownComplete { error: outcome.error, }); @@ -273,12 +321,11 @@ fn start_server_observed( match worker.resume_request(&job, input, &worker_namespace, &worker_notify) { RequestProgress::Pending(input) => { pending = Some((job, input)); - // A soft VM quantum is a control boundary, not a - // minimum-duration time slice. Yield to other ready - // threads without charging a timer delay per quantum. + // 软量子只提供控制边界,不强制睡固定毫秒,避免慢布局每片额外累计等待延迟。 thread::yield_now(); } RequestProgress::Complete(response) => { + // 一次性回应消费 job;客户端断开不重执行已完成业务输入,否则会重复副作用。 let _ = job.request.respond(response); } } @@ -286,6 +333,7 @@ fn start_server_observed( } // 空闲输入边界先推进视口刷新与任务,待执行刷新/协作任务时零等待,否则最多100ms检查一次。 worker.refresh_viewport(&worker_namespace, &worker_notify); + // 活跃视觉续体/可运行协程应立即再推进;只有未来计时/I/O 等待时才最多阻塞 100ms。 let wait = if worker.repl.as_ref().is_some_and(|(_, runtime)| { runtime.has_pending_refresh() || runtime.has_cooperative_tasks() }) { @@ -295,8 +343,10 @@ fn start_server_observed( }; match receiver.recv_timeout(wait) { Ok(job) => { + // begin_request 既可能立即完成,也可能要求等待旧刷新或保存新的候选续体。 match worker.begin_request(&job, &worker_namespace, &worker_notify) { RequestProgress::Pending(input) => { + // 请求与继续句柄绑定留到下轮,不能先回应成功后再执行它的 UI 刷新。 if job.native.is_none() && matches!(&input, PendingRequest::BeforeInput) { @@ -308,11 +358,13 @@ fn start_server_observed( pending = Some((job, input)); } RequestProgress::Complete(response) => { + // 同步完成也只尝试一次回执,发送失败不回滚 VM 已完成写入或发布文档。 let _ = job.request.respond(response); } } } Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, + // timeout 不是 worker 已停止,仅是检查拥有资格/继续下一轮空闲推进的边界。 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { if worker.repl.as_ref().is_some_and(|(epoch, runtime)| { worker_namespace.read().is_ok_and(|current| { @@ -341,6 +393,7 @@ fn start_server_observed( // 退出后拒绝写入/执行路由,GET 与 OPTIONS 仍可读取最近发布值或探测接口。 && !matches!(request.method(), Method::Get | Method::Options) { + // 写入/执行在退出门闩后拒绝,GET/OPTIONS 保留观察;响应失败同样继续接收其他只读请求。 let _ = request.respond(error_response(StatusCode(503), "application is exiting")); continue; @@ -358,20 +411,21 @@ fn start_server_observed( .context("start Tessera agent API thread") } -// Capture only allowlisted headers: credentials and cookies never enter the inspector. -/// HTTP 请求的观察包装,在读取正文和响应时维护 Network 工具记录。 -/// HTTP and in-process native jobs share one serial execution/refresh path. -/// Native envelopes cannot be created by an HTTP header or request body. +/// BuildRequest 的一次性回应端,HTTP 与进程内原生工作共享执行/刷新路径但保持不同来源身份。 +/// HTTP 头或正文不能创建 Native 变体,也不能让任意远程报文走原生连续输入的宽松版本追随。 enum RequestReply { Http(ObservedRequest), Native(SyncSender>), } impl RequestReply { + /// worker 线程在请求终结/退出时调用,HTTP 发响应,Native 只写回一次确认或有限错误文本。 fn respond(self, response: Response>>) -> std::io::Result<()> { match self { Self::Http(request) => request.respond(response), Self::Native(done) => { + // 连续原生输入过期可能正常返回 409,按已消费处理,不把每条旧采样都显示成运行时故障。 let status = response.status_code().0; + // 原生侧不解析响应 JSON;非 200/409 取至多 2048 字符错误预览交 GUI 展示。 let result = if status == 200 || status == 409 { Ok(()) } else { @@ -380,6 +434,7 @@ impl RequestReply { .take(2048) .collect()) }; + // relay 可能已因窗口退出丢弃接收端,worker 不应为此把已处理输入重新执行或停止。 let _ = done.send(result); Ok(()) } @@ -387,14 +442,19 @@ impl RequestReply { } } +/// handle 包装 tiny_http 请求,持有直到 worker 最终响应;只允许特定头部进入 Network 观察日志。 struct ObservedRequest { + // 真正 socket 请求所有权,respond 消费它,Deref 只让路由/正文读取复用 tiny_http 接口。 inner: Request, + // 共享有界观察日志,不持 VM 强引用或业务对象身份。 activity: Activity, + // begin 返回的日志记录身份,异步执行后仍能对应正确请求项。 id: u64, + // 日志正文/响应更新后通知 GUI,而非只在实际文档发布时刷新工具。 notify: Notify, } impl ObservedRequest { - /// 从请求提取允许观察的头部与显示来源,创建活动记录并通知工具界面。 + /// handle 在路由前调用,提取白名单头和来源显示标签,不把凭据/Cookie 等复制到观察日志。 fn new(inner: Request, activity: Activity, notify: Notify) -> Self { // 活动观察只保留 Content-Type/Length;X-Tessera-Origin 仅判来源标签,不把任意认证头复制进工具日志。 let headers = inner @@ -415,6 +475,7 @@ impl ObservedRequest { "HTTP" } .to_owned(); + // 创建日志项后只留 id,worker 可以稍后再填响应,不借用锁中的 ActivityLog 记录。 let id = activity.lock().unwrap().begin( inner.method().to_string(), inner.url().to_owned(), @@ -429,15 +490,18 @@ impl ObservedRequest { notify, } } - /// 将受限请求正文记录到活动预览,不在观察器中执行源码。 + /// submit 成功读取有界 UTF-8 正文后调用,写观察预览并通知工具,不执行源码或创建候选 VM。 fn record_body(&self, body: &str) { self.activity.lock().unwrap().body(self.id, body); (self.notify)(); } - /// 发送最终 HTTP 响应,并同步结束该条活动记录。 + /// RequestReply::respond/handle 错误路径调用,先记预构造响应,再消费真实 tiny_http 请求发回客户端。 fn respond(self, response: Response>>) -> std::io::Result<()> { + // 拆开元信息和内存正文 Cursor,记录预览后按原内容重建,避免观察器吃掉实际响应。 let status = response.status_code(); + // 克隆头列表供日志和原响应同时使用,观察器不修改协议状态或内容类型。 let headers = response.headers().to_vec(); + // 保留数据长度给重建响应,不按 Unicode 字符数重算 Content-Length。 let length = response.data_length(); // 响应正文本已在内存 Cursor,记录预览后用原 status/headers/reader/length 重建响应,不消费正文内容。 let reader = response.into_reader(); @@ -459,19 +523,19 @@ impl ObservedRequest { } impl Deref for ObservedRequest { type Target = Request; - /// 将包装类型的只读访问转交给内部当前上下文。 + /// Rust 在路由/头部访问时隐式调用,借用底层 Request;包装日志所有权不转移。 fn deref(&self) -> &Request { &self.inner } } impl DerefMut for ObservedRequest { - /// 将包装类型的可变访问转交给内部当前上下文。 + /// submit/read_code 请求正文读取时隐式调用,访问底层流,不绕过有界读取检查。 fn deref_mut(&mut self) -> &mut Request { &mut self.inner } } -/// 解析 HTTP 方法和路径;只读路由读取发布值,执行路由交给 worker。 +/// HTTP 接收线程逐个请求调用,区分执行入队和纯发布值读取;执行请求由 worker 持有回应,不在此等 VM。 fn handle( request: Request, namespace: &Arc>, @@ -480,7 +544,9 @@ fn handle( activity: &Activity, observer_notify: &Notify, ) { + // 先建观察记录,即使路由非法也能在 Network 看到实际响应,不只记录成功执行请求。 let request = ObservedRequest::new(request, activity.clone(), observer_notify.clone()); + // method 克隆为纯枚举,后面 submit 移动 request 所有权不会留下 Request 借用。 let method = request.method().clone(); // 路由匹配去掉查询串,namespace 从固定路径提取;不把 query 当作命名空间或源代码。 let path = request @@ -490,32 +556,42 @@ fn handle( .unwrap_or(request.url()) .to_owned(); + // 三个 POST 后缀分别对应隔离表达式、持久 Console 和当前程序输入,不能因正文像源码就互换。 let evaluation = namespace_from_route(&path, "/evaluate"); + // DELETE 同一个 /repl 地址表示重置该持久会话。 let repl = namespace_from_route(&path, "/repl"); + // /events 接受 JSON 事件,后续仍由 worker 校验会话/窗口/布局身份。 let event = namespace_from_route(&path, "/events"); // PUT/code 完整替换,POST 在 events/repl/evaluate 中分类,DELETE/repl 是持久会话重置;其他交给资源读取/清空路径。 let route = if method == Method::Put { + // 完整替换只接受 /code,不让 PUT /repl 混成持久追加操作。 namespace_from_path(&path).map(|name| (name, RequestKind::Code)) } else if method == Method::Post { + // 先找事件,再 Console,再表达式;后缀严格匹配,命名空间不能含额外路径段。 event .map(|name| (name, RequestKind::Event)) .or_else(|| repl.map(|name| (name, RequestKind::Repl))) .or(evaluation.map(|name| (name, RequestKind::Expression))) } else if method == Method::Delete { + // 这里只识别需 worker 清理的 /repl,DELETE /code 的发布状态撤销留给 code_request。 repl.map(|name| (name, RequestKind::ResetRepl)) } else { + // GET/OPTIONS 与不支持方法继续同步读取/错误响应,不创建执行 job。 None }; if let Some((addressed, kind)) = route { + // submit 消费请求,队列/输入错误也由它负责响应;handle 不再重复发送第二个响应。 submit(request, addressed, namespace, sender, kind); return; } + // 这些只读/清空资源路径只读写 NamespaceState,不读取活 VM globals 或执行源函数。 let response = if method == Method::Get && path == "/agent" { match namespace.read() { Ok(namespace) => markdown(StatusCode(200), agent_guide(&namespace)), Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"), } } else if let Some(addressed_namespace) = namespace_from_path(&path).map(str::to_owned) { + // /code 的 GET/DELETE/OPTIONS 统一走资源处理,路径匹配不等于 namespace 已被授权存在。 code_request(method, &addressed_namespace, namespace, notify) } else if method == Method::Options { text(StatusCode(204), "", "text/plain") @@ -523,20 +599,23 @@ fn handle( error_response(StatusCode(404), "route not found; start with GET /agent") }; + // 同步路由只回应一次;连接已断开也不应再次处理路由或重复 DELETE 的清空副作用。 let _ = request.respond(response); } -/// 处理源码资源的 GET/DELETE 等请求,返回对应版本头或撤销运行时所有权。 +/// handle 对 /code 的同步资源路径调用,GET 返回最近发布源码,DELETE 撤销发布/执行归属并通知 GUI。 fn code_request( method: Method, addressed_namespace: &str, namespace: &Arc>, notify: &Notify, ) -> Response>> { + // 先短读检查 namespace 和 GET 元信息;DELETE 需要下方先 drop 读锁再取得写锁。 let Ok(current) = namespace.read() else { return error_response(StatusCode(500), "namespace lock is poisoned"); }; if addressed_namespace != current.name { + // 单活 namespace 模型不自动创建另一个域,拼错名字直接 404 而非向当前程序投递。 return error_response( StatusCode(404), format!( @@ -548,6 +627,7 @@ fn code_request( // GET 返回最近成功发布的 source 而非正在排队的输入,并通过头部暴露 revision/session/window/layout 及逻辑视口。 if method == Method::Get { + // 响应正文来自成功发布的文档 source,排队中的候选/未稳定 Console 草稿不可提前读取。 let mut response = text( StatusCode(200), ¤t.document.source, @@ -574,6 +654,7 @@ fn code_request( ¤t.document.frame.layout_version.to_string(), ); if let Some(viewport) = current.viewport { + // 尺寸头是逻辑可用 viewport,不是操作系统屏幕大小,未知时不造零值头。 add_header( &mut response, "X-Tessera-Viewport-Width", @@ -594,8 +675,10 @@ fn code_request( .as_ref() .is_some_and(|root| root.kind == "repl-session") { + // transcript 含多个已接受输入,调用者不应把它误当可直接替换的单个源快照。 "repl-transcript" } else { + // 完整程序原文快照与独立 Console transcript 的返回含义明确区分。 "source-snapshot" }, ); @@ -607,6 +690,7 @@ fn code_request( match method { Method::Delete => match namespace.write() { Ok(mut current) => { + // clear 同时推进撤销身份;worker 后续检查归属时不会再提交先前候选的晚到结果。 let revision = match current.clear() { Ok(revision) => revision, Err(error) => return error_response(StatusCode(500), error.to_string()), @@ -629,7 +713,7 @@ fn code_request( } } -/// 读取并检查请求体,分配提交身份,再把工作送进有界执行队列。 +/// handle 的执行路由调用,先读有界 UTF-8 正文,再持发布锁完成入队和接受身份更新,不执行 VM。 fn submit( mut request: ObservedRequest, addressed: &str, @@ -637,16 +721,20 @@ fn submit( sender: &SyncSender, kind: RequestKind, ) { + // 正文读取失败直接 400,不推进 submission 或创建一项只能在执行阶段再失败的队列工作。 let source = match read_code(&mut request) { Ok(source) => source, Err(error) => { + // 正文错误在入队前响应,失败连接不影响其他请求,也不分配新的提交身份。 let _ = request.respond(error_response(StatusCode(400), error.to_string())); return; } }; // 读取成功后才保存受限活动正文预览;超长或非法 UTF-8 请求不会进入执行队列。 request.record_body(&source); + // 写锁把“接受新提交”与 worker 的“发布旧提交”互斥,不能先递增身份再入队失败。 let Ok(mut current) = namespace.write() else { + // 无法可靠读取发布身份时返回服务端错误,不能绕过锁以猜测 submission 接受新代码。 let _ = request.respond(error_response( StatusCode(500), "namespace lock is poisoned", @@ -654,6 +742,7 @@ fn submit( return; }; if current.name != addressed { + // 路由名字只是请求指向,真正执行归属要匹配当前共享 namespace。 let _ = request.respond(error_response(StatusCode(404), "namespace is not active")); return; } @@ -665,10 +754,12 @@ fn submit( } { Ok(id) => id, Err(error) => { + // 身份计数耗尽等错误尚未入队,保留当前有效 submission,不接受无法唯一标识的工作。 let _ = request.respond(error_response(StatusCode(500), error.to_string())); return; } }; + // HTTP 信封明确 native=None,正文即使声明某个字段也不能成为原生后台回执输入。 let job = BuildRequest { request: RequestReply::Http(request), native: None, @@ -679,20 +770,21 @@ fn submit( }; // 持 namespace 写锁完成入队和接受身份更新;worker 提交使用同一锁,保证接受与发布之间没有竞态窗口。 match sender.try_send(job) { - // A rejected queue entry never invalidates accepted work. The worker - // uses this same lock when committing, so publication is atomic. Ok(()) => { + // 只有成功入队的完整 PUT 才把 submission 改成新值,未接受请求不会废掉已有工作。 if kind == RequestKind::Code { current.submission = submission; } } // 队列拒绝时不推进 submission,所以已经接受的旧工作不会因为本次失败请求而失去发布资格。 Err(TrySendError::Full(job)) => { + // try_send 把请求所有权退回,立即回应暂不可用;不等待空位占住 HTTP 接收线程。 let _ = job .request .respond(error_response(StatusCode(503), "build queue is full")); } Err(TrySendError::Disconnected(job)) => { + // worker 已消失与容量暂满分别报告,原 HTTP 请求仍在返回 job 中,可明确回应而非挂起。 let _ = job.request.respond(error_response( StatusCode(503), "execution worker is unavailable", @@ -701,7 +793,7 @@ fn submit( } } -/// 生成较旧请求已被新提交取代的响应,拒绝晚到结果覆盖当前文档。 +/// worker 在执行/发布边界发现提交已被新 PUT/DELETE 取代时调用,返回 409,不覆盖当前文档。 fn superseded(submission: u64) -> Response>> { json( StatusCode(409), @@ -710,19 +802,19 @@ fn superseded(submission: u64) -> Response>> { ) } -/// 从固定路由格式提取命名空间,避免把不匹配的路径当作代码资源。 +/// handle 的各 API 路由解析调用,借用严格前后缀之间的单段 namespace,失败返回 None 不做模糊回退。 fn namespace_from_route<'a>(path: &'a str, suffix: &str) -> Option<&'a str> { // 同时检查前后缀且命名空间非空、无额外斜杠;不做模糊路径前缀匹配。 path.strip_prefix("/v1/namespaces/")? .strip_suffix(suffix) .filter(|namespace| !namespace.is_empty() && !namespace.contains('/')) } -/// 从固定路由格式提取命名空间,避免把不匹配的路径当作代码资源。 +/// handle 为 /code 资源调用的特化解析,复用 namespace_from_route 的单段/非空约束。 fn namespace_from_path(path: &str) -> Option<&str> { namespace_from_route(path, "/code") } -/// 按源码字节上限读取 UTF-8 请求体,读取失败或超限时返回诊断。 +/// submit 在入队之前调用,已知长度预拒绝、真实流再硬限长,任何非 UTF-8 都不能进入 source 字符串。 fn read_code(request: &mut Request) -> Result { if request .body_length() @@ -744,8 +836,9 @@ fn read_code(request: &mut Request) -> Result { Ok(code) } -/// 将前端/编译/执行诊断编码为 API 错误响应,保留用户可理解的位置和消息。 +/// worker 执行/加载失败时调用,将 anyhow 错误链转成 422;有前端来源才附精确行列,不执行 VM 求值。 fn compile_error(error: anyhow::Error) -> Response>> { + // 先保存完整可读错误链,只有确有 SourceDiagnostic 才追加源位置字段。 let mut diagnostic = serde_json::json!({ "severity": "error", "message": format!("{error:#}") }); // 有 SourceDiagnostic 才附行/列和半开字节范围;其他运行时错误仍保留完整错误链消息,不编造源码位置。 @@ -764,7 +857,7 @@ fn compile_error(error: anyhow::Error) -> Response>> { ) } -/// 生成统一 JSON 错误响应,使调用方能区分协议错误与成功结果。 +/// 路由、入队、退出及 worker 的协议/运行状态错误共用助手,保留调用者选择的 HTTP 状态码。 fn error_response( status: StatusCode, message: impl Into, @@ -772,7 +865,7 @@ fn error_response( json(status, serde_json::json!({ "error": message.into() })) } -/// 以明确的内容类型和状态码构造响应,并应用本机 API 的公共响应头。 +/// 成功/错误 API 分支共用 JSON 编码入口,正文交 text 一次性包装成内存字节 Cursor。 fn json(status: StatusCode, value: serde_json::Value) -> Response>> { text( status, @@ -781,12 +874,12 @@ fn json(status: StatusCode, value: serde_json::Value) -> Response Response>> { text(status, &value, "text/markdown; charset=utf-8") } -/// 以明确的内容类型和状态码构造响应,并应用本机 API 的公共响应头。 +/// json/markdown 和源码 GET/OPTIONS 共用响应助手,统一状态/内容类型和固定本机 CORS 头。 fn text(status: StatusCode, value: &str, content_type: &str) -> Response>> { // 公共响应头只允许固定本机 Origin/方法/头名,非通配任意来源;这些头不替代回环绑定与路由校验。 let mut response = Response::from_string(value).with_status_code(status); @@ -809,7 +902,7 @@ fn text(status: StatusCode, value: &str, content_type: &str) -> Response>>, name: &str, value: &str) { // 由 HTTP 库验证字段格式,非法头被忽略,不把未校验字符拼进原始响应报文。 if let Ok(header) = Header::from_bytes(name, value) { diff --git a/src/runtime/endor_host.rs b/src/runtime/endor_host.rs index b3b2075..0fd2cd7 100644 --- a/src/runtime/endor_host.rs +++ b/src/runtime/endor_host.rs @@ -2,8 +2,10 @@ //! 完整源码 PUT 使用隔离候选;Console 输入则在已有 VM 上执行,先前发生的写入不等于可以回放或事务回滚。 //! 刷新状态机遍历窗口并逐步推进布局/绘制。完成所有必要轮次才形成文档;取消会释放挂起调用并恢复未提交的 Mount。 //! -//! Compile complete source in a fresh VM or retain a worker-owned Console VM. -//! Console retains mounted native references and publishes immutable frames after successful inputs. +//! api::worker 为完整 PUT 创建 ReplSession 候选,为 Console/事件/resize 调用已有会话;只有该 worker 拥有 VM。 +//! UI 类型与刷新由 bridge 适配;此模块负责源码预算、真实会话、执行结果和挂起刷新的生命周期。 +//! compile/evaluate 是一次性入口,关闭 Candidate 后只返回纯数据;生产持久路径使用增量入口并保存续体。 +//! 本模块仍直接依赖 UI Frame/桥接,不是已经可以独立搬走的通用 Nexus runtime,迁移需先抽出宿主接口。 use anyhow::{Result, bail}; use endor_core::{ExecutionLimits, ObjectRef, Value, Vm, VmError}; use frontend::{ @@ -18,33 +20,46 @@ use crate::{ layout, ui_bridge, }; +/// API、完整加载、Console 与 evaluate 共用的原始输入字节预算;不是字符数或 AST 节点数量。 pub const MAX_SOURCE_BYTES: usize = 1_048_576; +// 每次源执行的硬上限;脚本多片段会分摊指令额度,不能每段重新获得完整额度。 const LIMITS: ExecutionLimits = ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }; +// AstBudget 访问者的结构预算,在加载代码前拒绝过大或过深语法树。 const MAX_AST_NODES: usize = 10_000; const MAX_AST_DEPTH: usize = 64; -/// 把源输入编译并执行为一次独立结果,最终将可展示对象转换为检查后的文档。 +/// 一次性编译/现有 API 检查路径调用,在临时 VM 执行并提取文档,不返回持久可响应事件的 ReplSession。 pub fn compile(source: &str, namespace: &str, submission: u64) -> Result { if source.len() > MAX_SOURCE_BYTES { + // 先限制输入再进行词法/AST 分配,避免仅靠执行预算保护不了源码解析开销。 bail!("source exceeds the 1 MiB limit"); } + // 把请求来源和提交编号放入诊断 SourceUnit,后续注解错误可显示真实源行列。 let source = EndorReader::from_text(format!("{namespace} [submission {submission}]"), source); + // Module/Script 组织由宿主确定,语法解析和位置映射继续委托 endor_script/Aster。 let plan = endor_script::parse(&source)?; // 各 Script 根共享 AST 计数预算,不能通过拆成很多交互片段绕过整份源码节点上限。 let mut budget = AstBudget::default(); for ast in plan.roots() { + // 同一个 budget 累加每段,不因片段边界重置节点/深度检查。 budget.visit_ast_root(ast)?; } + // 编译得到可加载模板,此时还没有把业务语句在任何 VM 中运行。 let compiled = compile_plan(&source, &plan)?; + // 纯数据代码树供 Sources 浏览,和下面实际可执行代码/托管图不是同一结构。 let root = architecture(&source, &plan, namespace, submission); + // Candidate 是此一次性域的 VM 与根拥有者,所有出口都在下面 close 收尾。 let mut candidate = Candidate::new()?; + // 保存整个执行结果,不能让内部 ? 跳过 Candidate::close。 let result: Result = (|| { + // 同一程序的所有段共享一个 globals,真实引用由 candidate.roots 持有。 let globals = candidate.vm.new_dict()?; candidate.globals = Some(globals); candidate.roots.push(globals); + // 类型安装在执行前完成,全局别名引用同一批名义对象,不在每片脚本重建 UI 类型。 let bridge = ui_bridge::install(&mut candidate.vm, &mut candidate.roots)?; bridge.install_globals(&mut candidate.vm, globals)?; // Script 多段分别执行,指令总额按片段数均分;Module 作为整体获得完整预算,避免每段都重获一百万。 @@ -56,8 +71,10 @@ pub fn compile(source: &str, namespace: &str, submission: u64) -> Result Result bridge .rendered(&candidate.vm)? .ok_or_else(|| anyhow::anyhow!("script must call ui.page().render(root)"))?, // 模块先执行定义再调用 main;若 main 未显式 render,可将其返回值转换成展示根。 SourcePlan::Module(_) => { + // 从真实全局字典找 main,不能通过 AST 名字存在就假定运行结果也是 callable。 let entry = candidate .vm .dict_get(globals, &Value::Text("main".into()))? .ok_or_else(|| { anyhow::anyhow!("declare main or render a page from a top-level script") })?; + // 对象身份是可调用检查的第一步,VM.call 仍继续验证具体 callable 契约。 let Value::Object(entry) = entry else { bail!("main must be a callable function"); }; + // main 返回值有局部拥有根,快照成功/失败都须经过下面 release_value。 let value = candidate .vm .call_with_limits(entry, &[], LIMITS) .map_err(|error| runtime_error(&candidate.vm, error))?; bridge.refresh_bindings(&mut candidate.vm)?; + // 尊重 main 已主动展示的页面,否则用返回 Block 直接提取兼容快照。 let snapshot = bridge .rendered(&candidate.vm) .and_then(|rendered| match rendered { @@ -96,6 +119,7 @@ pub fn compile(source: &str, namespace: &str, submission: u64) -> Result Result Result { if source.len() > MAX_SOURCE_BYTES { bail!("expression exceeds the 1 MiB limit"); } + // 独立来源名明确区别于持久 Console transcript,不读取其历史或对象堆。 let source = EndorReader::from_text("console expression", source); // 隔离 evaluate 只接受一个表达式,默认编译选项不安装 ui 桥接,也不读已发布程序的 globals。 let ast = frontend::parse_source_ast(&source, frontend::ParseMode::Expression)?; AstBudget::default().visit_ast_root(&ast)?; + // 默认全局只有语言内置,不为 evaluate 暗中安装 ui 或活程序用户全局。 let compiled = compiler::compile_ast(&ast, &source, &compiler::CompileOptions::default()) .map_err(|error| source.annotate(error))?; + // 独立候选统一持有执行根,表达式失败也由 close 释放整个域。 let mut candidate = Candidate::new()?; + // 暂存执行/格式化错误,保证外层 close 不因这里的 ? 被跳过。 let result = (|| { + // 表达式全局环境为空的新字典,不能用运行时 namespace 文档代替真实 globals。 let globals = candidate.vm.new_dict()?; candidate.globals = Some(globals); candidate.roots.push(globals); + // 模板加载到此 VM 后才有真实代码对象,拥有根记入 Candidate。 let code = compiled.load(&mut candidate.vm)?; candidate.roots.push(code); + // 一次有界执行产生拥有返回值,字符串化后显式释放。 let value = candidate .vm .execute_with_limits(code, globals, LIMITS) @@ -149,6 +179,7 @@ pub fn evaluate(source: &str) -> Result { candidate.vm.release_value(value)?; Ok(rendered) })(); + // 先尝试关闭候选,再合并执行和清理结果,不能只因表达式已有字符串结果就跳过清理。 let cleanup = candidate.close(); match (result, cleanup) { (Ok(value), Ok(())) => Ok(value), @@ -156,7 +187,7 @@ pub fn evaluate(source: &str) -> Result { } } -/// 按 Script/Module 计划编译各根节点,为后续执行准备可加载模板。 +/// compile/prepare_program 调用,建立 UI 宿主全局名集合,再复用增量编译助手生成模板序列。 fn compile_plan(source: &SourceUnit, plan: &SourcePlan) -> Result> { compile_plan_with_globals( source, @@ -169,20 +200,19 @@ fn compile_plan(source: &SourceUnit, plan: &SourcePlan) -> Result, ) -> Result> { - // Match Aster module behavior for function forward references. Bindings are - // added in source order; every executable statement still uses Aster's own - // single-input Interactive compiler and the candidate's shared globals. // 先收集全部函数/类声明名以支持前向引用;普通绑定仍按源码执行次序逐段加入已知全局名。 for root in plan.roots() { if let AstRoot::Interactive(input) = root { + // 完整模块的声明可见性由 Aster 自己处理;只有拆成多个交互根时宿主需预收集声明名。 for statement in &input.statements { if let StmtKind::Declaration(decl) = &statement.kind { + // 只预声明函数/类,不提前把任意赋值名当作已存在业务值。 let name = match &decl.kind { DeclKind::Function(function) => &function.name, DeclKind::Class(class) => &class.name, @@ -192,16 +222,21 @@ fn compile_plan_with_globals( } } } + // 每个根保留一个模板,执行顺序必须与原根切片一一对应,不能按名字排序。 let mut compiled = Vec::with_capacity(plan.roots().len()); + /// 本函数在每段成功编译后调用,递归收集后续段可能引用的绑定名,不执行或预测条件真值。 fn assigned(statement: &Stmt, globals: &mut BTreeSet) { match &statement.kind { StmtKind::Binding(b) => { + // 绑定给后续片段的静态名称解析提供名字;字典中的真实值仍在执行时建立。 globals.insert(b.name.text.clone()); } StmtKind::Assignment(a) => { + // 顶层赋值也可能引入名字,按 Aster 交互语义让后续段可以编译引用。 globals.insert(a.name.text.clone()); } StmtKind::If(b) => { + // 两个分支都收集“可能声明”名字,不在编译阶段决定哪条路径会执行。 for s in &b.body.statements { assigned(s, globals); } @@ -212,14 +247,17 @@ fn compile_plan_with_globals( } } StmtKind::While(b) => { + // 循环体可能写入全局;是否真正执行一次由 VM 决定,宿主不展开循环。 for s in &b.body.statements { assigned(s, globals); } } + // 表达式、return、成员赋值等不引入新的普通全局名;函数内部作用域由语言编译器处理。 _ => {} } } for root in plan.roots() { + // 每段得到当前已知全局集合快照,先编译再把本段绑定加入下一段上下文。 let options = compiler::CompileOptions { globals: globals.iter().cloned().collect(), }; @@ -237,12 +275,13 @@ fn compile_plan_with_globals( Ok(compiled) } -/// One real VM, owned only by the existing execution worker. No history replay. -/// The UI bridge shares the same managed object heap and publishes atomic frames. -/// 指定窗口尚未完成的绘制工作,文档与源续体必须保持配对。 +/// refresh_step 挂起 paint 时创建、下一轮恢复/取消时消费,文档草稿与所属窗口续体必须成对保留。 struct PendingPaint { + // 原窗口 ID,恢复/取消前显式 select_window,不能依赖稍后变化的 current。 window: u64, + // 对应本次完成布局的源码/视觉/几何草稿,paint 结束前不得对外发布。 document: RenderDocument, + // owning VM 中未完成的 source_paint Work,取消时显式关闭,Rust drop 本身不替代 VM 清理。 work: Box, } @@ -251,16 +290,23 @@ struct RefreshWork { // 已完成窗口的草稿先留在本轮;所有窗口收敛后才合成一次完整发布。 // RefreshWork 先克隆最近完整文档作为草稿基线,各窗口更新暂存于此;全轮稳定前不写共享 NamespaceState。 documents: std::collections::BTreeMap, + // 当前稳定性遍历轮剩余窗口,按 bridge 的退旧优先次序排队。 windows: std::collections::VecDeque, + // 本片正在推进窗口,pending/paint 的上下文必须与其一致。 current: Option, + // 当前帧不可变根草稿,布局完成再绘制仍使用这一轮同一份快照。 root: Option>, // 续体必须保留其 owning VM;取消时显式关闭,不能靠丢弃 Rust 值。 pending: Option>, + // 与 pending 布局阶段互斥的未完成绘制,完成后才把当前窗口文档写进 documents。 paint: Option>, // pass_changed 要求再跑一轮确认稳定,changed 记录整次刷新是否有可发布变化。 pass_start: bool, + // 一轮是否有变化,决定下一轮还需确认稳定;不等于最终对外是否已有变化。 pass_changed: bool, + // 整次刷新累计变化,用于稳定后决定返回文档还是 None。 changed: bool, + // 整次刷新工作量预算,跨多个 worker 量子累计,不能让每片各得一份完整收敛额度。 work: usize, // 记录本轮尺寸目标,下一次 worker 轮次据此判断是否需要重定向。 viewports: Option>, @@ -268,30 +314,45 @@ struct RefreshWork { /// 刷新仍在进行或已完成;完成也可能没有新文档需要发布。 enum RefreshProgress { + // worker 保留整个 ReplSession,下次继续已有布局/绘制续体,不接新业务写入。 Pending, + // 已稳定;None 为没有新文档,Some 才有可以让 worker 校验/发布的完整值。 Complete(Option>), } /// 持久源运行时与其桥接会话,保存实际全局对象、代码投影、全窗口文档和挂起刷新。 pub struct ReplSession { + // 真正 VM、全局拥有根和清理状态,和 HTTP/UI 读取的文档快照分离。 candidate: Candidate, + // 同 VM 的 Tessera 扩展能力,实际窗口/布局/绘制规则留在 bridge/model/render。 bridge: ui_bridge::UiBridge, + // worker 分配的运行时代际 ID,输入即使数值窗口/Mount 相同也不能跨代沿用。 pub id: u64, + // 已接受 Console 输入数量与累计 AST 节点数,完整源加载和后续输入都受各自预算控制。 inputs: usize, ast_nodes: usize, + // 完整程序或累加后的 Console transcript,供发布和 Sources 使用,不通过回放恢复执行状态。 source: String, + // 交互记录的纯数据代码树,与 Candidate 的真实 globals/函数引用不是同一存储。 nodes: Vec, // 回调/渲染失败后置位,拒绝继续普通业务输入;保留画面不表示堆还能安全执行。 faulted: bool, + // 完整程序的 Sources 根,首次 Console 追加时移入 nodes 保留原程序浏览记录。 program_root: Option, + // 最近完成的逐窗基线文档;未稳定刷新只改 RefreshWork 草稿,不提前替换共享发布状态。 documents: std::collections::BTreeMap, + // 同一个 VM 上至多一份跨片刷新,所有业务写入口先 require_idle_refresh。 refresh: Option, } /// 一次 Console/事件操作的标量显示结果与可选完整文档,避免直接跨线程暴露托管返回值。 pub struct ReplResult { + // 用于 Console/API 的字符串结果,不跨线程暴露拥有根 Value。 pub value: String, + // 只有本轮已经稳定完成刷新才返回完整文档,挂起或无变化时为 None。 pub document: Option, + // 当前 globals 条目数的显示统计,具体计算按 execute_mode 的接口约定处理。 pub bindings: usize, + // 已接受输入编号,供 API/Console 对应 transcript 与代码树记录。 pub input: usize, } impl ReplSession { @@ -317,7 +378,9 @@ impl ReplSession { id: u64, viewport: Option, ) -> Result<(Self, RenderDocument)> { + // 测试只接受主窗口尺寸,先建立真实 VM,再同步排空初始刷新便于直接断言帧内容。 let mut runtime = Self::prepare_program(source, namespace, submission, id, viewport)?; + // 成功执行但没发布页面不算完整 GUI 程序,避免返回一个表面“成功”的空候选。 let document = runtime .refresh_document()? .ok_or_else(|| anyhow::anyhow!("program did not publish a Page"))?; @@ -327,7 +390,7 @@ impl ReplSession { Ok((runtime, document)) } - /// 建立可由 worker 分轮推进的完整源码候选,初始布局不必在一次调用里排空。 + /// api::worker 的 PUT 完整源码路径调用,建立候选并只推进有限刷新,未稳定时保留 runtime 等后续量子。 pub fn load_incremental( source: &str, namespace: &str, @@ -335,11 +398,14 @@ impl ReplSession { id: u64, viewport: Option, ) -> Result<(Self, Option)> { + // 源程序执行在隔离 Candidate,旧活跃程序和 GUI 发布文档尚未替换。 let mut runtime = Self::prepare_program(source, namespace, submission, id, viewport)?; + // 首轮只提供主窗口已知尺寸,源程序新建的其他窗口由后续 resize 通知补充。 let viewports = viewport .map(|size| (crate::window::MAIN_WINDOW, size)) .into_iter() .collect(); + // Option 文档为 None 既可能挂起也可能无变化,必须再看 has_pending_refresh 区分。 let document = runtime.refresh_after_mutation(Some(&viewports))?; // 初始刷新若已完成必须确实有 Page 文档才可接受候选;仍挂起则把整个运行时交 worker 下一量子继续。 if !runtime.has_pending_refresh() { @@ -351,7 +417,7 @@ impl ReplSession { Ok((runtime, document)) } - /// 建立隔离候选、安装宿主根并执行完整源码;候选尚未完成布局时不替换活跃程序。 + /// load/load_incremental 的共同准备入口,在新 VM 执行完整源码,验证已安排页面;布局/发布留给外层。 fn prepare_program( source: &str, namespace: &str, @@ -362,29 +428,39 @@ impl ReplSession { if source.len() > MAX_SOURCE_BYTES { bail!("source exceeds the 1 MiB limit"); } + // 来源标注把 API namespace/提交号接入 Aster 错误来源,不改变用户原文。 let unit = EndorReader::from_text(format!("{namespace} [submission {submission}]"), source); + // 按 Module/Script 路径编译,脚本片段仍共享同一个 globals。 let plan = endor_script::parse(&unit)?; + // 一份输入的所有根累计使用同一结构预算,过大 AST 在业务执行前失败。 let mut budget = AstBudget::default(); for ast in plan.roots() { budget.visit_ast_root(ast)?; } + // 全部编译成功后才创建并执行新运行时,解析/编译失败不改变任何活程序对象。 let compiled = compile_plan(&unit, &plan)?; + // new 安装类型和 globals,但不主动打开页面;源程序决定要展示的内容。 let mut runtime = Self::new(id)?; runtime .bridge .set_viewport(&mut runtime.candidate.vm, viewport)?; // 完整程序代码在候选自己的真实 globals 中执行,之后保留该堆用于事件与 Console,不通过重放历史恢复状态。 let globals = runtime.candidate.globals.expect("program globals"); + // 根据已编译根数确定脚本每段硬额度;完整模块仍使用一份 LIMITS 执行模块定义。 let limits = if matches!(plan, SourcePlan::Script(_)) { + // 指令硬额度在脚本段间均分,不因一个输入拆成更多交互段而放大执行总量。 ExecutionLimits { max_instructions: LIMITS.max_instructions / compiled.len().max(1), ..LIMITS } } else { + // 模块作为一个编译单元执行定义,之后 main 有单独一次有界调用。 LIMITS }; for template in &compiled { + // 代码只需此段执行期间持有,执行结果暂存,代码根释放后再传播调用错误。 let code = template.load(&mut runtime.candidate.vm)?; + // 保存 Result 以保证失败执行也经过 vm.release(code)。 let result = runtime .candidate .vm @@ -394,14 +470,17 @@ impl ReplSession { runtime.candidate.vm.release_value(result?)?; } if matches!(plan, SourcePlan::Module(_)) { + // 模块顶层只安装定义,main 从实际 globals 查找,而非只依据 AST 声明列表。 let main = runtime .candidate .vm .dict_get(globals, &Value::Text("main".into()))? .ok_or_else(|| anyhow::anyhow!("declare a main function"))?; + // 同名全局可能被赋为标量,先解码对象,再由 VM 完整检查可调用契约。 let Value::Object(main) = main else { bail!("main must be callable"); }; + // 返回值保留到 has_page/open_value 决策之后;无论打开成功与否都须显式释放。 let value = runtime .candidate .vm @@ -411,12 +490,14 @@ impl ReplSession { let opened = if runtime.bridge.has_page()? { Ok(()) } else { + // 返回对象按 Presentable/普通值规则进入桥接,不要求所有模块都手写 page.render。 runtime.bridge.open_value(&mut runtime.candidate.vm, &value) }; runtime.candidate.vm.release_value(value)?; opened?; } if !runtime.bridge.has_page()? { + // UI 程序必须有实际安排页面,单独计算出一个未打开 Block 不算可发布程序。 bail!("script must call page.render(root) or ui.open(value)"); } runtime.source = source.into(); @@ -426,8 +507,9 @@ impl ReplSession { Ok(runtime) } - /// 建立空的持久 Console 运行时,并安装当前逻辑视口输入。 + /// api::worker 新建 Console 域时调用,在未打开页面的空会话中记录原生主窗口尺寸。 pub fn new_with_viewport(id: u64, viewport: Option) -> Result { + // 新 Console 不回放旧源码,不复制先前程序的可变对象堆;显示的旧文档仍由 worker 决策保留。 let mut runtime = Self::new(id)?; runtime .bridge @@ -435,12 +517,15 @@ impl ReplSession { Ok(runtime) } - /// 建立真实持久 VM、全局字典与 UI 桥接根,尚未执行用户输入或产生窗口帧。 + /// prepare_program/new_with_viewport 调用,分配持久 Candidate、globals 和 UiBridge,不执行用户代码。 pub fn new(id: u64) -> Result { + // Candidate 的 Drop 负责异常返回时的最终清理,不让 bootstrap 半途失败泄漏 VM 根。 let mut candidate = Candidate::new()?; + // globals 的拥有根留在 Candidate,实际用户绑定在以后输入中原地更新这个字典。 let globals = candidate.vm.new_dict()?; candidate.globals = Some(globals); candidate.roots.push(globals); + // 每个会话安装一份类型域,后续输入继续复用,不按 Console 行重复建立名义类型。 let bridge = ui_bridge::install(&mut candidate.vm, &mut candidate.roots)?; bridge.install_globals(&mut candidate.vm, globals)?; Ok(Self { @@ -463,7 +548,7 @@ impl ReplSession { self.execute_mode(source, namespace, None) } - /// 执行一次输入的业务部分,再让展示刷新以可恢复工作跨 worker 轮次推进。 + /// api::worker 的 Console 输入请求调用,执行真实 globals 上的语句,展示部分允许跨片挂起。 pub fn execute_incremental( &mut self, source: &str, @@ -473,7 +558,7 @@ impl ReplSession { self.execute_mode(source, namespace, Some(viewports)) } - /// 在当前真实 VM 中编译并执行新输入,再按同步或分片模式刷新展示;已发生的业务写入不会被源重放回滚。 + /// execute/execute_incremental 共用入口,先完整编译再执行;解析错误不写业务堆,运行错误不回滚此前成功语句。 fn execute_mode( &mut self, source: &str, @@ -482,6 +567,7 @@ impl ReplSession { ) -> Result { self.require_idle_refresh()?; if self.faulted { + // 故障后保留可见画面不代表 VM 安全可继续变更,必须重置会话或替换完整程序。 bail!( "runtime stopped after a callback or rendering failure; reset the session or replace its program" ); @@ -493,11 +579,14 @@ impl ReplSession { if self.inputs >= 256 || self.source.len() + source.len() + 80 > MAX_SOURCE_BYTES { bail!("console session exceeds 256 inputs or 1 MiB; reset the session to continue"); } + // 对每次输入保留会话号/输入号作为来源,累计 transcript 的行号稍后再为 Sources 投影偏移。 let source = EndorReader::from_text( format!("console session {} input {}", self.id, self.inputs + 1), source, ); + // 输入可以是一段或多段交互语句,语法仍由 Aster 的 parser 决定。 let plan = endor_script::parse(&source)?; + // 新输入的结构量先算出,再与会话累计量合并,不为每次输入重新放宽全会话上限。 let mut budget = AstBudget::default(); for ast in plan.roots() { budget.visit_ast_root(ast)?; @@ -505,6 +594,7 @@ impl ReplSession { if self.ast_nodes + budget.nodes > MAX_AST_NODES { bail!("console session exceeds 10000 AST nodes; reset the session to continue"); } + // new 已建立的字典继续原地使用,保存对象身份和闭包,不能通过历史源码重放重建它。 let globals = self.candidate.globals.expect("live console globals"); // 从实际 globals 枚举已有名称供增量编译,支持前次输入创建的真实对象,而非靠解析历史猜名称。 let names = self @@ -513,6 +603,7 @@ impl ReplSession { .dict_items(globals)? .iter() .filter_map(|(key, _)| { + // 只把字符串键变成静态编译全局名,不向编译器传 VM Value 或托管引用。 if let Value::Text(name) = key { Some(name.clone()) } else { @@ -520,9 +611,8 @@ impl ReplSession { } }) .collect(); + // 所有根先编译完,任何语法/类型编译错误都不会执行输入前半部分。 let compiled = compile_plan_with_globals(&source, &plan, names)?; - // Parsing/compilation finish before any new statement runs. Successful earlier - // statements in a runtime-failed input remain live, as in an ordinary REPL. self.bridge.discard_pending(&mut self.candidate.vm)?; self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; @@ -530,9 +620,11 @@ impl ReplSession { self.inputs += 1; self.ast_nodes += budget.nodes; if let Some(root) = self.program_root.take() { + // 第一次 Console 输入把原完整程序浏览树移入 transcript 节点,不丢原程序的 Sources 内容。 self.nodes.push(root); } if !self.source.is_empty() && !self.source.ends_with('\n') { + // 分隔行不改变 VM 代码语义,只保证后续 Sources 行号和注释不粘到上一行末尾。 self.source.push('\n'); } // 交互源码前加 Console input 注释分隔,投影节点行号再整体平移到累计 transcript 中。 @@ -543,6 +635,7 @@ impl ReplSession { if !self.source.ends_with('\n') { self.source.push('\n'); } + // 本轮输入的代码树先使用局部行号,再用 offset 平移到累计源码中的对应位置。 let mut node = architecture( &source, &plan, @@ -552,22 +645,27 @@ impl ReplSession { node.name = format!("Input {}", self.inputs); shift_node_lines(&mut node, offset); self.nodes.push(node); + // 保存执行/刷新失败供下面丢弃未发布请求并给 Sources 标注,已完成赋值不做事务回滚。 let result: Result = (|| { + // 多根均分本次硬预算,不让后续片段每个都获得完整一百万条指令。 let limits = ExecutionLimits { max_instructions: LIMITS.max_instructions / compiled.len().max(1), ..LIMITS }; + // 依次覆盖为最后一个片段的可显示值;本局部只保存字符串,不延长任意返回对象寿命。 let mut rendered_value = "nil".to_owned(); for (ast, template) in plan.roots().iter().zip(&compiled) { + // 代码根只跨一次 execute,先暂存 Result 再 release(code),失败也有清理。 let code = template.load(&mut self.candidate.vm)?; + // VM 执行真实 globals,不把本轮 AST 当快照去覆盖旧业务状态。 let executed = self .candidate .vm .execute_with_limits(code, globals, limits) .map_err(|error| runtime_error(&self.candidate.vm, error)); self.candidate.vm.release(code)?; + // execute 返回拥有根,后面 repr/全局 _ 更新是否成功都必须释放这份根。 let value = executed?; - // Keep the actual last result accessible as `_`, including unnamed UI objects. // 绑定/赋值语句展示其新变量实际值,普通表达式展示返回值;不是仅显示该语句执行的 Unit。 let display = if let AstRoot::Interactive(input) = ast { match &input.statements[0].kind { @@ -588,6 +686,7 @@ impl ReplSession { }; // 先算 repr 并把实际最后值写入全局 _,保留未命名 UI 对象;随后释放 execute 返回根,错误也逐项传播。 let repr = value_repr(&self.candidate.vm, &display); + // _ 的字典强边独立保留最后值,避免匿名 Block 在 Console 显示后马上丢失身份。 let retained = self.candidate .vm @@ -596,6 +695,7 @@ impl ReplSession { retained?; rendered_value = repr?; } + // 有业务变更后才推进 UI 稳定循环,可能返回挂起时的 None,worker 继续保留该会话。 let document = self.refresh_after_mutation(viewports)?; Ok(ReplResult { value: rendered_value, @@ -611,6 +711,7 @@ impl ReplSession { })(); // 失败丢弃未发布展示请求和原生命令,保留已执行业务写入,并在代码投影上明确标记非事务语义。 if result.is_err() { + // 只丢待发布展示与原生命令,不假装撤销此前已完成的用户赋值或资源关闭。 self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; if let Some(node) = self.nodes.last_mut() { @@ -626,17 +727,17 @@ impl ReplSession { result } - /// 查询调度器是否仍有需要主动推进的源协程工作。 + /// api::worker 选择空闲等待策略时查询,区分可运行协程与仅等待定时/I/O 的任务,不执行用户代码。 pub fn has_cooperative_tasks(&self) -> bool { self.bridge.has_cooperative_tasks() } - /// 检查是否存在尚未结束的视觉刷新工作,决定能否接收新的业务变更。 + /// worker 和本模块写入口查询,刷新未结束时必须继续原续体,不能插入 Console/任务回调写入。 pub fn has_pending_refresh(&self) -> bool { self.refresh.is_some() } - /// 拒绝在未完成的只读刷新内部再次进行业务变更,防止同一 VM 重入。 + /// execute/dispatch/命令提取等入口的共同前置检查,禁止在原只读视觉调用未结束时重入变更。 fn require_idle_refresh(&self) -> Result<()> { anyhow::ensure!( !self.has_pending_refresh(), @@ -645,7 +746,7 @@ impl ReplSession { Ok(()) } - /// 在业务变化后开始或继续展示刷新,保证后续输入不会进入未结束的只读视觉调用。 + /// prepare_program 后的加载、Console/事件/任务回调后调用,按调用模式同步排空或只推进一个刷新步骤。 fn refresh_after_mutation( &mut self, viewports: Option<&std::collections::BTreeMap>, @@ -654,6 +755,7 @@ impl ReplSession { let Some(viewports) = viewports else { return self.refresh_document(); }; + // 传入视口表示 worker 增量模式,只前进一步;目标尺寸副本保存供后续重定向比较。 let progress = self.poll_refresh(true)?; if let Some(work) = &mut self.refresh { work.viewports = Some(viewports.clone()); @@ -664,10 +766,11 @@ impl ReplSession { }) } - /// 同步排空当前刷新状态机,直到全窗口收敛或出现错误。 + /// 测试同步入口与 drain=true resize 调用,循环推进同一刷新直到稳定,不重执行用户业务语句。 fn refresh_document(&mut self) -> Result> { loop { match self.poll_refresh(false)? { + // Pending 仍是活动 Work,只让出 OS 调度机会,不重建 VM 或重新开始源布局。 RefreshProgress::Pending => std::thread::yield_now(), RefreshProgress::Complete(document) => { return Ok(document.map(|document| *document)); @@ -676,7 +779,7 @@ impl ReplSession { } } - /// 推进一次刷新状态机,保存挂起的 Work;只有全窗口收敛时才生成可发布文档。 + /// refresh_after_mutation/advance_idle_refresh/同步循环共用,消费或保存 RefreshWork 并统一处理故障清理。 fn poll_refresh(&mut self, record_yield: bool) -> Result { // 从 self 取出工作以便可变推进,挂起时放回;首次从已发布各窗口文档克隆初始草稿。 let mut work = self.refresh.take().unwrap_or_else(|| RefreshWork { @@ -692,22 +795,26 @@ impl ReplSession { work: 0, viewports: None, }); + // 暂存 step 错误以便仍能显式取消 Work 内的续体;不能在这里直接 ? 丢掉 work。 let result = (|| { if self.step_refresh(&mut work)? { return Ok(true); } if record_yield { + // 只记录真正把控制权交回 worker 的增量步,同步排空循环不累计这种诊断次数。 self.bridge.note_refresh_yield()?; } Ok(false) })(); match result { Ok(false) => { + // 原样保存根/检查点/窗口队列和续体,下次 poll_refresh 从同一拥有域恢复。 self.refresh = Some(work); Ok(RefreshProgress::Pending) } // 收敛后统一组合文档,再恢复主窗口选择;收尾失败也标故障,不把失败草稿当已发布帧。 Ok(true) => { + // finish_refresh 验证整份多窗口文档后才更新本会话完成基线,仍由 worker 决定共享发布。 let result = self.finish_refresh(work); self.bridge.select_window(crate::window::MAIN_WINDOW)?; if result.is_err() { @@ -736,11 +843,12 @@ impl ReplSession { } } - /// 执行一个刷新步骤:刷新绑定、选择窗口、准备快照、推进布局或绘制,并决定是否需要下一轮。 + /// poll_refresh 每量子调用一次,推进稳定循环、一个窗口或一片布局/绘制;true 只表示全轮无新增变化。 fn step_refresh(&mut self, work: &mut RefreshWork) -> Result { // 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。 // 每轮先使属性绑定达到固定点,再取窗口顺序;生命周期或绑定可能改变窗口/页面结构,因此需要多轮确认。 if work.pass_start { + // 绑定/生命周期可能改窗口登记,新的稳定轮必须重新刷新绑定并获取窗口顺序。 self.bridge.refresh_bindings(&mut self.candidate.vm)?; work.windows = self.bridge.window_ids()?.into(); work.current = None; @@ -750,12 +858,14 @@ impl ReplSession { } // 阶段二:逐个选择窗口;一轮发生变化时再扫描一轮,直到依赖不再引起新帧。 let Some(id) = work.current else { + // 没有当前窗口时先取队列下一项,本次只做状态推进,下次才进入其 VM 布局/绘制。 if let Some(id) = work.windows.pop_front() { work.current = Some(id); return Ok(false); } // 任何窗口本轮生成新帧,就再扫描一轮检查依赖和挂载回调是否又引起变化,直到整轮无变化。 if work.pass_changed { + // 有帧变化就再完整扫一轮,才能发现 hooks/依赖引起的二次变化;不能第一轮结束就发布。 work.pass_start = true; return Ok(false); } @@ -764,11 +874,13 @@ impl ReplSession { self.bridge.select_window(id)?; // 已进入 paint 的窗口直接恢复原绘制续体,不重放业务回调或布局。 if let Some(paint) = work.paint.take() { + // 取回配对文档与源 paint Work,不重新 measure/arrange,也不把原业务输入再执行一遍。 let PendingPaint { window, document, work: paint_work, } = *paint; + // 同一 owning VM 上恢复固定预算的一片绘制,Complete/Pending 由共同接收器处理。 let progress = self .bridge .resume_paint_frame(&mut self.candidate.vm, *paint_work)?; @@ -776,18 +888,23 @@ impl ReplSession { } // 阶段三:恢复旧布局,或在快照/依赖确实变化时建立新布局。 let progress = if let Some(pending) = work.pending.take() { + // 原布局仍挂起时恢复,不能重新提取另一份根让当前续体读写不同展示上下文。 self.bridge .resume_layout_frame(&mut self.candidate.vm, *pending)? } else if let Some(root) = self.bridge.prepare_frame(&mut self.candidate.vm)? { + // 双层 Option 内层 None 也代表有空帧工作;保存准确根供布局完成后 paint 复用。 work.root = root.clone(); self.bridge .begin_layout_frame(&mut self.candidate.vm, root)? } else { + // 没有任何本窗变化才切下一窗,不能把“需发布空帧”误归为这一无工作分支。 work.current = None; return Ok(false); }; + // 未完成布局没有可用完整几何,必须保留 Work;Complete 才可以组装后续待绘制文档。 let frame = match progress { crate::ui_bridge::LayoutProgress::Pending(pending) => { + // current 和 root 保留不动,下次量子仍为此窗口续体继续执行。 work.pending = Some(pending); return Ok(false); } @@ -813,8 +930,10 @@ impl ReplSession { // 跨所有窗口和稳定轮次累计布局工作,空帧也计一,防止持续无几何变化请求绕过收敛预算。 work.work += document.frame.mounts.len().max(1); if work.work > 200_000 { + // 即使每次单帧都有限,失效反馈也可能永不收敛,这个累计预算禁止无限循环刷新。 bail!("layout dependencies did not stabilize within the rendering work budget"); } + // Frame 转移给 source_paint,document 暂留其他元数据;PendingPaint 会把两者重新配对保存。 let progress = self.bridge.begin_paint_frame( &mut self.candidate.vm, document.ui_root.clone(), @@ -823,7 +942,7 @@ impl ReplSession { Self::accept_window_paint(work, id, document, progress) } - /// 接收指定窗口的绘制进度,将挂起 Work 保留或把完整文档存入本轮结果。 + /// step_refresh 启动/恢复 paint 后调用,挂起就保留配对状态,完成就写逐窗草稿并标记需要稳定性复扫。 fn accept_window_paint( work: &mut RefreshWork, id: u64, @@ -832,6 +951,7 @@ impl ReplSession { ) -> Result { match progress { crate::source_paint::Progress::Pending(paint) => { + // 没完整 DisplayList 时绝不替换 documents,原完整画面仍可供 GUI 使用。 work.paint = Some(Box::new(PendingPaint { window: id, document, @@ -839,6 +959,7 @@ impl ReplSession { })); } crate::source_paint::Progress::Complete(draft) => { + // 绘制完成的 frame 含实际显示列表,验证通过再进入本轮草稿基线。 document.frame = draft.frame; document.validate()?; // 只有整个绘制 Complete 且验证通过才替换当前窗口草稿;Pending 保留配对文档和 Work,不对 GUI 可见。 @@ -850,16 +971,19 @@ impl ReplSession { Ok(false) } - /// 收敛后组合所有窗口文档,保留本轮完整结果;没有变化时不额外制造发布。 + /// poll_refresh 收到全轮稳定后调用,完成关闭资源清理并生成一次多窗口发布值;不会直接写 NamespaceState。 fn finish_refresh(&mut self, mut work: RefreshWork) -> Result> { // 收敛后完成已关闭窗口退役并删其旧文档;没有实际变化时不制造额外 revision。 self.bridge.finish_closed(&mut self.candidate.vm)?; + // 以桥接真实仍登记窗口为准删去旧文档,不让已关闭副窗口在下轮凭缓存“复活”。 let ids = self.bridge.window_ids()?; work.documents.retain(|id, _| ids.contains(id)); if !work.changed { + // 无新展示变化仍保存清理后的基线,但返回 None 避免增加无意义外部 revision。 self.documents = work.documents; return Ok(None); } + // 主窗可能尚无自己的已画文档,构造带 session 的空基线以容纳新副窗口和源码投影。 let mut main = work .documents .get(&crate::window::MAIN_WINDOW) @@ -901,22 +1025,25 @@ impl ReplSession { self.resize_windows_incremental(viewports, true) } - /// 用最新视口替换旧目标,显式取消未完成布局,同时保留已完成窗口结果和已发生的业务效果。 + /// resize_windows_incremental 检测目标尺寸集合变化时调用,取消当前续体并重建稳定轮,不回滚业务写入。 fn retarget_refresh( &mut self, viewports: &std::collections::BTreeMap, ) -> Result<()> { + // 没有挂起刷新就无需重定向,外层随后直接把新尺寸送入正常 set_viewport 路径。 let Some(mut work) = self.refresh.take() else { return Ok(()); }; // 记录实际取消了哪类续体用于诊断;已完成窗口草稿和已发生业务副作用不会因此回滚。 let cancelled = work.pending.is_some(); + // 两个独立诊断位记录实际取消阶段,不能把所有重启都记成布局取消。 let cancelled_paint = work.paint.is_some(); if let Some(paint) = work.paint.take() && let Err(error) = self.bridge .cancel_paint_frame(&mut self.candidate.vm, paint.window, *paint.work) { + // 取消 paint 失败后域不再继续正常执行,不把残留续体草稿交给新一轮目标使用。 self.faulted = true; return Err(error); } @@ -925,6 +1052,7 @@ impl ReplSession { .bridge .cancel_layout_frame(&mut self.candidate.vm, *pending) { + // layout 取消还要恢复 Mount 检查点,失败同样停域,不能仅删 Pending Rust 值。 self.faulted = true; return Err(error); } @@ -932,9 +1060,6 @@ impl ReplSession { self.faulted = true; return Err(error); } - // Keep completed window drafts and callback effects. Only the pending - // window's speculative Mount state was restored. A new external target - // begins a fresh convergence budget, not a continuation of a bad cycle. // 重新从全窗口稳定轮开始,清当前选择/旧根并重置此次外部尺寸目标的收敛工作预算;草稿 documents 保留。 work.windows.clear(); work.current = None; @@ -947,7 +1072,7 @@ impl ReplSession { Ok(()) } - /// 把各窗口最新尺寸送入同一运行时;视口更新可跨轮次挂起,也可由调用方同步排空。 + /// api::worker 的窗口 resize 和挂起刷新推进调用;同目标继续原 Work,新目标才显式取消并重定向。 pub fn resize_windows_incremental( &mut self, viewports: &std::collections::BTreeMap, @@ -963,24 +1088,30 @@ impl ReplSession { .and_then(|work| work.viewports.as_ref()) .is_some_and(|previous| previous != viewports); if retarget { + // 外部目标确实改变才重启,避免每次 worker 轮询都把慢布局从头做一遍。 self.retarget_refresh(viewports)?; } // 同目标 drain=true 同步排空旧刷新并保存完整结果;drain=false 只推进一量子,不能先执行新的尺寸变更。 let previous = if self.has_pending_refresh() && !retarget { if drain { + // 同步调用者要求先完成原目标的完整结果,再处理本次输入的尺寸变更。 let result = self.refresh_document(); self.finish_refresh_update(result)? } else { + // 增量轮本次只继续原视觉调用,不能在其只读挂起中插入 set_viewport 写入。 return self.advance_idle_refresh(); } } else { None }; + // 过滤仍属于此会话的窗口,GUI 可能还保留旧运行时窗口尺寸记录,不能自动创建同 ID 新资源。 let ids = self.bridge.window_ids()?; + // 聚合真正改变尺寸的窗口,重复通知不应引起重新布局或派生版本反馈。 let mut changed = false; // 仅更新桥接仍注册的窗口;旧 GUI 尺寸记录不应在新域里隐式创建窗口资源。 for (id, viewport) in viewports { if ids.contains(id) { + // 每次选明确窗口,使 ui.viewport 等隐式上下文读到对应窗口的尺寸输入。 self.bridge.select_window(*id)?; changed |= self .bridge @@ -993,11 +1124,13 @@ impl ReplSession { return Ok(previous); } if drain { + // 新刷新没变化时仍返回上面已排空的 previous,不让同步路径丢失完整已就绪结果。 let result = self .refresh_document() .map(|document| document.or(previous)); self.finish_refresh_update(result) } else { + // 只推进一次,目标集合随 Work 保存,下轮据此辨别“续跑”还是“真正新 resize”。 let result = self.advance_idle_refresh(); if let Some(work) = &mut self.refresh { work.viewports = Some(viewports.clone()); @@ -1006,8 +1139,9 @@ impl ReplSession { } } - /// 在空闲轮次推进一个刷新步骤,完成时执行统一结果收尾。 + /// resize 增量路径调用,推进一片并将 Pending 映成 None;仍需 has_pending_refresh 判断是否已空闲。 fn advance_idle_refresh(&mut self) -> Result> { + // 先保存错误/完成值供 finish_refresh_update 清理,不能因 poll 报错跳过未发布请求撤销。 let result = self.poll_refresh(true).map(|progress| match progress { RefreshProgress::Pending => None, RefreshProgress::Complete(document) => document.map(|document| *document), @@ -1015,31 +1149,30 @@ impl ReplSession { self.finish_refresh_update(result) } - /// 统一处理刷新失败、未发布状态清理和运行时引用回收。 + /// resize/advance_idle_refresh 共用收尾,错误时丢未发布命令,空闲后再回收不可达托管根。 fn finish_refresh_update( &mut self, result: Result>, ) -> Result> { if result.is_err() { + // GUI 继续使用旧完整文档,失败草稿的副作用命令不能外泄为 Resize/Focus 等原生动作。 self.take_window_commands()?; self.bridge.discard_pending(&mut self.candidate.vm)?; } if !self.has_pending_refresh() { + // 仍挂起时临时对象属于原视觉调用,不能在其中完整回收;只在续体结束边界 reclaim。 self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; } result } - /// Deliver ready work on the owning VM worker, never from the GUI or a - /// provider thread. A failed callback stops the same shared fault domain. - /// 同步推进可交付任务和必要刷新,供不使用分片 worker 的调用方使用。 + /// 同步调用方/测试使用,在 owning VM 推进就绪任务并排空展示;不允许 GUI 或 provider 线程调用 VM。 pub fn poll_tasks(&mut self) -> Result> { self.poll_tasks_mode(None) } - /// Idle delivery starts at most one callback, then parks its layout work. - /// 按有界任务交付量推进,再以可挂起刷新处理变化。 + /// api::worker 空闲轮调用,每轮至多推进一个任务,然后允许其布局/绘制挂起,给前台输入公平机会。 pub fn poll_tasks_incremental( &mut self, viewports: &std::collections::BTreeMap, @@ -1047,13 +1180,14 @@ impl ReplSession { self.poll_tasks_mode(Some(viewports)) } - /// 按本轮预算交付已完成任务或推进协程,再对其业务变化执行相同的展示刷新流程。 + /// poll_tasks/poll_tasks_incremental 共同执行路径,任务错误停用同一故障域,成功变更走正常刷新状态机。 fn poll_tasks_mode( &mut self, viewports: Option<&std::collections::BTreeMap>, ) -> Result> { // 异步回调不能插入只读布局/绘制续体中,即使后台邮箱已有结果也等刷新空闲边界。 if self.has_pending_refresh() { + // 只读视觉续体尚未结束时不取邮箱/回调写业务属性,先让 worker 继续原刷新。 return Ok(None); } if self.faulted { @@ -1064,17 +1198,21 @@ impl ReplSession { } return Ok(None); } + // 表示确有任务推进/清理,不等于某个协程已经 Completed;用于决定是否需要 reclaim。 let mut completed = false; + // 保留调度/刷新错误供下方统一停域、取消任务、丢弃未发布命令,避免中途直接返回。 let result = (|| { // worker 增量轮至多启动一个任务推进,给前台输入和窗口刷新公平机会;同步路径单轮允许64项。 let limit = if viewports.is_some() { 1 } else { 64 }; if self.bridge.poll_tasks(&mut self.candidate.vm, limit)? == 0 { + // 无实际推进就不启动视觉刷新,等待 I/O/计时器不会凭轮询本身制造新帧。 return Ok(None); } completed = true; self.refresh_after_mutation(viewports) })(); if result.is_err() { + // 任一共享 VM 任务回调故障会停整个域,不只把当前 Task 标失败后继续执行其他不安全回调。 self.faulted = true; self.bridge.cancel_tasks(&mut self.candidate.vm)?; self.take_window_commands()?; @@ -1088,7 +1226,7 @@ impl ReplSession { result } - /// 取得本轮已稳定运行时的窗口命令,挂起刷新时禁止提前取走。 + /// api::worker 准备一次发布/错误丢弃时调用,空闲边界才可排空命令,避免从未稳定草稿提前执行 OS 动作。 pub fn take_window_commands(&self) -> Result> { self.require_idle_refresh()?; self.bridge.take_window_commands(&self.candidate.vm) @@ -1107,6 +1245,7 @@ impl ReplSession { if !self.bridge.set_viewport(&mut self.candidate.vm, viewport)? { return Ok(None); } + // 同步测试 resize 保存刷新错误,随后同样丢弃失败请求并回收引用,不让 ? 跳过清理。 let result = self.refresh_document(); if result.is_err() { self.take_window_commands()?; @@ -1117,9 +1256,10 @@ impl ReplSession { result } - /// 关闭尚未完成的刷新任务,释放续体并恢复未提交 Mount;不能只丢弃 Work 结构。 + /// shutdown 和 ReplSession::drop 调用,显式取消 owning VM 上挂起的 paint/layout,不回放源程序。 fn cancel_pending_refresh(&mut self) -> Result<()> { if let Some(mut work) = self.refresh.take() { + // 从会话移走 Work 后逐项清理,检查点/临时 VM 根不能仅靠 Rust 值析构替代。 if let Some(paint) = work.paint.take() { self.bridge.cancel_paint_frame( &mut self.candidate.vm, @@ -1135,35 +1275,36 @@ impl ReplSession { Ok(()) } - /// 有界关闭任务、挂载和窗口,释放运行时根,并报告清理结果。 + /// api::worker 退役运行时调用,取消视觉续体并关闭任务/挂载/窗口;Candidate 的根仍由其 close/Drop 释放。 pub fn shutdown(&mut self) -> Result<(usize, usize, usize)> { // 先取消视觉续体恢复挂载检查点,再判断原域是否允许正常关闭回调,随后标故障禁止后续普通执行。 self.cancel_pending_refresh()?; + // 正常域允许卸载钩子,已经故障的域只做宿主清理,不能再次执行不可靠的源回调。 let callbacks = !self.faulted; self.faulted = true; self.bridge.shutdown(&mut self.candidate.vm, callbacks) } - /// 撤销指定窗口的运行时展示资源,防止后续刷新重新发布它。 + /// worker 在故障域处理原生关闭时调用,只退役指定副窗口与缓存,不允许再执行源生命周期钩子。 pub fn retire_window(&mut self, id: u64) -> Result<()> { self.require_idle_refresh()?; - // This path is exclusively host cleanup after the domain has stopped. - // No language functions, layout, paint or lifecycle hooks may run here. + // 只用于域已停止后的宿主清理,不能将此路径当作正常 Window.close 源方法的替代。 self.faulted = true; // 专用退役只清宿主资源,不调源生命周期;删除已缓存文档和窗口命令,避免后续重发该副窗口。 self.bridge.retire_window(&mut self.candidate.vm, id)?; self.documents.remove(&id); + // 故障退役只排空命令并丢弃,不能把失败域旧 Focus/Resize 发布到当前原生资源。 let _ = self.take_window_commands()?; self.bridge .reclaim(&mut self.candidate.vm, &mut self.candidate.roots) } - /// 查询当前运行时是否仍接受对应的窗口或拖放目标。 + /// api::worker 接受 tab-drop 前调用,验证两端发布版本/页面顺序,普通非拖放信封由 bridge 返回 true。 pub fn accepts_tab_drop(&self, input: &crate::events::Input) -> bool { self.bridge.accepts_tab_drop(&self.candidate.vm, input) } - /// 查询当前运行时是否仍接受对应的窗口或拖放目标。 + /// api::worker 接纳窗口输入前调用,只查询资源是否仍登记且未 closing,后面仍需 session/Mount 校验。 pub fn accepts_window(&self, id: u64) -> bool { self.bridge.accepts_window(id) } @@ -1179,7 +1320,7 @@ impl ReplSession { self.dispatch_mode(input, None) } - /// 分发一次输入回调,再把视觉刷新交给 worker 分片推进。 + /// api::worker 的原生/HTTP 输入任务调用,在同一会话执行一次回调,然后分片推进其视觉变化。 pub fn dispatch_incremental( &mut self, input: &crate::events::Input, @@ -1188,7 +1329,7 @@ impl ReplSession { self.dispatch_mode(input, Some(viewports)) } - /// 在正确窗口上下文执行输入回调,再刷新展示;故障时停止受影响域并清理未完成工作。 + /// dispatch/dispatch_incremental 共用入口,核对运行时代际/拖放目标,再选窗分派并恢复主窗口上下文。 fn dispatch_mode( &mut self, input: &crate::events::Input, @@ -1207,8 +1348,10 @@ impl ReplSession { } // 将 ui.window/viewport 等隐式上下文切到事件所属窗口,分发后统一恢复主窗口。 self.bridge.select_window(input.window)?; + // 暂存回调/刷新错误,外层始终尝试恢复主窗口选择、丢弃未发布结果并回收空闲根。 let result = (|| { self.bridge.dispatch(&mut self.candidate.vm, input)?; + // 输入已执行一次,后续只续跑视觉刷新,不能为多片 layout 再调用同一 click 回调。 let document = self.refresh_after_mutation(viewports)?; Ok(ReplResult { value: "nil".into(), @@ -1245,15 +1388,16 @@ impl Drop for ReplSession { } } -/// 递归修正代码投影的源行号,使拼接输入仍可定位到原始文本。 +/// execute_mode 为本轮 Sources 根调用,递归把局部一基行号平移到累计 Console transcript,不改变执行源码。 fn shift_node_lines(node: &mut CodeNode, offset: usize) { node.start_line += offset; node.end_line += offset; for child in &mut node.children { + // 所有后代都保存独立行号,父节点平移不会隐式修正内部函数/表达式的选择范围。 shift_node_lines(child, offset); } } -/// 把 VM 返回值转换为 Console 的可显示表示,不让借用的托管引用跨线程。 +/// execute_mode 在保存全局 _ 前调用,把真实返回值/新绑定值转成显示字符串;返回根仍由调用者释放。 fn value_repr(vm: &Vm, value: &Value) -> Result { Ok(match value { Value::Nil => "nil".into(), @@ -1266,8 +1410,11 @@ fn value_repr(vm: &Vm, value: &Value) -> Result { /// 拥有 VM、全局字典与宿主保留根;关闭时按顺序释放,失败也必须收尾。 struct Candidate { + // 实际解释器/托管堆,仅执行 worker 线程操作,不放入共享 NamespaceState。 vm: Vm, + // 宿主安装期间保留的拥有根,close 逆序 pop;ObjectRef Rust 数值本身没有自动 release 行为。 roots: Vec, + // 主全局字典也在 roots 中,先清字典断业务根,再统一释放根,避免闭包环残留。 globals: Option, } @@ -1281,10 +1428,11 @@ impl Candidate { }) } - /// 释放候选拥有的宿主根并清理 VM;通过显式关闭保证错误也能被报告。 + /// compile/evaluate 显式调用,Candidate::drop 兜底调用;按拥有顺序拆根并验证堆没有遗留对象。 fn close(&mut self) -> Result<()> { // 清全局字典先断业务对象可达根,再逆序释放宿主分配根;最后执行循环回收并验证对象计数归零。 if let Some(globals) = self.globals.take() { + // take 使重复 close 不重复清同一身份;先断用户全局再释放类型/函数等宿主根。 self.vm.dict_clear(globals)?; } // pop 逐项移出确保再次 close 不重复释放已成功处理的根;错误仍交显式关闭调用方报告。 @@ -1293,6 +1441,7 @@ impl Candidate { } self.vm.collect_cycles(); if self.vm.object_count() != 0 { + // 源程序返回了纯数据也不掩盖资源泄漏,显式 close 会把这个错误报告给调用者。 bail!("candidate VM retained objects after cleanup"); } Ok(()) @@ -1307,13 +1456,14 @@ impl Drop for Candidate { } } -/// 把 VM 错误与当前源位置/调用栈组合成用户可读的诊断。 +/// 本模块和 bridge/tasks 的 VM 错误映射调用,立即取 last_traceback 拼成纯文本,避免后续调用覆盖故障栈。 pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error { // 错误发生后立即读取 VM traceback,保留函数、源文件与指令偏移;无栈信息时直接保留原 VmError。 let locations = vm .last_traceback() .iter() .map(|frame| { + // 每帧保留来源文件、函数名和指令位置,不把托管异常对象直接跨线程交付。 format!( "{}:{} in {} (instruction {})", frame.filename, frame.first_line, frame.function, frame.instruction_offset @@ -1322,8 +1472,10 @@ pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error { .collect::>() .join("\n"); if locations.is_empty() { + // 无 VM 栈时保留原始 VmError,不能凭当前源码猜一个错误行号。 error.into() } else { + // 含栈则附加每帧可读位置,Console/API 不需读取 VM 就能展示错误。 anyhow::anyhow!("{error}\n{locations}") } } @@ -1331,18 +1483,21 @@ pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error { #[derive(Default)] /// 编译前的语法节点数量/嵌套预算,限制输入结构复杂度。 struct AstBudget { + // 当前递归路径深度,leave_node 归还;不能与累计节点数共用一个计数。 depth: usize, + // 已访问节点总量,多个 Script 根共享累计,leave_node 不减少。 nodes: usize, } impl Visitor for AstBudget { type Error = anyhow::Error; - /// 更新源结构遍历的深度/数量预算,拒绝超出允许范围的语法树。 + /// Aster Visitor 在进入任意语法节点时调用,compile/evaluate/加载/Console 共用这个结构预算。 fn enter_node(&mut self, _span: Span) -> Result<()> { // 每次进入节点累计总数和当前深度;leave 只归还深度,整份输入总量不会因退出子树重置。 self.depth += 1; self.nodes += 1; if self.depth > MAX_AST_DEPTH || self.nodes > MAX_AST_NODES { + // 在加载/执行代码前停止,防止巨大源码结构绕过仅约束运行指令的 LIMITS。 bail!("Aster AST exceeds the 64-level or 10000-node host budget"); } Ok(()) @@ -1353,7 +1508,7 @@ impl Visitor for AstBudget { } } -/// 把输入语法结构转换为 Sources 工具使用的 CodeNode 层次,保存原始源码位置。 +/// compile/prepare_program/execute_mode 调用,将已解析 AST 投影为 Sources 浏览树,不保存可执行托管引用。 fn architecture( source: &SourceUnit, plan: &SourcePlan, @@ -1366,9 +1521,11 @@ fn architecture( next_id: 0, identity: format!("{namespace}@{submission}"), }; + // 把各根按源顺序拼入一个显示根,后续 CodeNode ID 在这一次 tree 构造中递增。 let mut children = Vec::new(); for root in plan.roots() { match root { + // 模块展示声明树,脚本展示每段语句树,表达式模式保留单表达式层次。 AstRoot::Module(program) => children.extend( program .declarations @@ -1381,12 +1538,14 @@ fn architecture( AstRoot::Expression(expression) => children.push(tree.expression(expression)), } } + // 只借根列表取首尾跨度,不克隆整棵 AST;Script 的 span 已由 endor_script 映回原文件。 let roots = plan.roots(); // 整体范围取首根起点至末根终点,Script 的多个交互片段在原始文本中的 span 已恢复偏移。 let span = Span::new( roots.first().map_or(0, |root| root.span().start), roots.last().map_or(0, |root| root.span().end), ); + // 代码根种类是 Sources 显示分类,不参与 VM 模块/脚本执行方式的选择。 let kind = if matches!(plan, SourcePlan::Module(_)) { "module" } else { @@ -1395,13 +1554,15 @@ fn architecture( tree.node(namespace.to_owned(), kind, span, children) } -/// 建立源文本每行起点表,供字节 span 转换为行列位置。 +/// architecture/execute_mode 调用,建立字节偏移→行号的搜索索引,兼容 CRLF、LF 和孤立 CR。 fn line_starts(source: &str) -> Vec { + // Span 使用 UTF-8 字节单位,所以不用 chars().enumerate() 的字符索引匹配它。 let bytes = source.as_bytes(); // 按字节扫描行终止符,CRLF 仅在 LF 记一次,孤立 CR 也换行;空文本仍保留第1行起点0。 let mut starts = vec![0]; for (offset, byte) in bytes.iter().enumerate() { if *byte == b'\n' || (*byte == b'\r' && bytes.get(offset + 1) != Some(&b'\n')) { + // 记录换行符之后的下一行首,CRLF 只在 LF 分支记录一次,空末行也有可定位起点。 starts.push(offset + 1); } } @@ -1410,14 +1571,18 @@ fn line_starts(source: &str) -> Vec { /// 构造 Sources 语法投影所需的源码与定位辅助状态。 struct Architecture { + // 排好序的字节行首,node 通过 partition_point 得到一基行号。 line_starts: Vec, + // 单次代码投影中的序号,高水位只用于 UI ID,不是 VM 对象身份。 next_id: usize, + // namespace@submission 前缀,区分不同提交/Console 输入产生的 AST 节点。 identity: String, } impl Architecture { - /// 分配本投影内节点 ID,并将半开字节 span 映射为1基起止行,组装 CodeNode。 + /// declaration/function/statement/expression 共用的纯数据分配助手,统一 ID 与半开 span→一基行号转换。 fn node(&mut self, name: String, kind: &str, span: Span, children: Vec) -> CodeNode { + // 先取当前序号再递增,父子节点构建顺序可不同但同投影内身份唯一。 let id = self.next_id; self.next_id += 1; // partition_point 返回不大于 offset 的行首数量,天然得到1基行号;span.end 是半开端,先减1找最后实际字符行。 @@ -1432,7 +1597,7 @@ impl Architecture { } } - /// 区分函数与类声明;类下先投影字段,再投影方法。 + /// architecture 的模块声明和 statement 的声明分支调用,区分普通函数与含字段/方法的类。 fn declaration(&mut self, declaration: &Decl) -> CodeNode { match &declaration.kind { DeclKind::Function(function) => self.function(function), @@ -1449,7 +1614,7 @@ impl Architecture { } } - /// 将函数声明及其体内语句投影为一个带源码范围的 function 节点。 + /// declaration 的函数和类方法分支调用,逐条投影函数体;参数包含在源码范围中但不另建显示节点。 fn function(&mut self, function: &frontend::ast::Function) -> CodeNode { // 函数投影按函数体语句顺序递归展开,不单独生成参数节点;展示范围仍覆盖完整函数声明。 let children = function @@ -1466,25 +1631,30 @@ impl Architecture { ) } - /// 按赋值、返回、表达式和声明种类构建 Sources 层次,保留真实子表达式关系。 + /// architecture/function/分支体递归调用,生成语句浏览节点,不执行条件、不改变 AST 或业务对象。 fn statement(&mut self, statement: &Stmt) -> CodeNode { match &statement.kind { StmtKind::If(branch) => { + // 条件是第一个孩子,随后 then/else 依源顺序展开;树不预测运行时会走哪支。 let mut children = vec![self.expression(&branch.condition)]; children.extend(branch.body.statements.iter().map(|s| self.statement(s))); if let Some(body) = &branch.otherwise { + // 没有 else 就不造空显示分支,保持 Sources 仅浏览实际存在的源码。 children.extend(body.statements.iter().map(|s| self.statement(s))); } self.node("if".into(), "if", statement.span, children) } StmtKind::While(loop_) => { + // 只展开一次静态循环体,不能按运行次数复制显示节点或重演循环。 let mut children = vec![self.expression(&loop_.condition)]; children.extend(loop_.body.statements.iter().map(|s| self.statement(s))); self.node("while".into(), "while", statement.span, children) } // 成员赋值投影两个子表达式:目标对象和右值,成员名作为本节点显示名称。 StmtKind::MemberAssignment(assignment) => { + // 成员访问目标也可能是复合调用,独立投影才能浏览到其中的对象表达式。 let object = self.expression(&assignment.object); + // 右值另建子树,不把赋值目标名字混进计算表达式的显示类型。 let value = self.expression(&assignment.value); self.node( assignment.name.text.clone(), @@ -1494,6 +1664,7 @@ impl Architecture { ) } StmtKind::Binding(binding) => { + // 显示绑定名字,孩子保留初始值表达式及其完整范围。 let child = self.expression(&binding.value); self.node( binding.name.text.clone(), @@ -1503,6 +1674,7 @@ impl Architecture { ) } StmtKind::Assignment(assignment) => { + // 和 let 显示类型区分,Sources 可分清声明与之后更新同一名字的语句。 let child = self.expression(&assignment.value); self.node( assignment.name.text.clone(), @@ -1512,6 +1684,7 @@ impl Architecture { ) } StmtKind::Return(returned) => { + // Option::iter 保持无值 return 的孩子列表为空,不凭空补一个 nil AST 节点。 let children = returned .value .iter() @@ -1524,8 +1697,9 @@ impl Architecture { } } - /// 递归投影表达式层次,用简短文本作显示名,并保留完整原始 span。 + /// statement 和 expression 自递归调用,保留运算/成员/调用层次,显示标签可截短但源范围不截短。 fn expression(&mut self, expr: &Expr) -> CodeNode { + // 三元组统一提供 UI 名称、种类和孩子,最后仍用同一个 node 助手分配 ID/行号。 let (name, kind, children) = match &expr.kind { ExprKind::Name(name) => (name.clone(), "name", vec![]), ExprKind::Number(value) => (value.clone(), "number", vec![]), @@ -1542,6 +1716,7 @@ impl Architecture { vec![self.expression(left), self.expression(right)], ), ExprKind::Member { object, name } => { + // 成员名作显示标签,对象表达式保留为孩子,类型/属性读取语义不在此求值。 (name.text.clone(), "member", vec![self.expression(object)]) } ExprKind::Call { callee, arguments } => { diff --git a/src/runtime/tasks.rs b/src/runtime/tasks.rs index ae34f11..05e5363 100644 --- a/src/runtime/tasks.rs +++ b/src/runtime/tasks.rs @@ -2,8 +2,10 @@ //! 任务可能处于等待 I/O、定时器、可执行协程或等待另一任务的状态;取消 owner/key 不能让晚到结果再次执行。 //! 调度使用有界量子和公平推进;await 挂起后保留操作数与帧,不重放此前副作用。失败和依赖环有明确处理路径。 //! -//! Owner-managed asynchronous work. Only weak identities enter the scheduler; -//! callbacks and scopes are ordinary traced edges in the Aster object graph. +//! ui_bridge::bootstrap 调用 install,注册 ui.after/sleep/readText/fetchText/spawn 并取得每 VM 的 Scheduler。 +//! 源 API 经 After 创建 Task;UiBridge/endor_host 的轮询调用 Scheduler::poll,才在所属 VM 交付结果或推进协程。 +//! 页面/窗口关闭调用 cancel_owner,运行时退出调用 quiesce;只取消身份和执行边,不允许旧 I/O 结果复活它们。 +//! 这里同时含通用调度和 Tessera 展示资格检查,未来迁移 Nexus 时需将 scope/eligible 的 UI 规则留在宿主适配中。 use std::{ sync::{ Arc, Mutex, @@ -22,8 +24,11 @@ use crate::{block::Block, mount::Mount, ui_bridge::desktop_type, window::Managed // 普通对象的私有任务字典属性名;Block/Mount/Window 则把同一拥有关系保存在宿主 payload。 const SCOPE: &str = "\0tessera.tasks"; +// 单 VM 弱调度索引的活任务预算;已完成任务可被源变量持有,但不继续占调度槽。 const MAX_TASKS: usize = 1024; +// 单 owner 作用域字典的项数上限;同 key 替换不新增一项,允许在满额度时进行。 const MAX_OWNER_TASKS: usize = 64; +// 一轮 poll 最多完成/推进/取消的工作数,避免任务队列独占 VM worker。 const MAX_READY: usize = 64; // 调度器只保存弱任务身份,不因排队把任务、回调或 owner 隐式保活。 @@ -32,14 +37,19 @@ type Index = Arc>>; #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// 任务对源代码可见的生命周期状态,完成/取消/失败各有不同 await 语义。 enum State { + // 等截止时间、I/O 或首次推进;不表示后台操作尚未开始。 Pending, + // 普通回调正在执行,或协作任务已有保存的协程,仍可能处于 await 等待。 Running, + // 正常终态,允许 await/result 读取保存的结果或 Nil。 Completed, + // 失去 owner/key 资格或主动取消的终态,await 应报告 AwaitCancelled。 Cancelled, + // provider/回调/协程失败,error 保存诊断而非可当作成功值交付的文字。 Failed, } impl State { - /// 返回内部枚举或对象对应的稳定源名称。 + /// Task.status 方法调用,返回稳定的小写 API 状态字符串,不用于控制调度分支。 fn name(self) -> &'static str { match self { Self::Pending => "pending", @@ -56,21 +66,29 @@ impl State { struct Task { // owner 通过 scope 字典强持有 Task,Task 反向弱引用 owner,避免任务把已关闭页面永久保活。 owner: WeakObjectRef, + // owner 内唯一键,作用域字典决定它是否仍是当前任务;不是跨运行时的全局 ID。 key: String, // 单调时钟截止点;ready 与到期是两个独立条件,I/O 完成不受系统日期调整影响。 due: Instant, + // 定时/I/O 的零参回调或 spawn 的初始函数,由 trace 声明真实强边。 callback: Option, + // 真实执行状态;只读 status 可能因失去资格投影为 cancelled,而不立刻改写此字段。 state: State, // 定时/协作任务创建即 ready,但仍等 due;I/O 任务等 provider 结果写回后才 ready。 ready: bool, + // I/O 成功纯文本,普通回调执行期间也可通过 Task.result 查询;非托管字符串不需 trace。 text: Option>, + // provider/执行失败的有界预览,Completed 的成功值不能与此混用。 error: Option>, // 仅任务 payload 持有 I/O 租约;取消/终态清除此边即可撤销后台交付并归还额度。 lease: Option>, + // 仅 spawn 为 true:使用可恢复 Coroutine 和软量子,其余回调一次有界调用完成。 cooperative: bool, + // spawn 首次推进创建的托管协程帧,后续 resume 保留此前副作用与累计硬预算。 coroutine: Option, // 协程 await 的对象是真实托管强边;等待期间由它保活被等待值,不以弱调度索引代替。 waiting: Option, + // 协程最终返回的任意语言值;对象结果也经 trace 保活,取消时清掉。 result: Option, } impl ObjectPayload for Task { @@ -86,13 +104,15 @@ impl ObjectPayload for Task { fn reference_assignment_only(&self) -> bool { true } - /// 把 Task 状态投影成 await 的就绪、等待、取消或失败结果,不在等待期间主动轮询执行。 + /// Aster 的 await 执行和 Scheduler 的等待检查调用,查询状态而不递归推进任务或 VM。 fn poll_await(&self) -> Result { match self.state { + // I/O 已收到 ready 文本但尚未真正处理到终态时,await 仍不能抢先越过回调。 State::Pending | State::Running => Ok(endor_core::AwaitPoll::Pending), // 协作任务优先返回保存的任意 Value,I/O 返回 text,普通定时器默认为 Nil。 State::Completed => Ok(endor_core::AwaitPoll::Ready( self.result.clone().unwrap_or_else(|| { + // 协作返回值优先,否则 I/O 文本转语言字符串,纯计时器/sleep 无值则返回 Nil。 self.text .as_ref() .map(|text| Value::Text(text.to_string())) @@ -107,6 +127,7 @@ impl ObjectPayload for Task { } /// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。 fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + // Aster GC 只追踪真实拥有边,owner/调度索引的弱身份均不得在这里访问。 if let Some(callback) = self.callback { visit(callback); } @@ -121,42 +142,46 @@ impl ObjectPayload for Task { } } } -/// 统一构造本模块的参数错误,让调用方停止处理无效输入。 +/// API 解码和 Task 方法检查共用错误构造器,调用位置与源栈由外层 VM 诊断补充。 fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } -/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。 +/// After/Method/作用域遍历解码对象值时调用,只复制句柄,不建立隐藏托管强边。 fn object(value: &Value) -> Result { match value { Value::Object(object) => Ok(*object), _ => Err(invalid("task owner and callback must be managed objects")), } } -/// 取得 owner 关联的托管任务作用域,供 key 替换和取消使用。 +/// After、current、cancel_owner/finish 调用,读取 owner 真正拥有 Task 的字典;不创建新作用域。 fn scope(store: &ObjectStore, owner: ObjectRef) -> Result, VmError> { // scope 的位置依 owner 类别而异;普通对象即使使用私有属性,也要验证该值确为字典。 let payload = store.object(owner)?.payload(); if let Some(value) = payload.as_any().downcast_ref::() { + // Block 的宿主任务边保存在 payload,不能因普通属性同名而被覆盖或伪造。 return Ok(value.tasks); } if let Some(value) = payload.as_any().downcast_ref::() { + // 同业务对象的不同出现可分别拥有任务,Mount 作用域不是 Block 作用域别名。 return Ok(value.tasks); } if let Some(value) = payload.as_any().downcast_ref::() { + // 窗口级任务跟资源生命周期,不随某一页切换自动转移到另一页。 return Ok(value.tasks); } match store.get_attr(owner, SCOPE) { Ok(Value::Object(scope)) => { - // Validate even the private host field before using it as an owner edge. + // 普通对象的私有属性也必须真为字典,不能把任意 Object 当作 owner/key 拥有表。 store.dict_items(scope)?; Ok(Some(scope)) } + // 尚未提交过任务是正常 None,其他读取错误必须传播而非被当作缺省空字典。 Err(VmError::AttributeNotFound(_)) => Ok(None), Ok(_) => Err(invalid("invalid task scope")), Err(error) => Err(error), } } -/// 为可拥有任务的对象安装作用域,真实引用仍由托管图追踪。 +/// After 首次给 owner 创建字典时调用,按 owner 种类建立可追踪强边,不修改其他任务内容。 fn install_scope( cx: &mut NativeContext<'_>, owner: ObjectRef, @@ -165,42 +190,50 @@ fn install_scope( // 宿主 payload 通过 replace_payload 建立受 GC 追踪的强边,普通对象通过 set_attr;均不能绕过写屏障。 let payload = cx.store().object(owner)?.payload(); if let Some(value) = payload.as_any().downcast_ref::() { + // 克隆 payload 后改任务边,replace_payload 负责引用增减与版本记录,不能直接改 store 借用值。 let mut value = value.clone(); value.tasks = Some(scope); return cx.replace_payload(owner, value); } if let Some(value) = payload.as_any().downcast_ref::() { + // 出现位置持字典,不把它挂到共享 Block 导致关闭一个出现时取消另一个窗口的任务。 let mut value = value.clone(); value.tasks = Some(scope); return cx.replace_payload(owner, value); } if let Some(value) = payload.as_any().downcast_ref::() { + // 复用同一写屏障保持窗口资源对象的 GC 关系,不另建 Rust HashMap 持 Task 根。 let mut value = value.clone(); value.tasks = Some(scope); return cx.replace_payload(owner, value); } cx.set_attr(owner, SCOPE, Value::Object(scope)) } -/// 检查任务的 owner 仍有效且任务具备交付条件,避免晚到结果复活已关闭页面。 +/// After 创建、Task.active/status 和 Scheduler 交付前调用,验证 owner 的 Tessera 展示资格,不检查 key 身份。 fn eligible(store: &ObjectStore, owner: ObjectRef, desktop: ObjectRef) -> Result { + // 本函数只做同步借用查询,不升级额外托管根;真正调用者负责在检查/交付期间持有 owner。 let payload = store.object(owner)?.payload(); if let Some(mount) = payload.as_any().downcast_ref::() { if !mount.mounted { + // Mount 对象可被源变量持有,但卸载后任务不能再有展示交付资格。 return Ok(false); } // Mount 仅 mounted 不足以交付,所属 Page 还必须在某个活动窗口页面注册表里,关闭再打开不能恢复旧任务。 if let Some(page) = mount.page { - // No retained root is needed for this synchronous borrowed lookup. + // desktop 指主资源域;在其真实 windows 注册表中找页,不只检查当前 active 页面。 let windows = object(&store.get_attr(desktop, "windows")?)?; for (_, value) in store.dict_items(windows)? { + // 各窗口所有已登记 Page 都算有效,未选中标签不等于页面已关闭。 for candidate in crate::window::pages(store, object(value)?)? { if store.downgrade(*candidate)? == page { + // 比较弱代际身份,避免堆槽复用后把新 Page 误认成原出现的所属页。 return Ok(true); } } } return Ok(false); } + // 没有所属 Page 的 Mount 不具备异步交付资格,不能因 mounted=true 就放行孤立出现。 return Ok(false); } // 窗口 owner 要求未 closed;Page owner 要求仍注册,普通非页面业务 owner 没有额外展示资格要求。 @@ -212,8 +245,10 @@ fn eligible(store: &ObjectStore, owner: ObjectRef, desktop: ObjectRef) -> Result .downcast_ref::() .is_some_and(|block| block.page) { + // Page 需要仍在任一窗口注册表,不能只靠对象可达/标题存在判断是否还可交付。 let windows = object(&store.get_attr(desktop, "windows")?)?; for (_, value) in store.dict_items(windows)? { + // 从字典值解码托管 Window,再按页面身份查登记关系,而非按字符串标题查找。 let window = object(value)?; if crate::window::pages(store, window)?.contains(&owner) { return Ok(true); @@ -221,9 +256,10 @@ fn eligible(store: &ObjectStore, owner: ObjectRef, desktop: ObjectRef) -> Result } return Ok(false); } + // 非展示类普通业务 owner 仅需可达;其作用域/当前 key 条件由 current 另行检查。 Ok(true) } -/// 检查弱索引对应的任务是否仍是 owner/key 当前登记的那一项。 +/// After 替换、Task 方法、Scheduler 和 finish 调用,验证 owner/key 字典仍精确指向本任务,防止旧代际交付。 fn current( store: &ObjectStore, owner: ObjectRef, @@ -233,24 +269,27 @@ fn current( // key 指向的对象必须恰是当前 Task,替换后旧 Task 即使还被外部持有,也不能接收结果或执行回调。 Ok(match scope(store, owner)? { Some(scope) => { + // 外部仍持有旧 Task token 也不能绕过同键替换,必须精确比对 Value::Object 身份。 store.dict_get(scope, &Value::Text(task.key.clone()))? == Some(Value::Object(reference)) } None => false, }) } -/// Called at the actual close/detach boundary, including close-then-reopen in -/// one language input. A later appearance never revives these identities. -/// 取消某个 owner 的全部任务,防止页面/窗口生命周期结束后继续执行回调。 +/// window/页面关闭与 Mount 退役边界调用,取消 owner 全部任务;同一输入 close 后再 open 也不能复活旧任务。 pub fn cancel_owner(cx: &mut NativeContext<'_>, owner: ObjectRef) -> Result<(), VmError> { + // 无作用域表示从未拥有任务,关闭仍正常成功,不为取消动作反而分配一个新字典。 let Some(scope) = scope(cx.store(), owner)? else { return Ok(()); }; // 克隆条目后逐个变更字典;取消 pending/running 的执行边,再删除所有 owner/key 拥有边。 for (key, value) in cx.store().dict_items(scope)?.to_vec() { + // 复制字典项使删除不会使迭代借用失效;每个值必须是当前 VM 的 Task。 let reference = object(&value)?; + // 取消修改副本并经写屏障提交,释放 callback/coroutine/waiting 的真实托管强边。 let mut task = cx.store().payload::(reference, "Task")?.clone(); if matches!(task.state, State::Pending | State::Running) { + // 终态内容不重新改成 Cancelled,但所有 owner/key 字典边最后都会解除。 task.state = State::Cancelled; task.callback = None; task.lease = None; @@ -277,12 +316,19 @@ enum Operation { #[derive(Debug)] /// 创建定时/I/O/协作任务时的原生调用适配器。 struct After { + // 区分计时、无回调 sleep、后台读取与协作 spawn,决定参数形状和初始 ready 状态。 operation: Operation, + // 源 API 和轮询 Scheduler 共用同一个 Provider mailbox,I/O 线程不接触此 VM。 provider: crate::io_tasks::Provider, + // install 按具体 API 构造的完整数量/类型/效果契约,进入 invoke 前由 VM 检查。 signature: endor_core::CallSignature, + // 弱调度索引,不是 Task 的拥有者;真正拥有边在 owner 的作用域字典。 index: Index, + // spawn 创建后置位,让 worker 知道有可推进协程,prune 在轮末重新计算。 runnable: Arc, + // shutdown/quiesce 置位的单向门闩,清理回调不得再提交新任务。 quiescing: Arc, + // 主资源域用于 Task 类型和跨窗口 owner 登记校验,不把 Task 固定到某个 current 页。 desktop: ObjectRef, } impl NativeCallable for After { @@ -295,40 +341,52 @@ impl NativeCallable for After { Some(self.signature.arity) } - /// 先拒绝退出或只读域的新工作,再解码 owner/key/提供方参数,取消同键旧任务并建立受作用域管理的新 Task。 + /// VM 执行 ui.after/sleep/readText/fetchText/spawn 时调用,验证全部输入再建立新 owner/key 身份,最后取消旧项。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // 退出域先拒绝新任务;只读渲染也禁止异步提交,即使某构造操作只分配新的任务对象。 if self.quiescing.load(Ordering::Acquire) { + // 退出门闩优先于任何分配/外部 I/O,卸载回调不能重建任务延长资源域寿命。 return Err(invalid("runtime is exiting; new tasks are forbidden")); } if cx.store().is_read_only() { + // 提交任务会改作用域/调度资格,即使只分配新 Task 也不是合法的纯测量/绘制操作。 return Err(invalid( "asynchronous work is forbidden during read-only rendering", )); } // 统一解码不同 API 参数:spawn 用零延时与回调,sleep 无回调,I/O 第三个位置是路径/URL。 + // spawn 没有延时参数,借一个局部零值复用后面的毫秒校验,不扩展源 API 参数表。 let zero = Value::Number(0_i64.into()); + // 统一得到 owner/key/输入/可选回调四部分;此处只借源实参,不在尚未验证时分配 Task。 let (owner, key, delay, callback) = match args { [owner, key, callback] if self.operation == Operation::Spawn => { + // spawn 的第三实参是零参源函数,不能误作 delay 或路径解析。 (owner, key, &zero, Some(callback)) } + // sleep 无回调,只通过 await/Task 结果状态通知完成。 [owner, key, delay] if self.operation == Operation::Sleep => (owner, key, delay, None), + // 四参形状服务 after 和带回调 I/O;第三位稍后依据 operation 解释成毫秒或路径。 [owner, key, delay, callback] => (owner, key, delay, Some(callback)), [owner, key, path] if matches!(self.operation, Operation::ReadText | Operation::FetchText) => { + // 三参 I/O 返回可 await Task,不要求为了接收文本而人为提供一个空回调。 (owner, key, path, None) } _ => { + // 元数据检查外仍防御直接调用/不匹配形状,不让后面按错误的位置读取输入。 return Err(invalid( "asynchronous request requires owner, key, delay/path and optional I/O callback", )); } }; + // owner 必须是真正托管对象,随后 eligible 再检查其展示生命周期。 let owner = object(owner)?; + // Some(错误值) 传播类型错误,None 仅表示合法无回调 API,二者不可混同。 let callback = callback.map(object).transpose()?; // 所有回调都按零显式参数执行,必须是 Function 且参数元数据确认接受零参,不能等执行时再碰运气。 if let Some(callback) = callback { + // 是否带 receiver 已由 VM 绑定函数体现,数量校验针对调用者仍需传的未绑定实参。 if !cx.store().object(callback)?.payload().is_function() { return Err(invalid("task callback must be a Function")); } @@ -341,6 +399,7 @@ impl NativeCallable for After { return Err(invalid("task callback requires zero unbound parameters")); } } + // key 是相同 owner 内的替换身份,不允许用任意对象 repr 当作不稳定字典键。 let Value::Text(key) = key else { return Err(invalid("task key must be text")); }; @@ -348,19 +407,24 @@ impl NativeCallable for After { if key.is_empty() || key.len() > 128 || key.chars().any(char::is_control) { return Err(invalid("invalid task key")); } + // I/O 得到零计时延迟和后台 Request,计时/协作则得到毫秒数且没有外部请求。 let (milliseconds, request) = if matches!(self.operation, Operation::ReadText | Operation::FetchText) { + // 原局部名 delay 此时借用的是路径/URL 字符串,按 operation 先分流再判断实际类型。 let Value::Text(path) = delay else { return Err(invalid("I/O input must be a String")); }; + // 只创建纯数据请求,provider 提交发生在 Task 已分配并有可取消租约的位置。 let request = match self.operation { Operation::ReadText => { + // 路径允许平台路径语义但拒绝空、过长或 NUL;文件类型/字节预算由 worker 再查。 if path.is_empty() || path.len() > 4096 || path.contains('\0') { return Err(invalid("invalid text path")); } crate::io_tasks::Request::ReadText(path.clone()) } Operation::FetchText => { + // 在创建后台工作前拒绝不支持 URL,worker 仍再次校验以维护独立边界。 if !crate::io_tasks::valid_url(path) { return Err(invalid( "expected HTTP(S) URL without credentials or control characters", @@ -372,6 +436,7 @@ impl NativeCallable for After { }; (0, Some(request)) } else { + // 只有计时/sleep/spawn 进入这里,spawn 由上面借用零 Number,因此也受同一整数约束。 let Value::Number(delay) = delay else { return Err(invalid( "task delay must be an integer number of milliseconds", @@ -387,16 +452,19 @@ impl NativeCallable for After { (milliseconds, None) }; if !eligible(cx.store(), owner, self.desktop)? { + // 不允许关闭/卸载对象创建新工作,即使源代码仍通过变量持有它的 ObjectRef。 return Err(invalid("task owner is closed or detached")); } // 在分配新任务前找同 key 旧身份,额度检查允许替换但不允许借其他 key 继续增加总量。 let existing_scope = scope(cx.store(), owner)?; + // 读取可选旧 Task,不删除旧项;新任务的参数/预算/分配失败时应保留旧身份。 let previous = existing_scope .map(|scope| cx.store().dict_get(scope, &Value::Text(key.clone()))) .transpose()? .flatten() .map(|value| object(&value)) .transpose()?; + // 短锁覆盖调度索引剪枝及最终换表,过程不运行源回调,避免看到半次替换索引。 let mut index = self .index .lock() @@ -410,16 +478,19 @@ impl NativeCallable for After { State::Pending | State::Running ) { + // NativeContext 临时升级的根在本次调用结束释放,live 保存的仍只有弱身份。 live.push(weak); } } if live.len() >= MAX_TASKS && previous.is_none() { + // 同 key 替换不是扩容;新 key 在总量上限时必须失败,不能通过长期保存 Task token 绕过。 return Err(VmError::ExecutionLimit("1024 pending tasks per runtime")); } if let Some(scope) = existing_scope && cx.store().dict_items(scope)?.len() >= MAX_OWNER_TASKS && previous.is_none() { + // owner 局部额度独立于全运行时额度,同 key 仍可原位更新已有槽。 return Err(VmError::ExecutionLimit("64 pending tasks per owner")); } // 先分配并设置截止点/状态,再安装 I/O 租约和 owner 字典边;弱 owner 不承担保活作用。 @@ -448,6 +519,7 @@ impl NativeCallable for After { // 只有旧任务仍待完成且真的持有 I/O 租约才使用 provider 临时替换额度,不给普通任务扩大 I/O 上限。 let replacing = previous .map(|previous| { + // 只有实际未完成 I/O 能归还一个旧租约,终态/纯定时器不能借来放宽 provider 上限。 cx.store().payload::(previous, "Task").map(|task| { matches!(task.state, State::Pending | State::Running) && task.lease.is_some() @@ -455,9 +527,11 @@ impl NativeCallable for After { }) .transpose()? .unwrap_or(false); + // 非阻塞提交拿到交付资格;后台即使很快完成,VM 交付仍等后续 poll 核验 owner/key。 let lease = self .provider .submit(cx.store().downgrade(task)?, request, replacing)?; + // 把租约写进 Task payload 才成为其生命周期资源,后台 Job 自己不持有 Lease。 let mut value = cx.store().payload::(task, "Task")?.clone(); value.lease = Some(lease); cx.replace_payload(task, value)?; @@ -465,14 +539,15 @@ impl NativeCallable for After { // 首次提交才安装字典;以后同 owner 复用 scope,key 替换始终在同一拥有表中发生。 let task_scope = if let Some(scope) = existing_scope { + // 已有作用域保持原字典身份;替换不使其他 key 的 Task 所有权发生变化。 scope } else { + // 第一个任务才分配字典并建立 owner→scope 强边,之后 dict_set 建立 scope→Task 强边。 let scope = cx.new_dict()?; install_scope(cx, owner, scope)?; scope }; - // Publish the new identity before invalidating the old result. Nothing - // external is dispatched until the worker accepts this VM generation. + // 先保存新旧身份所需弱键,下面登记成功才取消旧 Task;此处不是向 GUI 发布画面。 let previous_weak = previous.map(|old| cx.store().downgrade(old)).transpose()?; // 替换字典边前临时升级旧任务,避免 dict_set 删除最后拥有边后,清理旧 payload 时对象已被回收。 if let Some(weak) = previous_weak { @@ -481,9 +556,9 @@ impl NativeCallable for After { cx.dict_set(task_scope, Value::Text(key.clone()), Value::Object(task))?; // 新 key 身份登记成功后再将旧任务取消,切断回调/协程/等待/结果与 I/O 租约,晚到结果不得复活。 if let Some(previous) = previous { - // The replacement above can release the old Task's last owner edge. - // Preserve it through the replacement even without a saved token. + // 前面临时升级旧身份保护它跨过字典替换,即使用户没有保存旧 Task token 也能安全清理。 if cx.store().object(previous).is_ok() { + // 只取消仍活跃项,已完成/失败的旧 token 保留自己的终态结果供已持有者检查。 let mut old = cx.store().payload::(previous, "Task")?.clone(); if matches!(old.state, State::Pending | State::Running) { old.state = State::Cancelled; @@ -497,12 +572,14 @@ impl NativeCallable for After { } } } + // 新 Task 的真正根已经在作用域字典中,调度器只需要弱索引,不附加第二条隐藏拥有边。 let weak = cx.store().downgrade(task)?; // 弱索引也移除被替换身份;spawn 额外设置 runnable 唤醒标志,使外层知道存在可推进协程。 live.retain(|old| Some(*old) != previous_weak); live.push(weak); *index = live; if self.operation == Operation::Spawn { + // cooperative 工作首次即可执行,唤醒标志让 worker 不误以为只有未来计时器而进入长等待。 self.runnable.store(true, Ordering::Release); } Ok(NativeResult::Return(Value::Object(task))) @@ -519,10 +596,13 @@ enum Query { Error, } #[derive(Debug)] -/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。 +/// install 安装到 Task 类型的原生方法包装;VM 绑定 receiver 后调用,不为每个 Task 单独创建闭包。 struct Method { + // 包含隐式 Task receiver 的签名,查询只读,cancel 修改任务与 owner/key 字典。 signature: endor_core::CallSignature, + // 五个 API 共用 receiver/资格检查后,各按不同状态可读性规则返回结果。 query: Query, + // eligible 的跨窗口注册表检查使用所属 VM 主资源域,不能以 current 窗口替代。 desktop: ObjectRef, } impl NativeCallable for Method { @@ -535,26 +615,32 @@ impl NativeCallable for Method { Some(endor_core::CallArity::exact(1)) } - /// 验证 Task 接收者并按当前状态处理 status/active/result/error/cancel;仍活跃的任务需重新确认 owner/key 身份。 + /// VM 执行 task.status/active/result/error/cancel 时调用,重新核验活任务资格,查询不因失效偷偷改 payload。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // 方法无显式参数,实际 args 唯一一项是隐式 Task 接收者。 let [value] = args else { return Err(invalid("Task methods accept no arguments")); }; + // 解码 receiver 后验证真实 Task payload,普通对象无法靠同名方法字段伪造任务。 let reference = object(value)?; + // 状态副本用于 cancel 的写屏障提交;只读查询保留原 Task 不变。 let mut task = cx.store().payload::(reference, "Task")?.clone(); + // pending 这里同时涵盖 Running 协程,不等同于严格的 State::Pending 枚举值。 let pending = matches!(task.state, State::Pending | State::Running); + // 只有活任务才需验证 owner;终态结果不因 owner 后续销毁而突然不可读。 let owner = if pending { cx.upgrade(task.owner)? } else { None }; + // active 同时要求展示资格和当前 key 精确身份,单看 Task.state 会漏掉尚未轮询清理的旧项。 let active = if let Some(owner) = owner { eligible(cx.store(), owner, self.desktop)? && current(cx.store(), owner, reference, &task)? } else { false }; + // 各方法返回类型由 install 签名对应;result 的可用性不同于 active,不能共享一个布尔快路。 let value = match self.query { // pending/running 但已失去 owner/key 资格时,对源报告 cancelled;只读查询不因此改写 payload。 Query::Status => Value::Text( @@ -572,6 +658,7 @@ impl NativeCallable for Method { || task.error.is_some() || (task.cooperative && task.state != State::Completed) { + // 协程未完成不能返回半成品;I/O 错误也不能把 text/Nil 当成功返回。 return Err(invalid( "task result is not available; inspect status and error", )); @@ -593,6 +680,7 @@ impl NativeCallable for Method { ), Query::Cancel => { if cx.store().is_read_only() { + // 即使任务已失效也不允许在只读渲染里执行具有取消语义的方法。 return Err(invalid( "task cancellation is forbidden during read-only rendering", )); @@ -600,7 +688,7 @@ impl NativeCallable for Method { if pending { task.state = State::Cancelled; task.callback = None; - // 回调开始前释放 I/O 名额,已收到的 text 仍留在任务内,可由回调查询。 + // 取消立即放弃 I/O 租约与执行边,下面也清掉 text/result;不保留可交付的成功结果。 task.lease = None; task.coroutine = None; task.waiting = None; @@ -611,6 +699,7 @@ impl NativeCallable for Method { && current(cx.store(), owner, reference, &task)? && let Some(scope) = scope(cx.store(), owner)? { + // 必须仍是当前 key 的同一 Task 才删 owner 边,旧 token.cancel 不能删掉替换后的新任务。 cx.dict_delete(scope, &Value::Text(task.key))?; } } @@ -622,41 +711,49 @@ impl NativeCallable for Method { } } -/// 弱任务索引、公平调度位置和 provider,索引不会单独把任务保活。 +/// bootstrap 返回给 UiBridge 的每 VM 调度能力,endor_host 轮询/退出时使用,原生字段不持有 Task 强根。 pub struct Scheduler { + // 弱项顺序也是轮转顺序,实际推进过的任务在 poll 尾部移到末尾。 index: Index, + // worker 等待策略读取此标志;只表示可能存在可运行协程,不保证下次一定完成一个任务。 runnable: Arc, + // 退出后不可重新打开,新任务 API 与本调度器共享门闩。 quiescing: Arc, + // 跨窗口 owner 生命周期核验的主注册域,不持 GUI 窗口指针。 desktop: ObjectRef, + // 后台只提交 Completion 到邮箱,本调度器才有权在 VM 中写入 Task 与执行回调。 provider: crate::io_tasks::Provider, } impl Scheduler { - /// 停止接收该域的新任务工作,为关闭/替换运行时做准备。 + /// UiBridge::shutdown 调用,先关新任务入口再取消所有活任务,允许退出钩子执行但不能重建工作。 pub fn quiesce(&self, vm: &mut Vm) -> Result { // 先设置不可逆的退出门闩再取消全部任务,防止清理回调间隙继续接收新工作。 self.quiescing.store(true, Ordering::Release); self.cancel_all(vm) } - /// 检查是否有可推进的协程,而不是仅仅存在正在等待外部结果的任务。 + /// UiBridge/endor_host 的等待策略查询,不扫描 VM 堆,仅读取上次创建/prune 得到的可运行标志。 pub fn has_cooperative_work(&self) -> bool { self.runnable.load(Ordering::Acquire) } - /// Mutation boundaries collect unreachable owner/callback cycles before - /// this weak lookup. Idle timer polling does not rescan the entire heap. - /// 在当前 VM 中有界交付就绪任务、执行回调或推进协程;开始布局后由外层刷新流程继续。 + /// UiBridge::poll_tasks 由 endor_host 调用,在 owning VM 内交付邮箱、推进至多 limit 项工作并更新公平索引。 + /// 不在空闲轮询中扫描整堆;不可达 owner/回调环由外层变更边界的 collect_cycles 清理。 pub fn poll(&self, vm: &mut Vm, limit: usize) -> Result { + // limit 是本轮工作预算而非毫秒超时,零或超过 MAX_READY 都不能进入调度。 anyhow::ensure!((1..=MAX_READY).contains(&limit), "invalid task poll limit"); - // Provider threads only fill the mailbox. Only this VM worker updates - // managed payloads, and delivery below rechecks owner/key generation. // provider 只产纯数据,VM worker 此处重新核对任务 pending、租约仍在、owner 有效且 key 仍当前。 for (weak, completion) in self.provider.take_ready()? { if let Some(reference) = vm.upgrade(weak)? { + // 保存邮箱交付 Result,外层先释放 upgrade 得到的 Task 根,再传播任意中途错误。 let result = (|| -> Result<()> { + // 任务可能在后台运行时已被源代码取消/替换,不能直接把 Completion 写进旧副本。 let mut task = vm.payload::(reference, "Task")?.clone(); if task.state != State::Pending || task.lease.is_none() { + // 已终态或放弃租约的晚到结果直接忽略,不设置 ready 把旧任务重新变为可执行。 return Ok(()); } + // 临时拥有 owner 跨过资格检查,不能在完成邮箱里保存一个长期 owner 根。 let owner = vm.upgrade(task.owner)?; + // 即使 eligible/current 报错,也先走下面 owner 根释放路径。 let active = (|| -> Result { Ok(if let Some(owner) = owner { eligible(vm.object_store(), owner, self.desktop)? @@ -669,11 +766,13 @@ impl Scheduler { vm.release(owner)?; } if !active? { + // 身份/生命周期已失效,留给后续有界扫描清理,不交付回调或保存可观察成功文本。 return Ok(()); } // 收到后台结果只设置 ready/text/error,实际终态与回调交付由下面的有界调度扫描决定。 task.ready = true; match completion { + // provider 的纯文本结果在 VM 线程转换为 Task 字段,后台不直接改对象版本。 Ok(text) => task.text = Some(text.into()), Err(error) => task.error = Some(error.into()), } @@ -684,6 +783,7 @@ impl Scheduler { result?; } } + // 复制本轮弱索引后释放锁,用户回调可在 poll 期间提交任务,不应因重入同一索引锁死锁。 let snapshot = self .index .lock() @@ -691,22 +791,32 @@ impl Scheduler { .clone(); // 本轮使用同一 now 判断到期,避免长回调执行后在同轮无限吸收后来才到期的定时器。 let now = Instant::now(); + // 取消无效任务也消耗工作预算,避免大量死亡 owner 使一次轮询无限延长。 let mut work = 0; + // 记录本轮实际执行的弱身份,扫描结束后移到队尾形成 round-robin,不边迭代边改原表。 let mut advanced = Vec::new(); for weak in snapshot { if work >= limit { + // 未扫描的任务保留原索引,下一次 poll 继续有机会运行,而非丢弃余下项。 break; } + // Task 已不可达时不复活它,失效弱项由 prune 最后清理。 let Some(reference) = vm.upgrade(weak)? else { continue; }; + // 一项扫描的任何 VM 错误都先回到 Task 根释放,再让外层按运行时故障规则处理。 let result = (|| -> Result<()> { + // 每轮重读当前 payload,不能沿用 mailbox 阶段或上轮协程恢复前的状态副本。 let mut task = vm.payload::(reference, "Task")?.clone(); if !matches!(task.state, State::Pending | State::Running) { + // 已终态仍可被用户变量持有,但不再消耗执行量子,也不能重复执行 callback。 return Ok(()); } + // owner 弱边可能已死亡,按取消处理而不是把当前运行时当作替代 owner。 let owner = vm.upgrade(task.owner)?; + // 内层结果暂存,使任何回调/完成错误都不会跳过 owner 根释放。 let result = (|| -> Result<()> { + // owner 展示资格和同 key 当前身份都必须满足,保留旧 Task token 不能继续取得执行权。 let active = if let Some(owner) = owner { eligible(vm.object_store(), owner, self.desktop)? && current(vm.object_store(), owner, reference, &task)? @@ -720,9 +830,11 @@ impl Scheduler { } // 被等待对象未就绪则不消费量子;被取消会取消等待者,失败等其他结果交回协程 await 语义处理。 if let Some(waiting) = task.waiting { + // 仅查询 AwaitPoll,不递归执行被等待对象,公平调度仍由外层轮询负责。 match vm.object(waiting)?.payload().poll_await() { Ok(endor_core::AwaitPoll::Pending) => return Ok(()), Err(VmError::AwaitCancelled) => { + // 被等待任务取消会取消等待者;失败则留给恢复后的 await 按语言语义抛出。 work += 1; return finish(vm, reference, State::Cancelled); } @@ -730,6 +842,7 @@ impl Scheduler { } } if task.due > now || !task.ready { + // 时间到了不代表 I/O 已就绪,I/O 就绪也不能越过计时截止;两条件均需满足。 return Ok(()); } work += 1; @@ -737,22 +850,26 @@ impl Scheduler { advanced.push(weak); // spawn 使用可恢复协程而非普通 call;其余 I/O/定时器回调作为一次完整有界调用执行。 if task.cooperative { + // 只推进一次软量子,完成/await/仍可运行分别由 advance_coroutine 处理。 return advance_coroutine(vm, reference); } if task.error.is_some() { + // provider 失败不调用成功回调,用 Failed 终态让 await/error 观察原始文本诊断。 return finish(vm, reference, State::Failed); } + // sleep 或无回调 I/O 直接完成,结果已保存在 Task,不需为了 await 虚构空函数。 let Some(callback) = task.callback else { return finish(vm, reference, State::Completed); }; - // The provider is finished. Keep the result in the Task, - // but release its I/O slot before running source callbacks. + // provider 已完成,回调开始前归还 I/O 名额;text 暂留给回调中的 Task.result 查询。 task.lease = None; task.coroutine = None; task.waiting = None; + // 先发布 Running 状态再进入用户代码,回调可查询任务或取消/替换自身。 let mut running = task.clone(); running.state = State::Running; vm.replace_payload(reference, running)?; + // 普通回调必须一次有界完成,不是 spawn 的可恢复协程;预算不因别的任务回调重入而累加到无限。 let result = vm .call_with_limits( callback, @@ -769,6 +886,7 @@ impl Scheduler { let unit = matches!(value, Value::Nil); vm.release_value(value)?; if !unit { + // 已释放非 Unit 返回根后再标记失败并报告,不能因契约错误泄漏任意对象返回值。 fail_callback( vm, reference, @@ -779,6 +897,7 @@ impl Scheduler { finish(vm, reference, State::Completed) } Err(error) => { + // 存有界诊断并清执行资源,同时把实际错误交给 endor_host 的故障处理。 fail_callback(vm, reference, &format!("{error:#}"))?; Err(error) } @@ -792,9 +911,9 @@ impl Scheduler { vm.release(reference)?; result?; } - // Round-robin resumable work so a long call cannot monopolize the - // one-callback idle budget and starve other runnable Tasks. + // 实际推进项移到末尾,空闲 limit=1 时一个长协程不能每轮都抢在其他可执行任务前。 if !advanced.is_empty() { + // 回调可能新建任务,所以重新取得当前索引再移动已推进项,不能拿旧 snapshot 覆盖新登记。 let mut index = self .index .lock() @@ -806,8 +925,9 @@ impl Scheduler { self.prune(vm)?; Ok(work) } - /// 取消当前域全部任务,切断回调、协程和等待交付资格。 + /// UiBridge 取消和 Scheduler::quiesce 调用,逐项 finish 并清空弱索引,不执行用户取消回调。 pub fn cancel_all(&self, vm: &mut Vm) -> Result { + // 不在锁内调用 finish;它会变更 owner 作用域,但索引不需在整个 VM 清理过程中借用。 let snapshot = self .index .lock() @@ -815,11 +935,13 @@ impl Scheduler { .clone(); for weak in snapshot { if let Some(task) = vm.upgrade(weak)? { + // 升级是拥有根,先保存 finish 错误再 release,避免失败时遗留额外 Task 引用。 let result = finish(vm, task, State::Cancelled); vm.release(task)?; result?; } } + // VM 清理完成后再锁索引,取原项数供诊断并一次性清表。 let mut index = self .index .lock() @@ -830,27 +952,34 @@ impl Scheduler { self.runnable.store(false, Ordering::Release); Ok(count) } - /// 移除已释放的弱任务索引,索引本身不让任务或其 owner 存活。 + /// poll 的轮末收尾调用,移除死亡/终态弱项并计算“是否有可运行协程”,防止等待 I/O 时忙循环。 fn prune(&self, vm: &mut Vm) -> Result<()> { + // 本函数不执行用户函数,短暂持锁扫描弱项及查询 AwaitPoll,不需把整个 VM 堆遍历一遍。 let mut index = self .index .lock() .map_err(|_| anyhow::anyhow!("task index poisoned"))?; + // live 保留待处理状态任务,终态由源变量自己持有,不因调度索引延长寿命。 let mut live = Vec::new(); + // 累加是否有任意协程不用等待外部事件即可推进,不把普通未来计时器视为 runnable。 let mut runnable = false; for weak in index.iter().copied() { if let Some(task) = vm.upgrade(weak)? { + // 先保存 payload 查询 Result,让升级临时根在返回错误之前也能被 release。 let pending = vm.payload::(task, "Task").map(|task| { + // 此局部 pending 是状态布尔,与外层承载查询结果的 Result 区分。 let pending = matches!(task.state, State::Pending | State::Running); // 可运行标记仅针对协作任务:无等待对象可立即推进,有等待对象则看其 poll_await 是否不再 Pending。 runnable |= pending && task.cooperative && task.waiting.is_none(); pending }); if pending.as_ref().is_ok_and(|pending| *pending) { + // 只对成功读出的活任务继续看 waiting,失败则留给释放后的 pending? 传播。 let value = vm.payload::(task, "Task")?; if value.cooperative && let Some(waiting) = value.waiting { + // 等待值已完成/失败/取消都可以恢复处理;只有明确 Pending 才继续休眠。 runnable |= !matches!( vm.object(waiting)?.payload().poll_await(), Ok(endor_core::AwaitPoll::Pending) @@ -868,17 +997,20 @@ impl Scheduler { Ok(()) } } -/// 沿等待链检查任务依赖环,防止 await 永远无法被满足。 +/// advance_coroutine 准备保存 await 依赖前调用,沿 Task.waiting 判断自等/互等,非 Task 值由其自身 await 协议处理。 fn cyclic_wait(vm: &Vm, source: ObjectRef, mut target: ObjectRef) -> Result { + // seen 记录目标身份,既识别回到 source,也识别原本已有的封闭依赖环。 let mut seen = std::collections::HashSet::new(); loop { // 等待链到达 source 或重复已有目标都表示环;不是 Task 或没有 waiting 边时链终止,不判环。 if target == source || !seen.insert(target) { return Ok(true); } + // 非本模块 Task 的 awaitable 没有可遍历 waiting 边,不能臆测其内部依赖结构。 let Some(task) = vm.object(target)?.payload().as_any().downcast_ref::() else { return Ok(false); }; + // 一个尚未等待其他对象的任务是链尾,当前这次新增等待不会自行形成环。 let Some(waiting) = task.waiting else { return Ok(false); }; @@ -886,16 +1018,20 @@ fn cyclic_wait(vm: &Vm, source: ObjectRef, mut target: ObjectRef) -> Result Result<()> { + // 保存初始执行边用于第一次准备,运行源代码后必须重读状态,不能让旧副本覆盖自取消结果。 let mut task = vm.payload::(reference, "Task")?.clone(); // 首次推进把 callback 转成托管 Coroutine,后续复用同一帧与预算,不重放已发生的源副作用。 let coroutine = if let Some(coroutine) = task.coroutine { + // 已有协程直接继续原帧,不从 callback 起点回放写属性/I/O 等副作用。 coroutine } else { + // 只有第一次推进才取 callback,成功保存协程后会清 callback 强边避免重复准备。 let callback = task .callback .ok_or_else(|| anyhow::anyhow!("spawn callback missing"))?; + // 协程自身保存硬预算,软量子每轮刷新也不会让总指令限制重新从零计算。 let prepared = vm .new_coroutine( callback, @@ -906,6 +1042,7 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error)); + // 准备可能因回调/类型/预算错误失败,统一标记任务失败并保留源诊断。 let coroutine = match prepared { Ok(coroutine) => coroutine, Err(error) => { @@ -913,10 +1050,11 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { return Err(error); } }; - // Native preparation can itself cancel or replace this Task. // 准备 coroutine 期间原生代码也可能取消/替换任务,必须重读当前状态,不能把旧 task 副本覆盖回去。 + // 看真实当前 payload 的状态,而不是此函数入口保存的 task.state。 let current = vm.payload::(reference, "Task")?.state; if !matches!(current, State::Pending | State::Running) { + // 已失去执行资格,刚准备出的协程返回根仍需释放,不能写回 Task 复活它。 vm.release(coroutine)?; return Ok(()); } @@ -924,17 +1062,17 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { task.callback = None; task.coroutine = Some(coroutine); // 先由 payload 建立协程拥有边,再释放创建根;安装失败也尝试释放局部根。 + // 先暂存安装结果,让创建根不管写屏障成功/失败都经过下面 release。 let installed = vm.replace_payload(reference, task); vm.release(coroutine)?; installed?; coroutine }; + // 续体结果先保存,运行函数可能主动取消/替换 Task,交付前还要核验真实状态。 let result = vm // 每次最多执行 256 指令软量子,达到量子时仅挂起;总 100000 指令上限仍由协程本身累计。 .resume_coroutine(coroutine, 256) .map_err(|error| crate::endor_host::runtime_error(vm, error)); - // Cancellation/replacement from the running function must win. A completed - // value still owns a root and must be released even when delivery is stale. // 源代码运行期间可取消自身,取消/替换优先于本轮完成;完成值即使不交付也必须释放其拥有根。 let active = matches!( vm.payload::(reference, "Task")?.state, @@ -943,6 +1081,7 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { match result { Ok(endor_core::CoroutineProgress::Waiting { awaitable, .. }) => { if !active { + // 等待来自已取消旧执行时不再登记 Task.waiting,让取消/替换优先。 return Ok(()); } // 登记等待强边前检查依赖环,拒绝永不满足的互相 await,并把当前任务转失败。 @@ -950,6 +1089,7 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { fail_callback(vm, reference, "cyclic task await")?; bail!("cyclic task await"); } + // 保存强等待边,让被等待值在协程挂起期间真实可达,而不是靠弱调度表保活。 let mut task = vm.payload::(reference, "Task")?.clone(); task.waiting = Some(awaitable); vm.replace_payload(reference, task)?; @@ -958,6 +1098,7 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { // 软量子挂起不代表等待外部值,清 waiting 后下一轮仍视作可运行。 Ok(endor_core::CoroutineProgress::Pending { .. }) => { if active { + // 本片量子用尽但并未 await,清旧 waiting 让 prune 把它判为下一轮可运行。 let mut task = vm.payload::(reference, "Task")?.clone(); task.waiting = None; vm.replace_payload(reference, task)?; @@ -966,13 +1107,16 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { } Ok(endor_core::CoroutineProgress::Complete { value, .. }) => { if !active { + // 完成值来自 VM 返回拥有根;即使不再交付也不能直接 drop Rust Value 而不 release。 vm.release_value(value)?; return Ok(()); } + // 重新读可能被源函数更新过的字段,仅附加本次成功结果,避免入口旧副本覆盖新状态。 let mut task = vm.payload::(reference, "Task")?.clone(); // 结果先写入受 trace 管理的 payload,再释放 VM 返回值根,最后 finish 清执行边但保留成功结果。 task.result = Some(value.clone()); task.waiting = None; + // 结果对象先由 Task.trace 保活;局部返回根随后释放,即使存储失败也不跳过释放。 let stored = vm.replace_payload(reference, task); vm.release_value(value)?; stored?; @@ -980,6 +1124,7 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { } Err(error) => { if active { + // 已终态任务不再改成 Failed,保持自取消/替换结果;实际执行错误仍返回上层。 fail_callback(vm, reference, &format!("{error:#}"))?; } Err(error) @@ -987,8 +1132,9 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> { } } -/// 记录回调执行失败并沿运行时故障规则停止后续不安全的交付。 +/// Scheduler::poll/advance_coroutine 的回调错误分支调用,保存有界诊断,再经 finish 统一清理执行资源。 fn fail_callback(vm: &mut Vm, reference: ObjectRef, message: &str) -> Result<()> { + // 只更新 error 字段,不自行复制 Task 结果或重新分配一份故障任务对象。 let mut task = vm.payload::(reference, "Task")?.clone(); // 错误预览按字符限长,保证 UTF-8 完整;记录后统一走终态清理而非保留失败协程。 task.error = Some(message.chars().take(1024).collect::().into()); @@ -996,8 +1142,9 @@ fn fail_callback(vm: &mut Vm, reference: ObjectRef, message: &str) -> Result<()> finish(vm, reference, State::Failed) } -/// 将任务转入终态并释放回调、provider 租约、协程和等待引用;取消还会清除保存的结果。 +/// poll、cancel_all、协程收尾和 fail_callback 共用的终态转换,清执行边并只撤销仍匹配的 owner/key 拥有关系。 fn finish(vm: &mut Vm, reference: ObjectRef, state: State) -> Result<()> { + // 先核对当前真实状态,已完成/失败/取消不再被另一个迟到的收尾覆盖。 let mut task = vm.payload::(reference, "Task")?.clone(); if !matches!(task.state, State::Pending | State::Running) { return Ok(()); @@ -1009,11 +1156,13 @@ fn finish(vm: &mut Vm, reference: ObjectRef, state: State) -> Result<()> { task.coroutine = None; task.waiting = None; if state == State::Cancelled { + // 取消不保存成功结果;Completed/Failed 的诊断/结果由保留 Task token 的源代码继续观察。 task.text = None; task.result = None; } vm.replace_payload(reference, task.clone())?; if let Some(owner) = vm.upgrade(task.owner)? { + // owner 可能已经回收,无 owner 时无需删表;升级成功则先保留清理结果再 release。 let result = (|| -> Result<()> { // 只有 key 仍指向本任务才删 owner 边,避免旧任务结束时误删同 key 新任务。 if current(vm.object_store(), owner, reference, &task)? @@ -1029,7 +1178,7 @@ fn finish(vm: &mut Vm, reference: ObjectRef, state: State) -> Result<()> { Ok(()) } -/// 把本模块的类型、方法或能力注册到所属运行时上下文。 +/// ui_bridge::bootstrap 在 Task 类型建立后调用,注册五种创建 API 和 Task 实例方法,返回同域 Scheduler。 pub fn install( vm: &mut Vm, roots: &mut Vec, @@ -1038,9 +1187,13 @@ pub fn install( ) -> Result { // 每个运行时一份弱索引、可运行/退出标记和 provider mailbox,UI API 与 Scheduler 共享这些原生状态。 let index = Arc::new(Mutex::new(Vec::new())); + // 初始化没有可推进协程,spawn 及轮末 prune 才会更新这个唤醒提示。 let runnable = Arc::new(AtomicBool::new(false)); + // 运行时退出单向门闩共享给所有 After callable,不能只在 Scheduler 自己保留一个无效本地副本。 let quiescing = Arc::new(AtomicBool::new(false)); + // 所有 API 返回同一 Task 名义类型,不能为 after/sleep 各建一个不兼容 Task 类。 let definition = desktop_type(vm.object_store(), "Task", desktop)?; + // 每运行时独立完成邮箱/租约额度,工作线程池由 Provider 内部全进程共用。 let provider = crate::io_tasks::Provider::default(); for (name, operation) in [ ("after", Operation::Timer), @@ -1049,20 +1202,25 @@ pub fn install( ("fetchText", Operation::FetchText), ("spawn", Operation::Spawn), ] { + // I/O 第三参数为字符串并声明 External;其余初始模板用毫秒 Number/Mutating。 let external = matches!(operation, Operation::ReadText | Operation::FetchText); + // After 共享索引/门闩/Provider,只有 operation 与签名不同,仍保留各源 API 独立名字。 let function = vm.allocate(ContextNativeFunction::new( name, After { operation, provider: provider.clone(), signature: { + // 基础四参模板为 owner、key、delay/path、callback,随后针对无回调/零延时 API 收窄。 let mut signature = endor_core::CallSignature::exact( vec![ endor_core::AnnotationType::Any, endor_core::AnnotationType::String, if external { + // readText/fetchText 使用路径/URL,不能让 API 签名把它误认作计时延迟。 endor_core::AnnotationType::String } else { + // Timer/Sleep 使用整数毫秒,整数性与范围由 After::invoke 再查。 endor_core::AnnotationType::Number }, endor_core::AnnotationType::Function, @@ -1076,13 +1234,16 @@ pub fn install( ); // I/O 可带可选回调;spawn 去掉延时参数,sleep 去掉回调参数,保留各自正确 arity。 if external { + // I/O 允许不传回调并直接 await 返回 Task,但 owner/key/path 三参始终必填。 signature.arity = endor_core::CallArity::range(3, 4); } if operation == Operation::Spawn { + // 第三位改为源函数,去掉延时注解而非只把 required 数量改成 3。 signature.parameters.remove(2); signature.arity = endor_core::CallArity::exact(3); } if operation == Operation::Sleep { + // sleep 无回调,不能把三参 API 的注解末尾遗留成任意函数尾参。 signature.parameters.pop(); signature.arity = endor_core::CallArity::exact(3); } @@ -1106,6 +1267,7 @@ pub fn install( ("result", Query::Result), ("error", Query::Error), ] { + // 方法存在 Task 类型表上,receiver 由 VM 绑定;返回值契约按 query 区分。 let method = vm.allocate(ContextNativeFunction::new( name, Method { @@ -1114,11 +1276,13 @@ pub fn install( signature: endor_core::CallSignature::exact( vec![endor_core::AnnotationType::Nominal(definition)], match query { + // status/error 返回文本,result 保留任意源值,cancel/active 则返回布尔。 Query::Status | Query::Error => endor_core::AnnotationType::String, Query::Result => endor_core::AnnotationType::Any, _ => endor_core::AnnotationType::Bool, }, if matches!(query, Query::Cancel) { + // 查询和取消的效果不同,不能让 cancel 在只读布局里改变任务资格。 endor_core::CallEffect::Mutating } else { endor_core::CallEffect::ReadOnly @@ -1130,6 +1294,7 @@ pub fn install( roots.push(method); vm.define_method(definition, name, method)?; } + // 返回与全部 After callable 相同的共享状态,UiBridge 后续通过它轮询/退出本 VM 的任务。 Ok(Scheduler { index, runnable,