diff --git a/README.md b/README.md index e510423..6d12753 100644 --- a/README.md +++ b/README.md @@ -499,7 +499,7 @@ Flex main-axis policy: flexBasis is "auto" by default (zero basis when flex > 0, When an occurrence's cross-axis constraint changes during settling, its preferred main-axis size is invalidated and remeasured. Any obsolete native flex allocation on that axis is removed for the replacement measurement, so narrowing a nested Column can increase wrapped text height. Source arrange allocations remain explicit. Preferred-size presence and values participate in convergence/cycle detection. -The source layout driver now uses Aster's resumable read-only call API with a soft quantum of 256 instructions. Pauses retain the same VM frames, operand/local roots, return contracts and read tracking; total instruction/frame limits still apply across slices. ui.stats().layoutSlices counts slices in the most recent solve (native fast path: 1). Viewport and idle-task refresh park Pending across worker turns; newer viewport targets can cancel and replace unpublished work. Explicit source/event inputs still drain synchronously. Native callbacks are not preempted. Only the completed settled frame is published. +The source layout driver now uses Aster's resumable read-only call API with a soft quantum of 256 instructions. Pauses retain the same VM frames, operand/local roots, return contracts and read tracking; total instruction/frame limits still apply across slices. ui.stats().layoutSlices counts slices in the most recent solve (native fast path: 1). Viewport and idle-task refresh park Pending across worker turns; newer viewport targets can cancel and replace unpublished work. Source/event callbacks execute atomically; their refresh can park across worker turns. Native callbacks are not preempted. Only the completed settled frame is published. source_layout::start now returns either Complete(Draft) or Pending(Work). The host-owned Work retains the unpublished frame, derived runtime/cache snapshots, convergence plan and prepare root without borrowing caller frame/cache variables. Work::resume consumes the saved job and advances the same VM continuation; Work::cancel closes that continuation and releases the prepare root without merging a draft. A second start is rejected while any read-only execution is active. Every pending Work must be resumed or explicitly cancelled with its owning VM; dropping it is not cancellation. Synchronous ReplSession operations drain the same saved refresh state. The worker can park viewport and idle-task refresh jobs; cancellation restores still-pending Mount preparation. @@ -511,7 +511,7 @@ UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around ReplSession now owns a resumable RefreshWork containing window traversal, pending bridge layout, unpublished per-window documents and the cumulative refresh work budget. poll_refresh advances one refresh stage or VM quantum; only a complete stable pass replaces the session's full document map. Synchronous callers drain the same state machine. Failure stops the domain, releases any pending bridge work and preserves the externally published picture. refreshYields counts cooperative refresh returns, not wall-clock time. Only still-pending Mount preparation is rolled back; completed callbacks remain committed. -Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. Mutating requests are serialized; newer viewport targets coalesce by cancelling unpublished layout. Initial loads and explicit REPL/event refresh still drain synchronously. Idle task delivery starts at most one ready callback per turn and parks its subsequent layout; callbacks themselves and individual Rust measurement/paint calls remain atomic. +Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. Mutating requests are serialized; newer viewport targets coalesce by cancelling unpublished layout. Initial loads still drain synchronously. Explicit REPL/event refresh can park across worker turns. Idle task delivery starts at most one ready callback per turn and parks its subsequent layout; callbacks themselves and individual Rust measurement/paint calls remain atomic. Runtime mutation entry points reject direct calls while refresh work is pending, before executing source or changing metadata; task polling defers. The API worker drains the saved refresh before accepting those mutations, keeping the runtime usable. Incremental refresh keeps the prior complete document map until a stable pass, including across window traversal boundaries where no VM read-only frame is currently active. @@ -530,3 +530,6 @@ Large intrinsic layout trees (more than 64 occurrences) now use the resumable pe Idle timer delivery now processes at most one ready callback (or one ineligible task cancellation) before starting an incremental refresh. No further task is delivered while that refresh is pending. Viewport changes can replace its unfinished layout without replaying the completed task callback. Task effects and native window commands publish only after settling; a previously completed resize may publish separately. Explicit input still drains pending work first. Synchronous host callers retain a maximum of 64 task deliveries per poll. + + +REPL and event mutations now have incremental refresh entry points. They execute source/callbacks once, retain scalar response metadata, then park layout with the current viewport target. While pending, mutation, command extraction and garbage collection wait; layout can be retargeted by a newer native size. Synchronous helpers remain for host regression coverage. Discarding a ReplSession explicitly cancels any still-pending bridge layout before the Candidate releases service/global roots. This does not preempt source/callback execution, structural preparation or paint. diff --git a/src/README.md b/src/README.md index deb9d03..2794ee2 100644 --- a/src/README.md +++ b/src/README.md @@ -163,7 +163,7 @@ UiBridge::begin_layout_frame/resume_layout_frame now expose PendingLayout around ReplSession now owns a resumable RefreshWork containing window traversal, pending bridge layout, unpublished per-window documents and the cumulative refresh work budget. poll_refresh advances one refresh stage or VM quantum; only a complete stable pass replaces the session's full document map. Synchronous callers drain the same state machine. Failure stops the domain, releases any pending bridge work and preserves the externally published picture. refreshYields counts cooperative refresh returns, not wall-clock time. Only still-pending Mount preparation is rolled back; completed callbacks remain committed. -Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. Mutating requests are serialized; newer viewport targets coalesce by cancelling unpublished layout. Initial loads and explicit REPL/event refresh still drain synchronously. Idle tasks deliver at most one callback per turn and park subsequent layout; callbacks and individual native nodes remain atomic. Large intrinsic trees use the sliced driver; small intrinsic trees keep the fast path. +Viewport refresh now advances through ReplSession's saved refresh state on worker idle turns. A turn advances one traversal stage or a 256-instruction layout quantum; a pending job shortens the next worker wait to 1 ms. Tasks/window commands and document publication wait for complete refresh. ui.stats().refreshYields reports cooperative idle returns. Mutating requests first drain the existing job, so they cannot run inside its read-only scope. Mutating requests are serialized; newer viewport targets coalesce by cancelling unpublished layout. Initial loads still drain synchronously. Explicit REPL/event refresh can park across worker turns. Idle tasks deliver at most one callback per turn and park subsequent layout; callbacks and individual native nodes remain atomic. Large intrinsic trees use the sliced driver; small intrinsic trees keep the fast path. Runtime mutation entry points reject direct calls while refresh work is pending, before executing source or changing metadata; task polling defers. The API worker drains the saved refresh before accepting those mutations, keeping the runtime usable. Incremental refresh keeps the prior complete document map until a stable pass, including across window traversal boundaries where no VM read-only frame is currently active. @@ -185,3 +185,6 @@ source_layout uses the same resumable node driver for trees over 64 occurrences Worker idle polls use ReplSession::poll_tasks_incremental with the current viewport map. Scheduler::poll accepts a checked 1..=64 work limit, counting both delivery and ineligible-task cancellation; idle uses one, synchronous compatibility uses 64. Once delivery begins layout, further tasks and window-command projection wait until the saved refresh completes. The viewport stamp allows cancellation of task-induced pending layout without repeating callback effects. + + +REPL and event mutations now have incremental refresh entry points. They execute source/callbacks once, retain scalar response metadata, then park layout with the current viewport target. While pending, mutation, command extraction and garbage collection wait; layout can be retargeted by a newer native size. Synchronous helpers remain for host regression coverage. Discarding a ReplSession explicitly cancels any still-pending bridge layout before the Candidate releases service/global roots. This does not preempt source/callback execution, structural preparation or paint. diff --git a/src/endor_host.rs b/src/endor_host.rs index dcb14f9..3cd7a15 100644 --- a/src/endor_host.rs +++ b/src/endor_host.rs @@ -355,7 +355,26 @@ impl ReplSession { refresh: None, }) } + #[cfg(test)] pub fn execute(&mut self, source: &str, namespace: &str) -> Result { + self.execute_mode(source, namespace, None) + } + + pub fn execute_incremental( + &mut self, + source: &str, + namespace: &str, + viewports: &std::collections::BTreeMap, + ) -> Result { + self.execute_mode(source, namespace, Some(viewports)) + } + + fn execute_mode( + &mut self, + source: &str, + namespace: &str, + viewports: Option<&std::collections::BTreeMap>, + ) -> Result { self.require_idle_refresh()?; if self.faulted { bail!( @@ -466,7 +485,7 @@ impl ReplSession { retained?; rendered_value = repr?; } - let document = self.refresh_document()?; + let document = self.refresh_after_mutation(viewports)?; Ok(ReplResult { value: rendered_value, document, @@ -487,8 +506,10 @@ impl ReplSession { .push_str(" (error; earlier statements may remain live)"); } } - self.bridge - .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; + if !self.has_pending_refresh() { + self.bridge + .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; + } result } @@ -504,6 +525,23 @@ impl ReplSession { Ok(()) } + fn refresh_after_mutation( + &mut self, + viewports: Option<&std::collections::BTreeMap>, + ) -> Result> { + let Some(viewports) = viewports else { + return self.refresh_document(); + }; + let progress = self.poll_refresh(true)?; + if let Some(work) = &mut self.refresh { + work.viewports = Some(viewports.clone()); + } + Ok(match progress { + RefreshProgress::Pending => None, + RefreshProgress::Complete(document) => document.map(|document| *document), + }) + } + fn refresh_document(&mut self) -> Result> { loop { match self.poll_refresh(false)? { @@ -824,18 +862,7 @@ impl ReplSession { return Ok(None); } completed = true; - if let Some(viewports) = viewports { - let progress = self.poll_refresh(true)?; - if let Some(work) = &mut self.refresh { - work.viewports = Some(viewports.clone()); - } - Ok(match progress { - RefreshProgress::Pending => None, - RefreshProgress::Complete(document) => document.map(|document| *document), - }) - } else { - self.refresh_document() - } + self.refresh_after_mutation(viewports) })(); if result.is_err() { self.faulted = true; @@ -901,7 +928,24 @@ impl ReplSession { self.faulted } + #[cfg(test)] pub fn dispatch(&mut self, input: &crate::events::Input) -> Result { + self.dispatch_mode(input, None) + } + + pub fn dispatch_incremental( + &mut self, + input: &crate::events::Input, + viewports: &std::collections::BTreeMap, + ) -> Result { + self.dispatch_mode(input, Some(viewports)) + } + + fn dispatch_mode( + &mut self, + input: &crate::events::Input, + viewports: Option<&std::collections::BTreeMap>, + ) -> Result { self.require_idle_refresh()?; if self.faulted { bail!("runtime stopped after a callback or rendering failure"); @@ -915,7 +959,7 @@ impl ReplSession { self.bridge.select_window(input.window)?; let result = (|| { self.bridge.dispatch(&mut self.candidate.vm, input)?; - let document = self.refresh_document()?; + let document = self.refresh_after_mutation(viewports)?; Ok(ReplResult { value: "nil".into(), document, @@ -934,11 +978,26 @@ impl ReplSession { self.faulted = true; self.bridge.discard_pending(&mut self.candidate.vm)?; } - self.bridge - .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; + if !self.has_pending_refresh() { + self.bridge + .reclaim(&mut self.candidate.vm, &mut self.candidate.roots)?; + } result } } + +impl Drop for ReplSession { + fn drop(&mut self) { + if let Some(mut work) = self.refresh.take() + && let Some(pending) = work.pending.take() + { + // Close the read-only continuation and checkpoint roots before + // Candidate releases the globals/services of a discarded domain. + let _ = self.bridge.cancel_layout_frame(&mut self.candidate.vm, *pending); + } + } +} + fn shift_node_lines(node: &mut CodeNode, offset: usize) { node.start_line += offset; node.end_line += offset;