Skip to main content

gpui_rhai/
engine.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::rc::Rc;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9use rhai::{
10    AST, ASTNode, Array, Dynamic, Engine, EvalAltResult, Expr, FnPtr, FuncArgs, FuncRegistration,
11    ImmutableString, Map, Module, ModuleResolver, Position, Scope, Stmt,
12};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::asset::{AssetId, ImageDecodeHandle, asset_id_from_script};
17use crate::backend::{AstInterpreter, ExecutionBackend};
18use crate::canvas::register_canvas_api;
19use crate::column_resize::{
20    ColumnMeasurementRegistry, ColumnResizePrimitiveHandler, IntrinsicTextMeasurePrimitiveHandler,
21    column_resize_primitive_descriptor, intrinsic_text_measure_primitive_descriptor,
22};
23use crate::component::{ComponentExportCollector, ComponentExportError, ComponentRegistry};
24use crate::context::{UiContext, register_ui_context_api};
25use crate::date::register_date_api;
26use crate::drag_drop::{
27    DragSourcePrimitiveHandler, DropZonePrimitiveHandler, drag_source_primitive_descriptor,
28    drop_zone_primitive_descriptor,
29};
30use crate::draggable::{DraggablePrimitiveHandler, draggable_primitive_descriptor};
31use crate::motion::register_motion_api;
32use crate::node::{
33    asset_image_node, box_node, canvas_node, column_node, directional_asset_image_node,
34    directional_image_node, error_boundary_node, fragment_node, generic_directional_image_node,
35    generic_image_node, image_node, layer_node, lazy_error_boundary_node, motion_group_node,
36    overlay_node, rich_text_node, row_node, span_value, stack_node, svg_node, text_node,
37};
38use crate::pan_zoom::{PanZoomPrimitiveHandler, pan_zoom_primitive_descriptor};
39use crate::primitive::{PrimitiveDescriptor, PrimitiveError, PrimitiveHandler, PrimitiveRegistry};
40use crate::range_input::{RangeInputPrimitiveHandler, range_input_primitive_descriptor};
41use crate::range_slider::{RangeSliderPrimitiveHandler, range_slider_primitive_descriptor};
42use crate::resizable::{ResizablePrimitiveHandler, resizable_primitive_descriptor};
43use crate::rotatable::{RotatablePrimitiveHandler, rotatable_primitive_descriptor};
44use crate::selection_area::{SelectionAreaPrimitiveHandler, selection_area_primitive_descriptor};
45use crate::sortable::{SortablePrimitiveHandler, sortable_primitive_descriptor};
46use crate::split_resize::{SplitResizePrimitiveHandler, split_resize_primitive_descriptor};
47use crate::style::register_style_api;
48use crate::text_area::{
49    TextAreaPrimitiveHandler, register_text_area_api, text_area_primitive_descriptor,
50};
51use crate::text_input::{TextInputPrimitiveHandler, text_input_primitive_descriptor};
52use crate::value::OpaqueHandle;
53use crate::{
54    ComponentInstancePath, ComponentStateSchema, EventSchema, ExecutionPhase, ModuleId,
55    RenderStateTransaction, UiNode, UiRuntimeState, UiValue,
56};
57use crate::{SubscriptionHandle, TaskHandle};
58
59#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub struct ScriptGeneration(u64);
61
62impl ScriptGeneration {
63    #[must_use]
64    pub const fn get(self) -> u64 {
65        self.0
66    }
67
68    #[cfg(test)]
69    #[must_use]
70    pub(crate) fn initial() -> Self {
71        Self(1)
72    }
73
74    #[cfg(test)]
75    #[must_use]
76    pub(crate) fn next(self) -> Self {
77        Self(self.0.saturating_add(1))
78    }
79}
80
81impl fmt::Display for ScriptGeneration {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        self.0.fmt(formatter)
84    }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum ExecutionOperation {
90    Compile,
91    Render,
92    /// A formal-component subtree returned without executing its Rhai render.
93    /// The payload is the number of retained formal component instances in the
94    /// reused subtree, including its root.
95    ComponentReuse(usize),
96    VirtualCollection(String),
97    Lifecycle(String),
98    Callback(String),
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct ExecutionTiming {
103    pub operation: ExecutionOperation,
104    pub source: String,
105    pub duration: Duration,
106    pub operations: u64,
107    /// The effective Host quota captured at the start of this execution.
108    pub operation_limit: u64,
109    /// Cumulative consumption of the enclosing execution round. `operations`
110    /// remains this timing span's delta for performance attribution.
111    pub round_operations: u64,
112    pub operation_semantics: u32,
113    pub slow: bool,
114    pub succeeded: bool,
115}
116
117pub const OPERATION_SEMANTICS_VERSION: u32 = 2;
118
119#[derive(Debug, Error)]
120pub enum RuntimeError {
121    #[error("failed to compile Rhai UI source: {0}")]
122    Compile(#[source] Box<EvalAltResult>),
123    #[error("failed to evaluate Rhai UI source: {0}")]
124    Evaluate(#[source] Box<EvalAltResult>),
125    #[error("callback `{name}` is invalid: {source}")]
126    CallbackDefinition {
127        name: String,
128        #[source]
129        source: Box<EvalAltResult>,
130    },
131    #[error("callback `{name}` cannot be retained: {source}")]
132    RetainedCallback {
133        name: String,
134        source: ScriptCallbackDefinitionError,
135    },
136    #[error(
137        "callback `{name}` belongs to script generation {callback_generation}, but current generation is {current_generation}"
138    )]
139    StaleCallback {
140        name: String,
141        callback_generation: ScriptGeneration,
142        current_generation: ScriptGeneration,
143    },
144    #[error("callback `{name}` belongs to an unmounted incarnation of component `{component}`")]
145    StaleComponentCallback {
146        name: String,
147        component: ComponentInstancePath,
148    },
149    #[error("component runtime failed: {0}")]
150    ComponentRuntime(String),
151    #[error("component invocation `{0}` is not retained in the active generation")]
152    MissingComponentInvocation(ComponentInstancePath),
153    #[error("invalid script import: {0}")]
154    Import(String),
155    #[error(
156        "compiled Rhai source contains an assignment target that Rhai 1.26 cannot evaluate safely at {0}"
157    )]
158    InvalidAssignmentTarget(Position),
159}
160
161#[derive(Debug, Error)]
162pub enum ScriptCallbackDefinitionError {
163    #[error("anonymous or capturing functions cannot escape one synchronous Rhai evaluation")]
164    Anonymous,
165    #[error("curried argument {index} cannot cross the retained callback boundary: {source}")]
166    InvalidCurry {
167        index: usize,
168        source: crate::UiValueError,
169    },
170}
171
172/// Private provenance carried inside a callback prop while a formal component
173/// renders. Rhai clones curried values when a callback is forwarded, so this
174/// survives arbitrary component depth without adding a public constructor or
175/// type. Components treat callback props as opaque; the marker is stripped
176/// before the callback crosses the retained boundary.
177#[derive(Clone, Debug)]
178struct ComponentCallbackBinding {
179    component: ComponentInstancePath,
180    incarnation: crate::ComponentIncarnation,
181    events: BTreeMap<String, EventSchema>,
182    context: Option<crate::invocation::ScriptInvocationContext>,
183}
184
185#[derive(Clone)]
186pub struct CompiledUi {
187    ast: AST,
188    generation: ScriptGeneration,
189}
190
191#[derive(Clone, Debug)]
192pub struct ScriptCallback {
193    function: FnPtr,
194    curry: Vec<UiValue>,
195    generation: ScriptGeneration,
196    component: Option<ComponentInstancePath>,
197    incarnation: Option<crate::ComponentIncarnation>,
198    events: BTreeMap<String, EventSchema>,
199    #[allow(deprecated)]
200    native_context: Option<crate::invocation::ScriptInvocationContext>,
201}
202
203impl PartialEq for ScriptCallback {
204    fn eq(&self, other: &Self) -> bool {
205        self.name() == other.name()
206            && self.curry == other.curry
207            && self.generation == other.generation
208            && self.component == other.component
209            && self.incarnation == other.incarnation
210    }
211}
212
213impl Eq for ScriptCallback {}
214
215impl ScriptCallback {
216    #[must_use]
217    pub fn name(&self) -> &str {
218        self.function.fn_name()
219    }
220
221    #[must_use]
222    pub fn generation(&self) -> ScriptGeneration {
223        self.generation
224    }
225
226    pub(crate) fn try_from_fn_ptr(
227        mut function: FnPtr,
228        generation: ScriptGeneration,
229    ) -> Result<Self, ScriptCallbackDefinitionError> {
230        if function.is_anonymous() {
231            return Err(ScriptCallbackDefinitionError::Anonymous);
232        }
233        let mut binding = None;
234        let mut curry = Vec::new();
235        for (index, value) in function.iter_curry().cloned().enumerate() {
236            if value.is::<ComponentCallbackBinding>() {
237                binding = Some(value.cast::<ComponentCallbackBinding>());
238                continue;
239            }
240            curry.push(
241                UiValue::from_dynamic(value).map_err(|source| {
242                    ScriptCallbackDefinitionError::InvalidCurry { index, source }
243                })?,
244            );
245        }
246        function.set_curry(curry.iter().cloned().map(UiValue::into_dynamic));
247        let (component, incarnation, events, native_context) = binding.map_or_else(
248            || (None, None, BTreeMap::new(), None),
249            |binding| {
250                (
251                    Some(binding.component),
252                    Some(binding.incarnation),
253                    binding.events,
254                    binding.context,
255                )
256            },
257        );
258        Ok(Self {
259            function,
260            curry,
261            generation,
262            component,
263            incarnation,
264            events,
265            native_context,
266        })
267    }
268
269    pub(crate) fn bind_generation(&mut self, generation: ScriptGeneration) {
270        self.generation = generation;
271    }
272
273    #[cfg(test)]
274    pub(crate) fn bind_component_if_unset(
275        &mut self,
276        component: ComponentInstancePath,
277        events: BTreeMap<String, EventSchema>,
278    ) {
279        if self.component.is_none() {
280            self.component = Some(component);
281            self.events = events;
282        }
283    }
284
285    pub(crate) fn bind_component_scope_if_unset(
286        &mut self,
287        component: &ComponentInstancePath,
288        incarnation: crate::ComponentIncarnation,
289        events: BTreeMap<String, EventSchema>,
290    ) {
291        if self.component.is_none() {
292            self.component = Some(component.clone());
293            self.events = events;
294        }
295        if self.component.as_ref() == Some(component) && self.incarnation.is_none() {
296            self.incarnation = Some(incarnation);
297        }
298    }
299
300    pub(crate) fn bind_native_context_if_unset(
301        &mut self,
302        context: crate::invocation::ScriptInvocationContext,
303    ) {
304        if self.native_context.is_none() {
305            self.native_context = Some(context);
306        }
307    }
308
309    pub(crate) fn component(&self) -> Option<&ComponentInstancePath> {
310        self.component.as_ref()
311    }
312
313    pub(crate) const fn incarnation(&self) -> Option<crate::ComponentIncarnation> {
314        self.incarnation
315    }
316
317    pub(crate) fn events(&self) -> &BTreeMap<String, EventSchema> {
318        &self.events
319    }
320
321    pub(crate) fn native_context(&self) -> Option<&crate::invocation::ScriptInvocationContext> {
322        self.native_context.as_ref()
323    }
324}
325
326impl CompiledUi {
327    #[must_use]
328    pub fn generation(&self) -> ScriptGeneration {
329        self.generation
330    }
331
332    #[must_use]
333    pub fn has_function(&self, name: &str, parameter_count: usize) -> bool {
334        self.ast
335            .iter_functions()
336            .any(|function| function.name == name && function.params.len() == parameter_count)
337    }
338}
339
340struct ActiveComponentRender {
341    root_context: UiContext,
342    stack: Vec<ComponentInstancePath>,
343    contexts: Vec<UiContext>,
344    seen: BTreeSet<ComponentInstancePath>,
345    positions: BTreeMap<String, usize>,
346    transaction: RenderStateTransaction,
347    generation: ScriptGeneration,
348    state_snapshot: crate::StateStore,
349    event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
350    invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
351    effect_keys: Vec<Option<BTreeSet<String>>>,
352    effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
353    timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
354    signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
355    element_refs: BTreeSet<crate::ElementRefId>,
356    virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
357    environment: ComponentRenderEnvironment,
358    reuse: Option<ComponentReuseSnapshot>,
359    reused: Vec<(String, usize)>,
360    failed_component: Option<ComponentInstancePath>,
361}
362
363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
364struct ComponentRenderEnvironment {
365    theme_generation: Option<u64>,
366    component_style_generation: u64,
367    locale_generation: Option<u64>,
368    calendar_day: crate::GregorianDate,
369}
370
371#[derive(Clone)]
372struct ComponentReusePlan {
373    previous_root: Rc<UiNode>,
374    subtrees: crate::node::ComponentSubtreeIndex,
375    dirty: BTreeSet<ComponentInstancePath>,
376}
377
378impl ComponentReusePlan {
379    fn new(previous_root: Rc<UiNode>, dirty: BTreeSet<ComponentInstancePath>) -> Self {
380        let subtrees = crate::node::ComponentSubtreeIndex::new(&previous_root);
381        Self {
382            previous_root,
383            subtrees,
384            dirty,
385        }
386    }
387}
388
389#[derive(Clone)]
390struct ComponentReuseSnapshot {
391    previous_root: Rc<UiNode>,
392    subtrees: crate::node::ComponentSubtreeIndex,
393    dirty: BTreeSet<ComponentInstancePath>,
394    invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
395    event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
396    effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
397    timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
398    signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
399    element_refs: BTreeSet<crate::ElementRefId>,
400    virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
401}
402
403struct ReusedComponentScope {
404    node: UiNode,
405    invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
406    event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
407    resources: ReusedComponentResources,
408}
409
410struct ReusedComponentResources {
411    effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
412    timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
413    signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
414    element_refs: BTreeSet<crate::ElementRefId>,
415    virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
416}
417
418#[derive(Clone, Debug)]
419struct VirtualCollectionRecipe {
420    id: crate::VirtualCollectionId,
421    data: crate::VirtualCollectionData,
422    renderer: FnPtr,
423    renderer_context: crate::invocation::ScriptInvocationContext,
424    context: UiContext,
425    generation: ScriptGeneration,
426}
427
428type ActiveComponentRenderState = Rc<RefCell<Option<ActiveComponentRender>>>;
429
430#[derive(Clone)]
431struct PendingComponentCommit {
432    root: ComponentInstancePath,
433    previous: BTreeSet<ComponentInstancePath>,
434    retained_root: Option<ComponentInstancePath>,
435    transaction: RenderStateTransaction,
436    event_handlers: BTreeMap<(ComponentInstancePath, String), ScriptCallback>,
437}
438
439#[derive(Clone, Debug)]
440pub struct ComponentInvocationRecipe {
441    path: ComponentInstancePath,
442    parent: ComponentInstancePath,
443    component: ModuleId,
444    export: String,
445    declared_key: Option<String>,
446    props: crate::ComponentProps,
447    script_props: Map,
448    component_context: UiContext,
449    caller_context: UiContext,
450    part_styles: BTreeMap<String, crate::Style>,
451    event_callbacks: Vec<(String, FnPtr)>,
452    component_events: BTreeMap<String, EventSchema>,
453    declared_effects: BTreeSet<String>,
454    render: FnPtr,
455    snapshot: crate::node::ComponentOwnedSnapshot,
456    context: crate::invocation::ScriptInvocationContext,
457    generation: ScriptGeneration,
458    environment: ComponentRenderEnvironment,
459    reusable: bool,
460}
461
462#[derive(Clone)]
463pub(crate) struct RegisteredComponentRender {
464    generation: ScriptGeneration,
465    render: FnPtr,
466}
467
468type ComponentRenderRegistry = Rc<RefCell<BTreeMap<ModuleId, RegisteredComponentRender>>>;
469
470impl ComponentInvocationRecipe {
471    #[must_use]
472    pub fn path(&self) -> &ComponentInstancePath {
473        &self.path
474    }
475
476    #[must_use]
477    pub fn parent(&self) -> &ComponentInstancePath {
478        &self.parent
479    }
480
481    #[must_use]
482    pub fn component(&self) -> &ModuleId {
483        &self.component
484    }
485
486    #[must_use]
487    pub fn export(&self) -> &str {
488        &self.export
489    }
490
491    #[must_use]
492    pub fn declared_key(&self) -> Option<&str> {
493        self.declared_key.as_deref()
494    }
495
496    #[must_use]
497    pub fn props(&self) -> &crate::ComponentProps {
498        &self.props
499    }
500
501    #[must_use]
502    pub fn render_name(&self) -> &str {
503        self.render.fn_name()
504    }
505
506    #[must_use]
507    pub fn has_invocation_context(&self) -> bool {
508        let _ = &self.context;
509        true
510    }
511
512    #[must_use]
513    pub fn generation(&self) -> ScriptGeneration {
514        self.generation
515    }
516}
517
518pub struct RuntimeEngine {
519    engine: Engine,
520    generation: ScriptGeneration,
521    preparation_generation: Option<ScriptGeneration>,
522    component_exports: ComponentExportCollector,
523    evaluation_generation: Rc<Cell<ScriptGeneration>>,
524    primitives: PrimitiveRegistry,
525    primitive_modules: BTreeMap<String, Module>,
526    timings: RefCell<Vec<ExecutionTiming>>,
527    last_failed_timing: RefCell<Option<ExecutionTiming>>,
528    last_failed_component: RefCell<Option<ComponentInstancePath>>,
529    operation_tracker: Rc<OperationTracker>,
530    operation_limit: Rc<Cell<u64>>,
531    slow_threshold: Duration,
532    component_render: ActiveComponentRenderState,
533    component_invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
534    component_effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
535    component_timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
536    component_signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
537    component_element_refs: BTreeSet<crate::ElementRefId>,
538    pending_component_commits: BTreeMap<ComponentInstancePath, PendingComponentCommit>,
539    component_renderers: ComponentRenderRegistry,
540    native_handlers: crate::NativeHandlerRegistry,
541    syntax_registry: crate::SyntaxRegistry,
542    document_runtime: crate::DocumentRuntimeConfig,
543    #[cfg(feature = "charts")]
544    chart_transforms: crate::ChartTransformRegistry,
545    #[cfg(feature = "charts")]
546    chart_geo: crate::ChartGeoRegistry,
547    #[cfg(feature = "charts")]
548    chart_series: crate::ChartSeriesRegistry,
549    #[cfg(feature = "charts")]
550    chart_formatters: crate::ChartFormatterRegistry,
551    virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
552}
553
554pub const DEFAULT_SCRIPT_OPERATION_LIMIT: u64 = 1_000_000;
555
556#[derive(Debug, Default)]
557struct OperationTracker {
558    last_absolute: Cell<u64>,
559    total: Cell<u64>,
560}
561
562impl OperationTracker {
563    fn begin(&self, inherited_base: u64) {
564        self.last_absolute.set(inherited_base);
565        self.total.set(0);
566    }
567
568    fn observe(&self, absolute: u64) -> u64 {
569        let last_absolute = self.last_absolute.get();
570        let delta = if absolute >= last_absolute {
571            absolute - last_absolute
572        } else {
573            // Returning from a nested evaluator resumes a smaller parent
574            // counter. Rhai invokes progress once per operation, so the first
575            // lower observation represents one newly consumed parent step.
576            1
577        };
578        let total = self.total.get().saturating_add(delta);
579        self.total.set(total);
580        self.last_absolute.set(absolute);
581        total
582    }
583
584    fn align(&self, inherited_base: u64) {
585        // Stored NativeCallContext values clone the absolute counter captured
586        // in an earlier evaluator. Align only that absolute baseline before a
587        // retained call; the current execution session's aggregate must remain
588        // intact across multiple dirty components or virtual items.
589        self.last_absolute.set(inherited_base);
590    }
591}
592
593#[derive(Clone, Copy)]
594struct ExecutionTimingStart {
595    instant: Instant,
596    operations: u64,
597    operation_limit: u64,
598}
599
600#[derive(Clone)]
601pub(crate) struct RuntimeEngineCheckpoint {
602    generation: ScriptGeneration,
603    evaluation_generation: ScriptGeneration,
604    component_invocations: BTreeMap<ComponentInstancePath, ComponentInvocationRecipe>,
605    component_effects: BTreeMap<crate::EffectId, crate::EffectDescriptor>,
606    component_timers: BTreeMap<crate::TimerId, crate::TimerDescriptor>,
607    component_signals: BTreeMap<crate::SignalId, crate::signal::SignalDescriptor>,
608    component_element_refs: BTreeSet<crate::ElementRefId>,
609    pending_component_commits: BTreeMap<ComponentInstancePath, PendingComponentCommit>,
610    virtual_collections: BTreeMap<crate::VirtualCollectionId, VirtualCollectionRecipe>,
611    component_snapshots: Vec<(crate::node::ComponentOwnedSnapshot, Option<Rc<UiNode>>)>,
612}
613
614impl Default for RuntimeEngine {
615    fn default() -> Self {
616        Self::new()
617    }
618}
619
620impl RuntimeEngine {
621    #[cfg(feature = "dev-reload")]
622    pub(crate) fn candidate_engine(&self) -> Self {
623        let mut candidate = Self::new();
624        candidate.slow_threshold = self.slow_threshold;
625        candidate.operation_limit.set(self.operation_limit.get());
626        let (global, functions) = self.expression_depth_limits();
627        candidate.set_expression_depth_limits(global, functions);
628        #[cfg(feature = "charts")]
629        {
630            candidate.chart_transforms.copy_from(&self.chart_transforms);
631            candidate.chart_geo.copy_from(&self.chart_geo);
632            candidate.chart_series.copy_from(&self.chart_series);
633            candidate.chart_formatters.copy_from(&self.chart_formatters);
634        }
635        candidate
636    }
637
638    /// Create a runtime with all built-in primitive descriptors.
639    ///
640    /// # Panics
641    ///
642    /// Panics only if a compile-time built-in descriptor violates the same
643    /// validation rules enforced for downstream custom primitives.
644    #[must_use]
645    pub fn new() -> Self {
646        let mut engine = Engine::new();
647        engine.set_module_resolver(crate::source::RestrictedModuleResolver::new());
648        let operation_tracker = Rc::new(OperationTracker::default());
649        let progress = Rc::clone(&operation_tracker);
650        let operation_limit = Rc::new(Cell::new(DEFAULT_SCRIPT_OPERATION_LIMIT));
651        let limit_probe = Rc::clone(&operation_limit);
652        engine.on_progress(move |operations| {
653            let total = progress.observe(operations);
654            let limit = limit_probe.get();
655            (total > limit).then(|| {
656                Dynamic::from(format!(
657                    "script operation budget exceeded: {total} > {limit}"
658                ))
659            })
660        });
661        engine.build_type::<UiNode>();
662        engine.build_type::<OpaqueHandle>();
663        engine.build_type::<TaskHandle>();
664        engine.build_type::<SubscriptionHandle>();
665        engine.build_type::<AssetId>();
666        engine.build_type::<ImageDecodeHandle>();
667        engine.build_type::<crate::NativeSignal>();
668        engine.build_type::<crate::EventResponse>();
669        engine.build_type::<crate::ElementRef>();
670        engine.build_type::<crate::NativeHandlerRef>();
671        crate::native_collection::register_native_collection_api(&mut engine);
672        crate::document::register_document_api(&mut engine);
673        #[cfg(feature = "charts")]
674        {
675            crate::chart::register_chart_data_api(&mut engine);
676            register_chart_validation_api(&mut engine);
677        }
678        engine.build_type::<crate::Span>();
679        register_ui_context_api(&mut engine);
680        register_date_api(&mut engine);
681        register_style_api(&mut engine);
682        register_motion_api(&mut engine);
683        register_text_area_api(&mut engine);
684        register_canvas_api(&mut engine);
685        crate::collection_projection::register_collection_projection_api(&mut engine);
686        let evaluation_generation = Rc::new(Cell::new(ScriptGeneration::default()));
687        let component_exports = ComponentExportCollector::new();
688        let (component_render, component_renderers) = register_component_runtime_apis(
689            &mut engine,
690            &component_exports,
691            &evaluation_generation,
692        );
693        let primitives = PrimitiveRegistry::new();
694        let native_handlers = crate::NativeHandlerRegistry::new();
695        let syntax_registry = crate::SyntaxRegistry::new();
696        let document_runtime = crate::DocumentRuntimeConfig::new();
697        #[cfg(feature = "charts")]
698        let chart_transforms = crate::ChartTransformRegistry::new();
699        #[cfg(feature = "charts")]
700        let chart_geo = crate::ChartGeoRegistry::new();
701        #[cfg(feature = "charts")]
702        let chart_series = crate::ChartSeriesRegistry::new();
703        #[cfg(feature = "charts")]
704        let chart_formatters = crate::ChartFormatterRegistry::new();
705        register_native_handler_api(&mut engine, &native_handlers);
706
707        register_node_apis(&mut engine);
708
709        configure_engine_limits(&mut engine);
710
711        let mut runtime = Self {
712            engine,
713            generation: ScriptGeneration::default(),
714            preparation_generation: None,
715            component_exports,
716            evaluation_generation,
717            primitives,
718            primitive_modules: BTreeMap::new(),
719            timings: RefCell::new(Vec::new()),
720            last_failed_timing: RefCell::new(None),
721            last_failed_component: RefCell::new(None),
722            operation_tracker,
723            operation_limit,
724            slow_threshold: Duration::from_millis(16),
725            component_render,
726            component_invocations: BTreeMap::new(),
727            component_effects: BTreeMap::new(),
728            component_timers: BTreeMap::new(),
729            component_signals: BTreeMap::new(),
730            component_element_refs: BTreeSet::new(),
731            pending_component_commits: BTreeMap::new(),
732            component_renderers,
733            native_handlers,
734            syntax_registry,
735            document_runtime,
736            #[cfg(feature = "charts")]
737            chart_transforms,
738            #[cfg(feature = "charts")]
739            chart_geo,
740            #[cfg(feature = "charts")]
741            chart_series,
742            #[cfg(feature = "charts")]
743            chart_formatters,
744            virtual_collections: BTreeMap::new(),
745        };
746        runtime.register_builtin_primitives();
747        runtime
748    }
749
750    fn register_builtin_primitives(&mut self) {
751        let column_measurements = ColumnMeasurementRegistry::default();
752        self.register_primitive(
753            column_resize_primitive_descriptor(),
754            ColumnResizePrimitiveHandler::new(column_measurements.clone()),
755        )
756        .expect("built-in column resize primitive descriptor is valid");
757        self.register_primitive(
758            intrinsic_text_measure_primitive_descriptor(),
759            IntrinsicTextMeasurePrimitiveHandler::new(column_measurements),
760        )
761        .expect("built-in intrinsic text measurement primitive descriptor is valid");
762        self.register_primitive(
763            split_resize_primitive_descriptor(),
764            SplitResizePrimitiveHandler,
765        )
766        .expect("built-in split resize primitive descriptor is valid");
767        self.register_primitive(
768            resizable_primitive_descriptor(),
769            ResizablePrimitiveHandler::default(),
770        )
771        .expect("built-in resizable primitive descriptor is valid");
772        self.register_primitive(draggable_primitive_descriptor(), DraggablePrimitiveHandler)
773            .expect("built-in draggable primitive descriptor is valid");
774        self.register_primitive(
775            drag_source_primitive_descriptor(),
776            DragSourcePrimitiveHandler::default(),
777        )
778        .expect("built-in drag source primitive descriptor is valid");
779        self.register_primitive(drop_zone_primitive_descriptor(), DropZonePrimitiveHandler)
780            .expect("built-in drop zone primitive descriptor is valid");
781        self.register_primitive(
782            sortable_primitive_descriptor(),
783            SortablePrimitiveHandler::default(),
784        )
785        .expect("built-in sortable primitive descriptor is valid");
786        self.register_primitive(
787            pan_zoom_primitive_descriptor(),
788            PanZoomPrimitiveHandler::default(),
789        )
790        .expect("built-in pan zoom primitive descriptor is valid");
791        self.register_primitive(
792            range_slider_primitive_descriptor(),
793            RangeSliderPrimitiveHandler::default(),
794        )
795        .expect("built-in range slider primitive descriptor is valid");
796        self.register_primitive(rotatable_primitive_descriptor(), RotatablePrimitiveHandler)
797            .expect("built-in rotatable primitive descriptor is valid");
798        self.register_primitive(
799            selection_area_primitive_descriptor(),
800            SelectionAreaPrimitiveHandler::default(),
801        )
802        .expect("built-in selection area primitive descriptor is valid");
803        self.register_primitive(
804            text_input_primitive_descriptor(),
805            TextInputPrimitiveHandler::default(),
806        )
807        .expect("built-in TextInput primitive descriptor is valid");
808        self.register_primitive(
809            text_area_primitive_descriptor(),
810            TextAreaPrimitiveHandler::default(),
811        )
812        .expect("built-in Textarea primitive descriptor is valid");
813        self.register_primitive(
814            range_input_primitive_descriptor(),
815            RangeInputPrimitiveHandler::default(),
816        )
817        .expect("built-in RangeInput primitive descriptor is valid");
818        self.register_primitive(
819            crate::code_viewer_primitive_descriptor(),
820            crate::CodeViewerPrimitiveHandler::new(
821                self.syntax_registry.clone(),
822                self.document_runtime.clone(),
823            ),
824        )
825        .expect("built-in CodeViewer primitive descriptor is valid");
826        self.register_primitive(
827            crate::diff_viewer_primitive_descriptor(),
828            crate::DiffViewerPrimitiveHandler::new(
829                self.syntax_registry.clone(),
830                self.document_runtime.clone(),
831            ),
832        )
833        .expect("built-in DiffViewer primitive descriptor is valid");
834        #[cfg(feature = "charts")]
835        self.register_primitive(
836            crate::chart_primitive_descriptor(),
837            crate::ChartPrimitiveHandler::new(
838                self.chart_transforms.clone(),
839                self.chart_geo.clone(),
840                self.chart_series.clone(),
841                self.chart_formatters.clone(),
842            ),
843        )
844        .expect("built-in Chart primitive descriptor is valid");
845    }
846
847    /// Compile the default application entry source.
848    ///
849    /// # Errors
850    ///
851    /// Returns [`RuntimeError::Compile`] when Rhai rejects the source.
852    pub fn compile(&mut self, source: &str) -> Result<CompiledUi, RuntimeError> {
853        self.compile_named("ui/main.rhai", source)
854    }
855
856    /// Compile source with an explicit diagnostic name and advance the script generation.
857    ///
858    /// # Errors
859    ///
860    /// Returns [`RuntimeError::Compile`] when Rhai rejects the source.
861    pub fn compile_named(
862        &mut self,
863        source_name: &str,
864        source: &str,
865    ) -> Result<CompiledUi, RuntimeError> {
866        let generation = self.candidate_generation();
867        self.evaluation_generation.set(generation);
868        self.begin_execution_session();
869        let started = self.begin_timing();
870        let result = self.engine.compile(source);
871        self.record_timing(
872            ExecutionOperation::Compile,
873            source_name,
874            started,
875            result.is_ok(),
876        );
877        let mut ast = result.map_err(|error| RuntimeError::Compile(error.into()))?;
878        validate_assignment_targets(&ast)?;
879        ast.set_source(source_name);
880        Ok(CompiledUi { ast, generation })
881    }
882
883    /// Compile source while eagerly resolving and embedding literal imports.
884    ///
885    /// # Errors
886    ///
887    /// Returns [`RuntimeError::Compile`] for source, import, or module errors.
888    pub fn compile_self_contained_named(
889        &mut self,
890        source_name: &str,
891        source: &str,
892    ) -> Result<CompiledUi, RuntimeError> {
893        let generation = self.candidate_generation();
894        self.evaluation_generation.set(generation);
895        self.begin_execution_session();
896        let started = self.begin_timing();
897        let result = self
898            .engine
899            .compile_into_self_contained(&Scope::new(), source);
900        self.record_timing(
901            ExecutionOperation::Compile,
902            source_name,
903            started,
904            result.is_ok(),
905        );
906        let mut ast = result.map_err(RuntimeError::Compile)?;
907        validate_literal_imports(&ast)?;
908        validate_assignment_targets(&ast)?;
909        ast.set_source(source_name);
910        Ok(CompiledUi { ast, generation })
911    }
912
913    pub fn set_module_resolver(&mut self, resolver: impl ModuleResolver + 'static) {
914        self.engine.set_module_resolver(resolver);
915    }
916
917    /// Compile all modules and the entry AST for one candidate under one unique
918    /// program identity. Independent calls receive different generations even
919    /// when neither candidate has been rendered yet.
920    ///
921    /// # Errors
922    ///
923    /// Returns the error produced by `operation`.
924    pub fn with_program_preparation<T, E>(
925        &mut self,
926        operation: impl FnOnce(&mut Self) -> Result<T, E>,
927    ) -> Result<T, E> {
928        if self.preparation_generation.is_some() {
929            return operation(self);
930        }
931        let generation = Self::allocate_generation();
932        self.preparation_generation = Some(generation);
933        let result = operation(self);
934        self.preparation_generation = None;
935        result
936    }
937
938    fn begin_component_render(
939        &self,
940        context: UiContext,
941        generation: ScriptGeneration,
942        root_effects: Option<BTreeSet<String>>,
943        reuse_plan: Option<ComponentReusePlan>,
944    ) -> Result<(), RuntimeError> {
945        let root = context.component_path().clone();
946        context.reset_non_reusable_render_reads();
947        let (state_snapshot, environment, event_handlers) = {
948            let runtime = context.runtime().try_borrow().map_err(|_| {
949                RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
950            })?;
951            (
952                runtime.component_state.clone(),
953                ComponentRenderEnvironment {
954                    theme_generation: runtime.theme.as_ref().map(crate::ThemeManager::generation),
955                    component_style_generation: runtime.component_style_generation(),
956                    locale_generation: runtime
957                        .locale
958                        .as_ref()
959                        .map(crate::LocaleManager::generation),
960                    calendar_day: runtime.calendar_clock.today(),
961                },
962                reuse_plan
963                    .as_ref()
964                    .map(|_| runtime.component_event_handlers_in_scope(&root)),
965            )
966        };
967        let mut transaction = context
968            .runtime()
969            .try_borrow()
970            .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned()))?
971            .component_state
972            .begin_render_scope(root.clone());
973        let _ = transaction.retain_existing(&root);
974        let reuse = reuse_plan.map(|plan| ComponentReuseSnapshot {
975            subtrees: plan.subtrees,
976            previous_root: plan.previous_root,
977            dirty: plan.dirty,
978            invocations: self
979                .component_invocations
980                .iter()
981                .filter(|(path, _)| path.is_within(&root))
982                .map(|(path, recipe)| (path.clone(), recipe.clone()))
983                .collect(),
984            event_handlers: event_handlers.unwrap_or_default(),
985            effects: self
986                .component_effects
987                .iter()
988                .filter(|(id, _)| id.component().is_within(&root))
989                .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
990                .collect(),
991            timers: self
992                .component_timers
993                .iter()
994                .filter(|(id, _)| id.component().is_within(&root))
995                .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
996                .collect(),
997            signals: self
998                .component_signals
999                .iter()
1000                .filter(|(id, _)| id.component().is_within(&root))
1001                .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1002                .collect(),
1003            element_refs: self
1004                .component_element_refs
1005                .iter()
1006                .filter(|id| id.component().is_within(&root))
1007                .cloned()
1008                .collect(),
1009            virtual_collections: self
1010                .virtual_collections
1011                .iter()
1012                .filter(|(id, _)| id.component.is_within(&root))
1013                .map(|(id, recipe)| (id.clone(), recipe.clone()))
1014                .collect(),
1015        });
1016        let mut active = self.component_render.try_borrow_mut().map_err(|_| {
1017            RuntimeError::ComponentRuntime("component render stack is already borrowed".to_owned())
1018        })?;
1019        *active = Some(ActiveComponentRender {
1020            root_context: context.clone(),
1021            stack: vec![root],
1022            contexts: vec![context],
1023            seen: BTreeSet::new(),
1024            positions: BTreeMap::new(),
1025            transaction,
1026            generation,
1027            state_snapshot,
1028            event_handlers: BTreeMap::new(),
1029            invocations: BTreeMap::new(),
1030            effect_keys: vec![root_effects],
1031            effects: BTreeMap::new(),
1032            timers: BTreeMap::new(),
1033            signals: BTreeMap::new(),
1034            element_refs: BTreeSet::new(),
1035            virtual_collections: BTreeMap::new(),
1036            environment,
1037            reuse,
1038            reused: Vec::new(),
1039            failed_component: None,
1040        });
1041        Ok(())
1042    }
1043
1044    fn finish_component_render(&mut self, commit: bool) -> Result<(), RuntimeError> {
1045        let active = self
1046            .component_render
1047            .try_borrow_mut()
1048            .map_err(|_| {
1049                RuntimeError::ComponentRuntime(
1050                    "component render stack is already borrowed".to_owned(),
1051                )
1052            })?
1053            .take();
1054        let Some(active) = active else {
1055            return Ok(());
1056        };
1057        if commit {
1058            self.timings
1059                .borrow_mut()
1060                .extend(
1061                    active
1062                        .reused
1063                        .iter()
1064                        .map(|(source, components)| ExecutionTiming {
1065                            operation: ExecutionOperation::ComponentReuse(*components),
1066                            source: source.clone(),
1067                            duration: Duration::ZERO,
1068                            operations: 0,
1069                            operation_limit: self.operation_limit(),
1070                            round_operations: self.operation_total(),
1071                            operation_semantics: OPERATION_SEMANTICS_VERSION,
1072                            slow: false,
1073                            succeeded: true,
1074                        }),
1075                );
1076            let root = active.root_context.component_path().clone();
1077            let previous = active.state_snapshot.paths();
1078            self.pending_component_commits.insert(
1079                root.clone(),
1080                PendingComponentCommit {
1081                    root: root.clone(),
1082                    previous,
1083                    retained_root: (!active.invocations.contains_key(&root)
1084                        && active.transaction.retains_root())
1085                    .then_some(root.clone()),
1086                    transaction: active.transaction,
1087                    event_handlers: active.event_handlers,
1088                },
1089            );
1090            self.component_invocations
1091                .retain(|path, _| !path.is_within(&root));
1092            self.component_invocations.extend(active.invocations);
1093            self.component_effects
1094                .retain(|id, _| !id.component().is_within(&root));
1095            self.component_effects.extend(active.effects);
1096            self.component_timers
1097                .retain(|id, _| !id.component().is_within(&root));
1098            self.component_timers.extend(active.timers);
1099            self.component_signals
1100                .retain(|id, _| !id.component().is_within(&root));
1101            self.component_signals.extend(active.signals);
1102            self.component_element_refs
1103                .retain(|id| !id.component().is_within(&root));
1104            self.component_element_refs.extend(active.element_refs);
1105            self.virtual_collections
1106                .retain(|id, _| !id.component.is_within(&root));
1107            self.virtual_collections.extend(active.virtual_collections);
1108        } else {
1109            *self.last_failed_component.borrow_mut() = active
1110                .failed_component
1111                .or_else(|| active.stack.last().cloned());
1112            active
1113                .root_context
1114                .runtime()
1115                .try_borrow_mut()
1116                .map_err(|_| {
1117                    RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
1118                })?
1119                .component_state = active.state_snapshot;
1120        }
1121        Ok(())
1122    }
1123
1124    /// Evaluate the compiled application's `view` function.
1125    ///
1126    /// # Errors
1127    ///
1128    /// Returns [`RuntimeError::Evaluate`] when `view` is missing, fails, or
1129    /// returns a value other than [`UiNode`].
1130    pub fn render(&mut self, compiled: &CompiledUi) -> Result<UiNode, RuntimeError> {
1131        self.evaluation_generation.set(compiled.generation);
1132        self.begin_execution_session();
1133        let checkpoint = self.execution_checkpoint();
1134        let root_path = ComponentInstancePath::root("App", "root");
1135        let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
1136        let context = UiContext::new(
1137            Rc::clone(&runtime),
1138            root_path.clone(),
1139            None,
1140            ExecutionPhase::Render,
1141            BTreeMap::new(),
1142        )
1143        .with_generation(compiled.generation);
1144        self.begin_component_render(context, compiled.generation, None, None)?;
1145        let started = self.begin_timing();
1146        let result = AstInterpreter::call_fn::<UiNode, _>(
1147            &self.engine,
1148            &compiled.ast,
1149            &mut Scope::new(),
1150            "view",
1151            (),
1152        );
1153        self.record_timing(
1154            ExecutionOperation::Render,
1155            compiled.ast.source().unwrap_or("<script>"),
1156            started,
1157            result.is_ok(),
1158        );
1159        self.finish_component_render(result.is_ok())?;
1160        let mut root = result.map_err(RuntimeError::Evaluate)?;
1161        if !self.component_effects_in_scope(&root_path).is_empty()
1162            || !self.component_timers_in_scope(&root_path).is_empty()
1163            || !self.component_signals_in_scope(&root_path).is_empty()
1164            || !self.component_element_refs_in_scope(&root_path).is_empty()
1165            || !self.virtual_collection_ids_in_scope(&root_path).is_empty()
1166        {
1167            self.restore_execution_checkpoint(checkpoint);
1168            return Err(RuntimeError::ComponentRuntime(
1169                "effectful, timer/signal/ref-owning, or virtual components require ScriptLifecycle"
1170                    .to_owned(),
1171            ));
1172        }
1173        if let Err(error) = self.commit_component_renders(&mut runtime.borrow_mut()) {
1174            self.restore_execution_checkpoint(checkpoint);
1175            return Err(error);
1176        }
1177        root.bind_generation(compiled.generation);
1178        self.generation = compiled.generation;
1179        Ok(root)
1180    }
1181
1182    /// Evaluate the compiled application's `view(ctx)` function.
1183    ///
1184    /// # Errors
1185    ///
1186    /// Returns [`RuntimeError::Evaluate`] when `view` fails or returns a value
1187    /// other than [`UiNode`].
1188    pub fn render_with_context(
1189        &mut self,
1190        compiled: &CompiledUi,
1191        context: UiContext,
1192    ) -> Result<UiNode, RuntimeError> {
1193        let runtime = Rc::clone(context.runtime());
1194        let root = context.component_path().clone();
1195        let snapshot = runtime
1196            .try_borrow_mut()
1197            .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned()))?
1198            .begin_transaction()
1199            .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?;
1200        let checkpoint = self.execution_checkpoint();
1201        let result = self.render_with_context_staged(compiled, context);
1202        let result = result.and_then(|node| {
1203            if !self.component_effects_in_scope(&root).is_empty()
1204                || !self.component_timers_in_scope(&root).is_empty()
1205                || !self.component_signals_in_scope(&root).is_empty()
1206                || !self.component_element_refs_in_scope(&root).is_empty()
1207                || !self.virtual_collection_ids_in_scope(&root).is_empty()
1208            {
1209                return Err(RuntimeError::ComponentRuntime(
1210                    "effectful, timer/signal/ref-owning, or virtual components require ScriptLifecycle"
1211                        .to_owned(),
1212                ));
1213            }
1214            self.commit_component_renders(&mut runtime.borrow_mut())?;
1215            Ok(node)
1216        });
1217        match result {
1218            Ok(node) => {
1219                runtime
1220                    .try_borrow_mut()
1221                    .map_err(|_| {
1222                        RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
1223                    })?
1224                    .commit_transaction()
1225                    .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?;
1226                Ok(node)
1227            }
1228            Err(error) => {
1229                runtime
1230                    .try_borrow_mut()
1231                    .map_err(|_| {
1232                        RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned())
1233                    })?
1234                    .restore(snapshot)
1235                    .map_err(|restore| RuntimeError::ComponentRuntime(restore.to_string()))?;
1236                self.restore_execution_checkpoint(checkpoint);
1237                Err(error)
1238            }
1239        }
1240    }
1241
1242    pub(crate) fn render_with_context_staged(
1243        &mut self,
1244        compiled: &CompiledUi,
1245        context: UiContext,
1246    ) -> Result<UiNode, RuntimeError> {
1247        self.render_with_context_staged_impl(compiled, context, None)
1248    }
1249
1250    pub(crate) fn render_with_context_staged_reusing(
1251        &mut self,
1252        compiled: &CompiledUi,
1253        context: UiContext,
1254        previous_root: Rc<UiNode>,
1255        dirty: &BTreeSet<ComponentInstancePath>,
1256    ) -> Result<UiNode, RuntimeError> {
1257        self.render_with_context_staged_impl(
1258            compiled,
1259            context,
1260            Some(ComponentReusePlan::new(previous_root, dirty.clone())),
1261        )
1262    }
1263
1264    fn render_with_context_staged_impl(
1265        &mut self,
1266        compiled: &CompiledUi,
1267        context: UiContext,
1268        reuse: Option<ComponentReusePlan>,
1269    ) -> Result<UiNode, RuntimeError> {
1270        self.evaluation_generation.set(compiled.generation);
1271        self.begin_execution_session();
1272        self.begin_component_render(context.clone(), compiled.generation, None, reuse)?;
1273        let started = self.begin_timing();
1274        let result = AstInterpreter::call_fn::<UiNode, _>(
1275            &self.engine,
1276            &compiled.ast,
1277            &mut Scope::new(),
1278            "view",
1279            (context,),
1280        );
1281        self.record_timing(
1282            ExecutionOperation::Render,
1283            compiled.ast.source().unwrap_or("<script>"),
1284            started,
1285            result.is_ok(),
1286        );
1287        self.finish_component_render(result.is_ok())?;
1288        let mut root = result.map_err(RuntimeError::Evaluate)?;
1289        root.bind_generation(compiled.generation);
1290        self.generation = compiled.generation;
1291        Ok(root)
1292    }
1293
1294    pub(crate) fn rerender_component(
1295        &mut self,
1296        component: &ComponentInstancePath,
1297        previous_root: &UiNode,
1298        dirty: &BTreeSet<ComponentInstancePath>,
1299    ) -> Result<UiNode, RuntimeError> {
1300        let mut recipe = self
1301            .component_invocations
1302            .get(component)
1303            .cloned()
1304            .ok_or_else(|| RuntimeError::MissingComponentInvocation(component.clone()))?;
1305        if recipe.generation != self.generation {
1306            return Err(RuntimeError::StaleCallback {
1307                name: recipe.render.fn_name().to_owned(),
1308                callback_generation: recipe.generation,
1309                current_generation: self.generation,
1310            });
1311        }
1312        recipe
1313            .component_context
1314            .runtime()
1315            .try_borrow_mut()
1316            .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".to_owned()))?
1317            .reset_component_readers(component);
1318        for value in recipe.script_props.values_mut() {
1319            hydrate_replayed_node_value(value);
1320        }
1321        let reuse = ComponentReusePlan::new(Rc::new(previous_root.clone()), dirty.clone());
1322        self.begin_component_render(
1323            recipe.component_context.clone(),
1324            recipe.generation,
1325            Some(recipe.declared_effects.clone()),
1326            Some(reuse),
1327        )?;
1328        register_component_invocation(&self.component_render, recipe.clone())
1329            .map_err(RuntimeError::Evaluate)?;
1330
1331        self.align_execution_session_to(recipe.context.operation_base());
1332        let started = self.begin_timing();
1333        let result = (|| {
1334            let mut node = recipe
1335                .context
1336                .call::<UiNode>(
1337                    self.engine(),
1338                    &recipe.render,
1339                    (
1340                        recipe.component_context.clone(),
1341                        recipe.script_props.clone(),
1342                    ),
1343                )
1344                .map_err(RuntimeError::Evaluate)?;
1345            register_component_event_callbacks(
1346                &self.component_render,
1347                &recipe.path,
1348                &recipe.caller_context,
1349                recipe.event_callbacks.clone(),
1350                &recipe.context,
1351            )
1352            .map_err(RuntimeError::Evaluate)?;
1353            node = node.with_owned_part_styles(recipe.part_styles.clone());
1354            node =
1355                node.with_component_root_snapshot(recipe.path.clone(), recipe.snapshot.reference());
1356            node.bind_component_scope(
1357                &recipe.path,
1358                recipe.component_context.component_incarnation(),
1359                &recipe.component_events,
1360                Some(&recipe.context),
1361            );
1362            node.bind_generation(recipe.generation);
1363            Ok(node)
1364        })();
1365        if result.is_err() {
1366            record_component_render_failure(&self.component_render, &recipe.path);
1367        }
1368        {
1369            let mut active = self.component_render.try_borrow_mut().map_err(|_| {
1370                RuntimeError::ComponentRuntime(
1371                    "component render stack is already borrowed".to_owned(),
1372                )
1373            })?;
1374            if let Some(active) = active.as_mut() {
1375                let environment = active.environment;
1376                if let Some(retained) = active.invocations.get_mut(&recipe.path) {
1377                    retained.environment = environment;
1378                    retained.reusable = recipe.component_context.component_render_is_reusable();
1379                    if let Ok(node) = &result {
1380                        retained.snapshot.update_if_active(node);
1381                    }
1382                }
1383            }
1384        }
1385        self.record_timing(
1386            ExecutionOperation::Render,
1387            recipe.component.as_str(),
1388            started,
1389            result.is_ok(),
1390        );
1391        self.finish_component_render(result.is_ok())?;
1392        result
1393    }
1394
1395    pub(crate) fn execution_checkpoint(&self) -> RuntimeEngineCheckpoint {
1396        RuntimeEngineCheckpoint {
1397            generation: self.generation,
1398            evaluation_generation: self.evaluation_generation.get(),
1399            component_invocations: self.component_invocations.clone(),
1400            component_effects: self.component_effects.clone(),
1401            component_timers: self.component_timers.clone(),
1402            component_signals: self.component_signals.clone(),
1403            component_element_refs: self.component_element_refs.clone(),
1404            pending_component_commits: self.pending_component_commits.clone(),
1405            virtual_collections: self.virtual_collections.clone(),
1406            component_snapshots: self
1407                .component_invocations
1408                .values()
1409                .map(|recipe| (recipe.snapshot.clone(), recipe.snapshot.current()))
1410                .collect(),
1411        }
1412    }
1413
1414    pub(crate) fn restore_execution_checkpoint(&mut self, checkpoint: RuntimeEngineCheckpoint) {
1415        for (snapshot, value) in &checkpoint.component_snapshots {
1416            snapshot.restore(value.clone());
1417        }
1418        self.generation = checkpoint.generation;
1419        self.evaluation_generation
1420            .set(checkpoint.evaluation_generation);
1421        self.component_invocations = checkpoint.component_invocations;
1422        self.component_effects = checkpoint.component_effects;
1423        self.component_timers = checkpoint.component_timers;
1424        self.component_signals = checkpoint.component_signals;
1425        self.component_element_refs = checkpoint.component_element_refs;
1426        self.pending_component_commits = checkpoint.pending_component_commits;
1427        self.virtual_collections = checkpoint.virtual_collections;
1428    }
1429
1430    pub(crate) fn commit_component_renders(
1431        &mut self,
1432        runtime: &mut UiRuntimeState,
1433    ) -> Result<(), RuntimeError> {
1434        let commits = std::mem::take(&mut self.pending_component_commits);
1435        if commits.is_empty() {
1436            return Ok(());
1437        }
1438        // One batch has one final invocation graph. Intermediate ancestor
1439        // manifests must not retire descendants prepared by later targets.
1440        let mut active = self
1441            .component_invocations
1442            .keys()
1443            .cloned()
1444            .collect::<BTreeSet<_>>();
1445        active.extend(
1446            self.virtual_collections
1447                .keys()
1448                .map(|id| id.component.child("VirtualCollection", id.key.clone())),
1449        );
1450        active.extend(
1451            commits
1452                .values()
1453                .filter_map(|commit| commit.retained_root.clone()),
1454        );
1455        let roots = crate::state::topmost_paths(&commits.keys().cloned().collect());
1456        let previous = commits
1457            .values()
1458            .flat_map(|commit| commit.previous.iter().cloned())
1459            .collect();
1460        let mut transactions = Vec::with_capacity(commits.len());
1461        for (_, commit) in commits {
1462            runtime.replace_component_event_handlers(&commit.root, commit.event_handlers);
1463            transactions.push(commit.transaction);
1464        }
1465        runtime
1466            .component_state
1467            .commit_render_batch(transactions, &active);
1468        for root in roots {
1469            runtime
1470                .reconcile_component_lifetimes(&root, &active, &previous)
1471                .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?;
1472        }
1473        Ok(())
1474    }
1475
1476    pub(crate) fn component_effects_in_scope(
1477        &self,
1478        root: &ComponentInstancePath,
1479    ) -> BTreeMap<crate::EffectId, crate::EffectDescriptor> {
1480        self.component_effects
1481            .iter()
1482            .filter(|(id, _)| id.component().is_within(root))
1483            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1484            .collect()
1485    }
1486
1487    pub(crate) fn component_signals_in_scope(
1488        &self,
1489        root: &ComponentInstancePath,
1490    ) -> BTreeMap<crate::SignalId, crate::signal::SignalDescriptor> {
1491        self.component_signals
1492            .iter()
1493            .filter(|(id, _)| id.component().is_within(root))
1494            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1495            .collect()
1496    }
1497
1498    pub(crate) fn component_timers_in_scope(
1499        &self,
1500        root: &ComponentInstancePath,
1501    ) -> BTreeMap<crate::TimerId, crate::TimerDescriptor> {
1502        self.component_timers
1503            .iter()
1504            .filter(|(id, _)| id.component().is_within(root))
1505            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
1506            .collect()
1507    }
1508
1509    pub(crate) fn component_element_refs_in_scope(
1510        &self,
1511        root: &ComponentInstancePath,
1512    ) -> BTreeSet<crate::ElementRefId> {
1513        self.component_element_refs
1514            .iter()
1515            .filter(|id| id.component().is_within(root))
1516            .cloned()
1517            .collect()
1518    }
1519
1520    pub(crate) fn virtual_collection_ids_in_scope(
1521        &self,
1522        root: &ComponentInstancePath,
1523    ) -> BTreeSet<crate::VirtualCollectionId> {
1524        self.virtual_collections
1525            .keys()
1526            .filter(|id| id.component.is_within(root))
1527            .cloned()
1528            .collect()
1529    }
1530
1531    pub(crate) fn realize_virtual_collection(
1532        &mut self,
1533        id: &crate::VirtualCollectionId,
1534        indices: &BTreeSet<usize>,
1535        previous: &BTreeMap<usize, UiNode>,
1536    ) -> Result<BTreeMap<usize, UiNode>, RuntimeError> {
1537        let recipe = self.virtual_collections.get(id).cloned().ok_or_else(|| {
1538            RuntimeError::ComponentRuntime(format!(
1539                "virtual collection `{}` is not retained in the active generation",
1540                id.key
1541            ))
1542        })?;
1543        if recipe.generation != self.generation || recipe.id != *id {
1544            return Err(RuntimeError::StaleCallback {
1545                name: recipe.renderer.fn_name().to_owned(),
1546                callback_generation: recipe.generation,
1547                current_generation: self.generation,
1548            });
1549        }
1550        let scope = id.component.child("VirtualCollection", id.key.clone());
1551        let context = recipe
1552            .context
1553            .for_structural_scope(scope, BTreeMap::new())
1554            .with_generation(recipe.generation);
1555        let retained = previous
1556            .iter()
1557            .filter(|(index, _)| indices.contains(index))
1558            .map(|(index, node)| (*index, node.clone()))
1559            .collect::<BTreeMap<_, _>>();
1560        let previous_root = Rc::new(UiNode::box_node(retained.values().cloned().collect()));
1561        prune_virtual_read_contributions(&context, id, &recipe.data, indices, &previous_root)?;
1562        self.begin_component_render(
1563            context.clone(),
1564            recipe.generation,
1565            None,
1566            Some(ComponentReusePlan::new(previous_root, BTreeSet::new())),
1567        )?;
1568        retain_virtual_component_manifest(&self.component_render)?;
1569        let result = (|| {
1570            let mut realized = retained;
1571            for index in indices
1572                .iter()
1573                .copied()
1574                .filter(|index| *index < recipe.data.len() && !previous.contains_key(index))
1575            {
1576                let item = recipe
1577                    .data
1578                    .item(index)
1579                    .map_err(|error| RuntimeError::ComponentRuntime(error.to_string()))?
1580                    .ok_or_else(|| {
1581                        RuntimeError::ComponentRuntime(format!(
1582                            "virtual collection item {index} disappeared"
1583                        ))
1584                    })?;
1585                let (key, mut payload) =
1586                    collection_payload(&item, index).map_err(RuntimeError::Evaluate)?;
1587                add_collection_neighbors(&recipe.data, index, &mut payload);
1588                let invocation = &recipe.renderer_context;
1589                self.align_execution_session_to(invocation.operation_base());
1590                let started = self.begin_timing();
1591                let item_context = virtual_item_context(&context, id, &key);
1592                enter_virtual_collection_scope(&self.component_render, item_context.clone())
1593                    .map_err(RuntimeError::Evaluate)?;
1594                let item = invocation.call::<UiNode>(
1595                    self.engine(),
1596                    &recipe.renderer,
1597                    (item_context, Dynamic::from_map(payload)),
1598                );
1599                leave_component_render(&self.component_render).map_err(RuntimeError::Evaluate)?;
1600                self.record_timing(
1601                    ExecutionOperation::VirtualCollection(id.key.clone()),
1602                    id.key.as_str(),
1603                    started,
1604                    item.is_ok(),
1605                );
1606                let mut node = item.map_err(RuntimeError::Evaluate)?.with_key(key);
1607                node.bind_generation(recipe.generation);
1608                node.bind_component_scope(
1609                    recipe.context.callback_component_path(),
1610                    recipe.context.callback_component_incarnation(),
1611                    recipe.context.callback_event_schemas(),
1612                    Some(&recipe.renderer_context),
1613                );
1614                realized.insert(index, node);
1615            }
1616            Ok(realized)
1617        })();
1618        if result.is_err() {
1619            record_component_render_failure(&self.component_render, context.component_path());
1620        }
1621        self.finish_component_render(result.is_ok())?;
1622        result
1623    }
1624
1625    pub(crate) fn update_virtual_collection_snapshot(
1626        &mut self,
1627        id: &crate::VirtualCollectionId,
1628        items: &BTreeMap<usize, UiNode>,
1629    ) {
1630        // Structural collection scopes are not executable component owners.
1631        // Update every containing invocation snapshot so a later parent bailout
1632        // cannot resurrect an older realized window (including nested lists).
1633        let mut path = Some(id.component.clone());
1634        while let Some(current_path) = path {
1635            if let Some(owner) = self.component_invocations.get_mut(&current_path)
1636                && let Some(current) = owner.snapshot.current()
1637                && current.virtual_collection_spec(id).is_some()
1638            {
1639                let mut rendered = current.as_ref().clone();
1640                rendered.replace_virtual_collection_items(id, items.clone());
1641                owner.snapshot.replace_if_active(Rc::new(rendered));
1642            }
1643            path = current_path.parent();
1644        }
1645    }
1646
1647    /// Invoke an optional one-argument lifecycle function.
1648    ///
1649    /// # Errors
1650    ///
1651    /// Returns [`RuntimeError::Evaluate`] when the function exists but fails.
1652    pub fn call_optional_lifecycle(
1653        &self,
1654        compiled: &CompiledUi,
1655        function: &str,
1656        context: UiContext,
1657    ) -> Result<bool, RuntimeError> {
1658        if !compiled.has_function(function, 1) {
1659            return Ok(false);
1660        }
1661        self.evaluation_generation.set(compiled.generation);
1662        self.begin_execution_session();
1663        let started = self.begin_timing();
1664        let result = AstInterpreter::call_fn::<Dynamic, _>(
1665            &self.engine,
1666            &compiled.ast,
1667            &mut Scope::new(),
1668            function,
1669            (context,),
1670        );
1671        self.record_timing(
1672            ExecutionOperation::Lifecycle(function.to_owned()),
1673            compiled.ast.source().unwrap_or("<script>"),
1674            started,
1675            result.is_ok(),
1676        );
1677        let _ = result.map_err(RuntimeError::Evaluate)?;
1678        Ok(true)
1679    }
1680
1681    pub(crate) fn call_optional_lifecycle_with_value(
1682        &self,
1683        compiled: &CompiledUi,
1684        function: &str,
1685        context: UiContext,
1686        value: UiValue,
1687    ) -> Result<bool, RuntimeError> {
1688        if !compiled.has_function(function, 2) {
1689            return Ok(false);
1690        }
1691        self.evaluation_generation.set(compiled.generation);
1692        self.begin_execution_session();
1693        let started = self.begin_timing();
1694        let result = AstInterpreter::call_fn::<Dynamic, _>(
1695            &self.engine,
1696            &compiled.ast,
1697            &mut Scope::new(),
1698            function,
1699            (context, value.into_dynamic()),
1700        );
1701        self.record_timing(
1702            ExecutionOperation::Lifecycle(function.to_owned()),
1703            compiled.ast.source().unwrap_or("<script>"),
1704            started,
1705            result.is_ok(),
1706        );
1707        let _ = result.map_err(RuntimeError::Evaluate)?;
1708        Ok(true)
1709    }
1710
1711    /// Decode optional root `state_schema()` from an application entry.
1712    ///
1713    /// # Errors
1714    ///
1715    /// Returns [`RuntimeError::Evaluate`] when the function fails or cannot be
1716    /// decoded as [`ComponentStateSchema`].
1717    pub fn root_state_schema(
1718        &self,
1719        compiled: &CompiledUi,
1720    ) -> Result<ComponentStateSchema, RuntimeError> {
1721        if !compiled.has_function("state_schema", 0) {
1722            return Ok(ComponentStateSchema::default());
1723        }
1724        self.begin_execution_session();
1725        let started = self.begin_timing();
1726        let result = (|| {
1727            let raw: Dynamic = self
1728                .engine
1729                .call_fn(&mut Scope::new(), &compiled.ast, "state_schema", ())
1730                .map_err(RuntimeError::Evaluate)?;
1731            validate_state_default_encoding(&raw, "state_schema")
1732                .map_err(RuntimeError::Evaluate)?;
1733            rhai::serde::from_dynamic(&raw).map_err(RuntimeError::Evaluate)
1734        })();
1735        self.record_timing(
1736            ExecutionOperation::Lifecycle("state_schema".to_owned()),
1737            compiled.ast.source().unwrap_or("<script>"),
1738            started,
1739            result.is_ok(),
1740        );
1741        result
1742    }
1743
1744    /// Create a generation-bound callback for a script function.
1745    ///
1746    /// # Errors
1747    ///
1748    /// Returns [`RuntimeError::CallbackDefinition`] when the function name is
1749    /// not a valid Rhai function pointer name.
1750    pub fn callback(
1751        &self,
1752        compiled: &CompiledUi,
1753        function_name: &str,
1754    ) -> Result<ScriptCallback, RuntimeError> {
1755        let function =
1756            FnPtr::new(function_name).map_err(|source| RuntimeError::CallbackDefinition {
1757                name: function_name.to_owned(),
1758                source,
1759            })?;
1760        ScriptCallback::try_from_fn_ptr(function, compiled.generation).map_err(|source| {
1761            RuntimeError::RetainedCallback {
1762                name: function_name.to_owned(),
1763                source,
1764            }
1765        })
1766    }
1767
1768    /// Invoke a callback only when it belongs to the current compiled generation.
1769    ///
1770    /// # Errors
1771    ///
1772    /// Returns [`RuntimeError::StaleCallback`] for obsolete callbacks and
1773    /// [`RuntimeError::Evaluate`] when the Rhai function fails.
1774    pub fn invoke_callback(
1775        &self,
1776        compiled: &CompiledUi,
1777        callback: &ScriptCallback,
1778        args: impl FuncArgs,
1779    ) -> Result<Dynamic, RuntimeError> {
1780        if !self.is_current(callback.generation) || callback.generation != compiled.generation {
1781            return Err(RuntimeError::StaleCallback {
1782                name: callback.name().to_owned(),
1783                callback_generation: callback.generation,
1784                current_generation: self.generation,
1785            });
1786        }
1787
1788        self.invoke_callback_for_generation(compiled, callback, args)
1789    }
1790
1791    pub(crate) fn invoke_callback_for_generation(
1792        &self,
1793        compiled: &CompiledUi,
1794        callback: &ScriptCallback,
1795        args: impl FuncArgs,
1796    ) -> Result<Dynamic, RuntimeError> {
1797        if callback.generation != compiled.generation {
1798            return Err(RuntimeError::StaleCallback {
1799                name: callback.name().to_owned(),
1800                callback_generation: callback.generation,
1801                current_generation: compiled.generation,
1802            });
1803        }
1804        self.evaluation_generation.set(compiled.generation);
1805        let operation_base = callback.native_context.as_ref().map_or(
1806            0,
1807            crate::invocation::ScriptInvocationContext::operation_base,
1808        );
1809        self.begin_execution_session_from(operation_base);
1810        let started = self.begin_timing();
1811        #[allow(deprecated)]
1812        let result = if let Some(context) = callback.native_context.as_ref() {
1813            context.call(self.engine(), &callback.function, args)
1814        } else {
1815            callback.function.call(self.engine(), &compiled.ast, args)
1816        };
1817        self.record_timing(
1818            ExecutionOperation::Callback(callback.name().to_owned()),
1819            compiled.ast.source().unwrap_or("<script>"),
1820            started,
1821            result.is_ok(),
1822        );
1823        result.map_err(RuntimeError::Evaluate)
1824    }
1825
1826    #[must_use]
1827    pub fn is_current(&self, generation: ScriptGeneration) -> bool {
1828        generation == self.generation
1829    }
1830
1831    #[must_use]
1832    pub fn engine(&self) -> &Engine {
1833        &self.engine
1834    }
1835
1836    /// Generate the official Rhai language-server definition source for every
1837    /// function/type/module registered on this engine.
1838    ///
1839    /// Standard packages are omitted because the language server supplies them.
1840    #[must_use]
1841    pub fn definition_source(&self) -> String {
1842        // Parameter names, doc comments and readable return types come from the
1843        // script docs; Rhai's own printer is the fallback.
1844        crate::script_docs::ScriptApi::from_engine(&self.engine).map_or_else(
1845            |_| {
1846                self.engine
1847                    .definitions()
1848                    .include_standard_packages(false)
1849                    .single_file()
1850            },
1851            |api| api.definitions(),
1852        )
1853    }
1854
1855    /// Validate statically visible direct and method call arities in an entry
1856    /// script and its installed modules.
1857    ///
1858    /// This complements real lifecycle execution because Rhai resolves
1859    /// functions dynamically and an initial render cannot cover every branch.
1860    /// The catalog comes from the same registered Engine metadata used by
1861    /// [`Self::definition_source`].
1862    ///
1863    /// # Errors
1864    ///
1865    /// Returns a metadata or parse error if the lint catalog cannot be built.
1866    pub fn lint_known_calls(
1867        &mut self,
1868        entry_name: &str,
1869        entry_source: &str,
1870        modules: &BTreeMap<ModuleId, String>,
1871    ) -> Result<Vec<crate::KnownCallDiagnostic>, crate::KnownCallLintError> {
1872        crate::script_lint::lint_known_calls(
1873            &mut self.engine,
1874            entry_name,
1875            entry_source,
1876            modules,
1877            &BTreeSet::new(),
1878        )
1879        .map(|(diagnostics, _)| diagnostics)
1880    }
1881
1882    /// [`Self::lint_known_calls`] for scripts that also call functions the
1883    /// Host registers on its own engine, such as the `[host]` declarations
1884    /// `gpui-rhai check` reads: calls to `host` count as known. Returns the
1885    /// diagnostics and the declared functions the sources call.
1886    ///
1887    /// # Errors
1888    ///
1889    /// Returns a metadata or parse error if the lint catalog cannot be built.
1890    pub fn lint_known_calls_with_host(
1891        &mut self,
1892        entry_name: &str,
1893        entry_source: &str,
1894        modules: &BTreeMap<ModuleId, String>,
1895        host: &BTreeSet<crate::HostFunction>,
1896    ) -> Result<
1897        (
1898            Vec<crate::KnownCallDiagnostic>,
1899            BTreeSet<crate::HostFunction>,
1900        ),
1901        crate::KnownCallLintError,
1902    > {
1903        crate::script_lint::lint_known_calls(
1904            &mut self.engine,
1905            entry_name,
1906            entry_source,
1907            modules,
1908            host,
1909        )
1910    }
1911
1912    /// List entry-script functions that replace a native function of the same
1913    /// name and arity. Rhai prefers the script function for every call,
1914    /// including method calls inside imported components, so a helper named
1915    /// like a built-in breaks code that never mentions it.
1916    ///
1917    /// # Errors
1918    ///
1919    /// Returns a metadata or parse error if the lint catalog cannot be built.
1920    pub fn lint_shadowed_builtins(
1921        &mut self,
1922        entry_name: &str,
1923        entry_source: &str,
1924    ) -> Result<Vec<crate::ShadowedBuiltin>, crate::KnownCallLintError> {
1925        crate::script_lint::lint_shadowed_builtins(&mut self.engine, entry_name, entry_source)
1926    }
1927
1928    /// Return a snapshot of components exported through this engine.
1929    ///
1930    /// # Errors
1931    ///
1932    /// Returns [`ComponentExportError`] if the export registry was poisoned by
1933    /// a prior panic.
1934    pub fn component_exports(&self) -> Result<ComponentRegistry, ComponentExportError> {
1935        self.component_exports.snapshot()
1936    }
1937
1938    pub fn component_invocations(
1939        &self,
1940    ) -> impl ExactSizeIterator<Item = &ComponentInvocationRecipe> {
1941        self.component_invocations.values()
1942    }
1943
1944    pub(crate) fn component_renderer_snapshot(
1945        &self,
1946    ) -> Result<BTreeMap<ModuleId, RegisteredComponentRender>, ComponentExportError> {
1947        self.component_renderers
1948            .try_borrow()
1949            .map(|renderers| renderers.clone())
1950            .map_err(|_| ComponentExportError::Borrowed)
1951    }
1952
1953    pub(crate) fn restore_component_renderers(
1954        &self,
1955        renderers: BTreeMap<ModuleId, RegisteredComponentRender>,
1956    ) -> Result<(), ComponentExportError> {
1957        *self
1958            .component_renderers
1959            .try_borrow_mut()
1960            .map_err(|_| ComponentExportError::Borrowed)? = renderers;
1961        Ok(())
1962    }
1963
1964    /// Clear component exports before evaluating a hot-reload candidate.
1965    ///
1966    /// # Errors
1967    ///
1968    /// Returns [`ComponentExportError`] if the collector is unavailable.
1969    pub fn clear_component_exports(&self) -> Result<(), ComponentExportError> {
1970        self.component_renderers
1971            .try_borrow_mut()
1972            .map_err(|_| ComponentExportError::Borrowed)?
1973            .clear();
1974        self.component_exports.clear()
1975    }
1976
1977    /// Restore a prior export snapshot after a rejected candidate.
1978    ///
1979    /// # Errors
1980    ///
1981    /// Returns [`ComponentExportError`] if the collector is unavailable.
1982    pub fn restore_component_exports(
1983        &self,
1984        registry: ComponentRegistry,
1985    ) -> Result<(), ComponentExportError> {
1986        self.component_exports.replace(registry)
1987    }
1988
1989    /// Register a namespaced Rust custom primitive and its Rhai constructor.
1990    ///
1991    /// # Errors
1992    ///
1993    /// Returns [`PrimitiveError`] when the descriptor conflicts or is invalid.
1994    pub fn register_primitive(
1995        &mut self,
1996        descriptor: PrimitiveDescriptor,
1997        handler: impl PrimitiveHandler + 'static,
1998    ) -> Result<(), PrimitiveError> {
1999        self.primitives.register(descriptor.clone(), handler)?;
2000
2001        let namespace = descriptor.id.namespace().to_owned();
2002        let export = descriptor.export.clone();
2003        let id = descriptor.id;
2004        let primitives = self.primitives.clone();
2005        let generation = Rc::clone(&self.evaluation_generation);
2006        let module = self.primitive_modules.entry(namespace.clone()).or_default();
2007        FuncRegistration::new(export).set_into_module(
2008            module,
2009            move |mut props: Map| -> Result<UiNode, Box<EvalAltResult>> {
2010                let key = props
2011                    .remove("key")
2012                    .map(|value| {
2013                        if value.is::<ImmutableString>() {
2014                            Ok(value.cast::<ImmutableString>().to_string())
2015                        } else {
2016                            Err(Box::new(EvalAltResult::ErrorRuntime(
2017                                "primitive key must be a string".into(),
2018                                Position::NONE,
2019                            )))
2020                        }
2021                    })
2022                    .transpose()?;
2023                primitives
2024                    .create_node(&id, key, &props, generation.get())
2025                    .map_err(|error| {
2026                        Box::new(EvalAltResult::ErrorRuntime(
2027                            error.to_string().into(),
2028                            Position::NONE,
2029                        ))
2030                    })
2031            },
2032        );
2033        self.engine
2034            .register_static_module(namespace, module.clone().into());
2035        Ok(())
2036    }
2037
2038    #[must_use]
2039    pub fn primitive_registry(&self) -> PrimitiveRegistry {
2040        self.primitives.clone()
2041    }
2042
2043    /// Register a trusted Rust handler attachable by Rhai through `native_handler`.
2044    ///
2045    /// # Errors
2046    ///
2047    /// Returns descriptor/duplicate registry errors.
2048    pub fn register_native_handler(
2049        &self,
2050        descriptor: crate::NativeHandlerDescriptor,
2051        handler: impl FnMut(
2052            crate::NativeEvent,
2053            &mut UiRuntimeState,
2054            &mut gpui::Window,
2055            &mut gpui::App,
2056        ) -> Result<crate::EventResponse, String>
2057        + 'static,
2058    ) -> Result<(), crate::NativeHandlerError> {
2059        self.native_handlers.register(descriptor, handler)
2060    }
2061
2062    #[must_use]
2063    pub fn native_handler_registry(&self) -> crate::NativeHandlerRegistry {
2064        self.native_handlers.clone()
2065    }
2066
2067    #[must_use]
2068    pub fn syntax_registry(&self) -> crate::SyntaxRegistry {
2069        self.syntax_registry.clone()
2070    }
2071
2072    #[must_use]
2073    pub fn document_runtime_config(&self) -> crate::DocumentRuntimeConfig {
2074        self.document_runtime.clone()
2075    }
2076
2077    /// Register one trusted Rust chart transform.
2078    ///
2079    /// # Errors
2080    ///
2081    /// Returns an invalid or duplicate transform ID diagnostic.
2082    #[cfg(feature = "charts")]
2083    pub fn register_chart_transform(
2084        &self,
2085        id: impl Into<String>,
2086        transform: impl crate::HostChartTransform + 'static,
2087    ) -> Result<(), crate::ChartTransformError> {
2088        self.chart_transforms.register(id, transform)
2089    }
2090
2091    /// Register Host-owned GeoJSON/SVG geometry or typed map features.
2092    ///
2093    /// # Errors
2094    ///
2095    /// Returns an invalid or duplicate map diagnostic.
2096    #[cfg(feature = "charts")]
2097    pub fn register_chart_map(&self, map: crate::ChartGeoMap) -> Result<(), crate::ChartGeoError> {
2098        self.chart_geo.register_map(map)
2099    }
2100
2101    /// Register one trusted Rust geo projection.
2102    ///
2103    /// # Errors
2104    ///
2105    /// Returns an invalid or duplicate projection ID diagnostic.
2106    #[cfg(feature = "charts")]
2107    pub fn register_chart_projection(
2108        &self,
2109        id: impl Into<String>,
2110        projection: impl crate::ChartGeoProjection + 'static,
2111    ) -> Result<(), crate::ChartGeoError> {
2112        self.chart_geo.register_projection(id, projection)
2113    }
2114
2115    /// Register one trusted Rust custom chart series renderer.
2116    ///
2117    /// # Errors
2118    ///
2119    /// Returns an invalid or duplicate renderer ID diagnostic.
2120    #[cfg(feature = "charts")]
2121    pub fn register_chart_series(
2122        &self,
2123        id: impl Into<String>,
2124        renderer: impl crate::HostChartSeries + 'static,
2125    ) -> Result<(), crate::ChartSeriesExtensionError> {
2126        self.chart_series.register(id, renderer)
2127    }
2128
2129    /// Register one trusted Rust chart value formatter.
2130    ///
2131    /// # Errors
2132    ///
2133    /// Returns an invalid or duplicate formatter ID diagnostic.
2134    #[cfg(feature = "charts")]
2135    pub fn register_chart_formatter(
2136        &self,
2137        id: impl Into<String>,
2138        formatter: impl crate::HostChartFormatter + 'static,
2139    ) -> Result<(), crate::ChartFormatterError> {
2140        self.chart_formatters.register(id, formatter)
2141    }
2142
2143    #[cfg(feature = "charts")]
2144    #[must_use]
2145    pub fn chart_transform_registry(&self) -> crate::ChartTransformRegistry {
2146        self.chart_transforms.clone()
2147    }
2148
2149    #[cfg(feature = "charts")]
2150    #[must_use]
2151    pub fn chart_geo_registry(&self) -> crate::ChartGeoRegistry {
2152        self.chart_geo.clone()
2153    }
2154
2155    #[cfg(feature = "charts")]
2156    #[must_use]
2157    pub fn chart_series_registry(&self) -> crate::ChartSeriesRegistry {
2158        self.chart_series.clone()
2159    }
2160
2161    #[cfg(feature = "charts")]
2162    #[must_use]
2163    pub fn chart_formatter_registry(&self) -> crate::ChartFormatterRegistry {
2164        self.chart_formatters.clone()
2165    }
2166
2167    pub fn set_slow_threshold(&mut self, threshold: Duration) {
2168        self.slow_threshold = threshold;
2169    }
2170
2171    /// Set the per-execution script operation budget.
2172    ///
2173    /// The default is [`DEFAULT_SCRIPT_OPERATION_LIMIT`]. This is a cumulative
2174    /// operation quota for each execution round, not a frame-time guarantee.
2175    /// A host that runs known one-shot, cacheable
2176    /// script phases (a layout pass, an analysis sweep) may raise it; the
2177    /// wall-clock slow threshold still flags anything that stalls the
2178    /// foreground. Scripts cannot change this limit; only the embedding
2179    /// application can.
2180    /// Zero normalizes to one; it never disables protection. Changes apply to
2181    /// subsequent calls. Native Rust work is not preempted by this quota.
2182    pub fn set_operation_limit(&mut self, limit: u64) {
2183        self.operation_limit.set(limit.max(1));
2184    }
2185
2186    /// The per-execution script operation budget in force.
2187    #[must_use]
2188    pub fn operation_limit(&self) -> u64 {
2189        self.operation_limit.get()
2190    }
2191
2192    /// Configure parser depth independently of execution/recursion/data limits.
2193    /// Defaults are global=64/function=32 in both debug and release. Zero
2194    /// normalizes to one rather than adopting Rhai's unlimited convention.
2195    pub fn set_expression_depth_limits(&mut self, global: usize, functions: usize) {
2196        self.engine
2197            .set_max_expr_depths(global.max(1), functions.max(1));
2198    }
2199
2200    #[must_use]
2201    pub fn expression_depth_limits(&self) -> (usize, usize) {
2202        (
2203            self.engine.max_expr_depth(),
2204            self.engine.max_function_expr_depth(),
2205        )
2206    }
2207
2208    #[must_use]
2209    pub fn take_timings(&self) -> Vec<ExecutionTiming> {
2210        std::mem::take(&mut *self.timings.borrow_mut())
2211    }
2212
2213    /// Return the failed timing from the current execution session, if any.
2214    /// Starting the next compile, render, callback, or lifecycle call clears it.
2215    #[must_use]
2216    pub fn last_failed_timing(&self) -> Option<ExecutionTiming> {
2217        self.last_failed_timing.borrow().clone()
2218    }
2219
2220    #[must_use]
2221    pub(crate) fn last_failed_component(&self) -> Option<ComponentInstancePath> {
2222        self.last_failed_component.borrow().clone()
2223    }
2224
2225    fn record_timing(
2226        &self,
2227        operation: ExecutionOperation,
2228        source: &str,
2229        started: ExecutionTimingStart,
2230        succeeded: bool,
2231    ) {
2232        let duration = started.instant.elapsed();
2233        let operations = self.operation_total().saturating_sub(started.operations);
2234        let timing = ExecutionTiming {
2235            operation,
2236            source: source.to_owned(),
2237            duration,
2238            operations,
2239            operation_limit: started.operation_limit,
2240            round_operations: self.operation_total(),
2241            operation_semantics: OPERATION_SEMANTICS_VERSION,
2242            slow: duration >= self.slow_threshold,
2243            succeeded,
2244        };
2245        if !succeeded {
2246            *self.last_failed_timing.borrow_mut() = Some(timing.clone());
2247        }
2248        self.timings.borrow_mut().push(timing);
2249    }
2250
2251    pub(crate) fn begin_execution_session(&self) {
2252        self.begin_execution_session_from(0);
2253    }
2254
2255    fn begin_execution_session_from(&self, inherited_base: u64) {
2256        self.last_failed_timing.borrow_mut().take();
2257        self.last_failed_component.borrow_mut().take();
2258        self.operation_tracker.begin(inherited_base);
2259    }
2260
2261    fn align_execution_session_to(&self, inherited_base: u64) {
2262        self.operation_tracker.align(inherited_base);
2263    }
2264
2265    fn operation_total(&self) -> u64 {
2266        self.operation_tracker.total.get()
2267    }
2268
2269    fn begin_timing(&self) -> ExecutionTimingStart {
2270        ExecutionTimingStart {
2271            instant: Instant::now(),
2272            operations: self.operation_total(),
2273            operation_limit: self.operation_limit(),
2274        }
2275    }
2276
2277    fn candidate_generation(&mut self) -> ScriptGeneration {
2278        self.preparation_generation
2279            .unwrap_or_else(Self::allocate_generation)
2280    }
2281
2282    fn allocate_generation() -> ScriptGeneration {
2283        static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
2284        ScriptGeneration(NEXT_GENERATION.fetch_add(1, Ordering::Relaxed))
2285    }
2286}
2287
2288pub(crate) fn validate_assignment_targets(ast: &AST) -> Result<(), RuntimeError> {
2289    let mut invalid = None;
2290    ast.walk(&mut |path| {
2291        let Some(ASTNode::Stmt(Stmt::Assignment(assignment))) = path.last() else {
2292            return true;
2293        };
2294        if !matches!(
2295            assignment.1.lhs,
2296            Expr::ThisPtr(_) | Expr::Variable(..) | Expr::Index(..) | Expr::Dot(..)
2297        ) {
2298            invalid = Some(assignment.1.lhs.position());
2299            return false;
2300        }
2301        true
2302    });
2303    invalid.map_or(Ok(()), |position| {
2304        Err(RuntimeError::InvalidAssignmentTarget(position))
2305    })
2306}
2307
2308fn validate_literal_imports(ast: &AST) -> Result<(), RuntimeError> {
2309    let mut dynamic = None;
2310    ast.walk(&mut |path| {
2311        let Some(ASTNode::Stmt(Stmt::Import(import, position))) = path.last() else {
2312            return true;
2313        };
2314        if matches!(import.0, Expr::StringConstant(..)) {
2315            true
2316        } else {
2317            dynamic = Some(*position);
2318            false
2319        }
2320    });
2321    dynamic.map_or(Ok(()), |position| {
2322        Err(RuntimeError::Import(format!(
2323            "Rhai imports must use a literal module string at {position}"
2324        )))
2325    })
2326}
2327
2328fn register_node_apis(engine: &mut Engine) {
2329    // A caller-written shortcut as `#{ label, keystrokes, chords }`, formatted
2330    // like a bound action's (`cmd-p` is `⌘P` on macOS); a legend stays as is.
2331    FuncRegistration::new("key_shortcut")
2332        .in_global_namespace()
2333        .register_into_engine(engine, |text: ImmutableString| {
2334            crate::ActionShortcut::from_text(text.as_str())
2335                .to_ui_value()
2336                .into_dynamic()
2337        });
2338    FuncRegistration::new("text")
2339        .in_global_namespace()
2340        .register_into_engine(engine, text_node);
2341    FuncRegistration::new("text")
2342        .in_global_namespace()
2343        .register_into_engine(engine, rich_text_node);
2344    FuncRegistration::new("span")
2345        .in_global_namespace()
2346        .register_into_engine(engine, span_value);
2347    FuncRegistration::new("canvas")
2348        .in_global_namespace()
2349        .register_into_engine(engine, canvas_node);
2350    FuncRegistration::new("svg")
2351        .in_global_namespace()
2352        .register_into_engine(engine, svg_node);
2353    FuncRegistration::new("box")
2354        .in_global_namespace()
2355        .register_into_engine(engine, box_node);
2356    FuncRegistration::new("fragment")
2357        .in_global_namespace()
2358        .register_into_engine(engine, fragment_node);
2359    FuncRegistration::new("motion_group")
2360        .in_global_namespace()
2361        .register_into_engine(engine, motion_group_node);
2362    FuncRegistration::new("stack")
2363        .in_global_namespace()
2364        .register_into_engine(engine, stack_node);
2365    FuncRegistration::new("handled")
2366        .in_global_namespace()
2367        .register_into_engine(engine, || crate::EventResponse::new().stop());
2368    FuncRegistration::new("propagate")
2369        .in_global_namespace()
2370        .register_into_engine(engine, crate::EventResponse::new);
2371    FuncRegistration::new("event_response")
2372        .in_global_namespace()
2373        .register_into_engine(engine, crate::EventResponse::new);
2374    FuncRegistration::new("column")
2375        .in_global_namespace()
2376        .register_into_engine(engine, column_node);
2377    FuncRegistration::new("row")
2378        .in_global_namespace()
2379        .register_into_engine(engine, row_node);
2380    FuncRegistration::new("error_boundary")
2381        .in_global_namespace()
2382        .register_into_engine(engine, error_boundary_node);
2383    FuncRegistration::new("error_boundary_lazy")
2384        .in_global_namespace()
2385        .register_into_engine(engine, lazy_error_boundary_node);
2386    FuncRegistration::new("asset")
2387        .in_global_namespace()
2388        .register_into_engine(engine, asset_id_from_script);
2389    FuncRegistration::new("image")
2390        .in_global_namespace()
2391        .register_into_engine(engine, image_node);
2392    FuncRegistration::new("image")
2393        .in_global_namespace()
2394        .register_into_engine(engine, asset_image_node);
2395    FuncRegistration::new("directional_image")
2396        .in_global_namespace()
2397        .register_into_engine(engine, directional_image_node);
2398    FuncRegistration::new("directional_image")
2399        .in_global_namespace()
2400        .register_into_engine(engine, directional_asset_image_node);
2401    FuncRegistration::new("image_source")
2402        .in_global_namespace()
2403        .register_into_engine(engine, generic_image_node);
2404    FuncRegistration::new("directional_image_source")
2405        .in_global_namespace()
2406        .register_into_engine(engine, generic_directional_image_node);
2407    FuncRegistration::new("overlay")
2408        .in_global_namespace()
2409        .register_into_engine(engine, overlay_node);
2410    FuncRegistration::new("layer")
2411        .in_global_namespace()
2412        .register_into_engine(engine, layer_node);
2413}
2414
2415fn register_native_handler_api(engine: &mut Engine, registry: &crate::NativeHandlerRegistry) {
2416    let registry = registry.clone();
2417    FuncRegistration::new("native_handler")
2418        .in_global_namespace()
2419        .register_into_engine(
2420            engine,
2421            move |id: ImmutableString| -> Result<crate::NativeHandlerRef, Box<EvalAltResult>> {
2422                let id = crate::NativeHandlerId::parse(id.to_string())
2423                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2424                registry
2425                    .resolve(&id)
2426                    .map_err(|error| Box::new(component_render_error(error.to_string())))
2427            },
2428        );
2429}
2430
2431pub(crate) const RHAI_MAX_ARRAY_SIZE: usize = 10_000;
2432pub(crate) const RHAI_MAX_MAP_SIZE: usize = 100_000;
2433pub(crate) const RHAI_MAX_STRING_SIZE: usize = 1_048_576;
2434pub(crate) const RHAI_MAX_DATA_DEPTH: usize = 64;
2435
2436fn configure_engine_limits(engine: &mut Engine) {
2437    engine.set_max_call_levels(64);
2438    // Explicit defaults avoid Rhai's profile-dependent parser defaults. A
2439    // trusted Host may configure these separately; ordinary applications do
2440    // not inherit another application's deeper expression policy.
2441    engine.set_max_expr_depths(64, 32);
2442    // Rhai's counter is evaluator-local and cloned into stored callback
2443    // contexts. The runtime's progress adapter enforces one cumulative budget
2444    // across nested evaluators and starts delayed callbacks with fresh quota.
2445    engine.set_max_operations(0);
2446    engine.set_max_array_size(RHAI_MAX_ARRAY_SIZE);
2447    engine.set_max_map_size(RHAI_MAX_MAP_SIZE);
2448    engine.set_max_string_size(RHAI_MAX_STRING_SIZE);
2449}
2450
2451fn register_define_component_api(
2452    engine: &mut Engine,
2453    exports: &ComponentExportCollector,
2454    renderers: &ComponentRenderRegistry,
2455    generation: &Rc<Cell<ScriptGeneration>>,
2456) {
2457    let exports = exports.clone();
2458    let renderers = Rc::clone(renderers);
2459    let generation = Rc::clone(generation);
2460    FuncRegistration::new("define_component")
2461        .in_global_namespace()
2462        .register_into_engine(
2463            engine,
2464            move |mut raw: Map| -> Result<(), Box<EvalAltResult>> {
2465                let render = raw.remove("render").ok_or_else(|| {
2466                    Box::new(component_render_error(
2467                        "define_component requires a named `render` function",
2468                    ))
2469                })?;
2470                if !render.is::<FnPtr>() {
2471                    return Err(Box::new(component_render_error(
2472                        "define_component `render` must be a function pointer",
2473                    )));
2474                }
2475                let render = render.cast::<FnPtr>();
2476                if render.is_anonymous() || render.is_curried() {
2477                    return Err(Box::new(component_render_error(
2478                        "formal component render must be an uncurried named function",
2479                    )));
2480                }
2481                if let Some(schema) = raw.get("schema")
2482                    && schema.is::<Map>()
2483                    && let Some(state) = schema.clone_cast::<Map>().get("state")
2484                {
2485                    validate_state_default_encoding(state, "component.schema.state")?;
2486                }
2487                let decoded: crate::ComponentDefinition =
2488                    rhai::serde::from_dynamic(&Dynamic::from_map(raw))?;
2489                let definition = crate::ComponentDefinition::new(decoded.metadata, decoded.schema)
2490                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2491                let id = definition.metadata.id.clone();
2492                exports
2493                    .register_definition(definition)
2494                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2495                renderers
2496                    .try_borrow_mut()
2497                    .map_err(|_| {
2498                        Box::new(component_render_error(
2499                            "component render registry is already borrowed",
2500                        ))
2501                    })?
2502                    .insert(
2503                        id,
2504                        RegisteredComponentRender {
2505                            generation: generation.get(),
2506                            render,
2507                        },
2508                    );
2509                Ok(())
2510            },
2511        );
2512}
2513
2514fn validate_state_default_encoding(raw: &Dynamic, path: &str) -> Result<(), Box<EvalAltResult>> {
2515    if !raw.is::<Map>() {
2516        return Ok(());
2517    }
2518    let raw = raw.clone_cast::<Map>();
2519    let Some(fields) = raw.get("fields").filter(|fields| fields.is::<Map>()) else {
2520        return Ok(());
2521    };
2522    for (name, field) in fields.clone_cast::<Map>() {
2523        if !field.is::<Map>() {
2524            continue;
2525        }
2526        let field = field.cast::<Map>();
2527        if let Some(value) = field.get("default") {
2528            rhai::serde::from_dynamic::<UiValue>(value).map_err(|source| {
2529                Box::new(component_render_error(format!(
2530                    "{path}.fields.{name}.default: expected recursively tagged UiValue; map entries and array items each require type/value descriptors ({source})"
2531                )))
2532            })?;
2533        }
2534    }
2535    Ok(())
2536}
2537
2538fn register_component_runtime_apis(
2539    engine: &mut Engine,
2540    exports: &ComponentExportCollector,
2541    generation: &Rc<Cell<ScriptGeneration>>,
2542) -> (ActiveComponentRenderState, ComponentRenderRegistry) {
2543    let active = Rc::new(RefCell::new(None));
2544    let renderers = Rc::new(RefCell::new(BTreeMap::new()));
2545    register_define_component_api(engine, exports, &renderers, generation);
2546    register_render_component_api(engine, exports, &renderers, &active);
2547    register_effect_api(engine, &active);
2548    register_timer_api(engine, &active);
2549    register_signal_api(engine, &active);
2550    register_element_ref_api(engine, &active);
2551    register_virtual_collection_api(engine, &active);
2552    (active, renderers)
2553}
2554
2555fn register_render_component_api(
2556    engine: &mut Engine,
2557    exports: &ComponentExportCollector,
2558    renderers: &ComponentRenderRegistry,
2559    active: &ActiveComponentRenderState,
2560) {
2561    let exports = exports.clone();
2562    let renderers = Rc::clone(renderers);
2563    let active = Rc::clone(active);
2564    FuncRegistration::new("render_component")
2565        .in_global_namespace()
2566        .register_into_engine(
2567            engine,
2568            move |call: rhai::NativeCallContext<'_>,
2569                  id: ImmutableString,
2570                  props: Map|
2571                  -> Result<UiNode, Box<EvalAltResult>> {
2572                let id = ModuleId::parse(id.to_string())
2573                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2574                let generation = active
2575                    .try_borrow()
2576                    .map_err(|_| {
2577                        Box::new(component_render_error(
2578                            "component render stack is already borrowed",
2579                        ))
2580                    })?
2581                    .as_ref()
2582                    .ok_or_else(|| {
2583                        Box::new(component_render_error(
2584                            "render_component may run only inside view",
2585                        ))
2586                    })?
2587                    .generation;
2588                let registered = renderers
2589                    .try_borrow()
2590                    .map_err(|_| {
2591                        Box::new(component_render_error(
2592                            "component render registry is already borrowed",
2593                        ))
2594                    })?
2595                    .get(&id)
2596                    .cloned()
2597                    .ok_or_else(|| {
2598                        Box::new(component_render_error(format!(
2599                            "component `{id}` has no registered render function"
2600                        )))
2601                    })?;
2602                if registered.generation != generation {
2603                    return Err(Box::new(component_render_error(format!(
2604                        "component `{id}` render belongs to stale generation {}",
2605                        registered.generation
2606                    ))));
2607                }
2608                execute_component_render(
2609                    &call,
2610                    &exports,
2611                    &active,
2612                    id.as_str().into(),
2613                    props,
2614                    &registered.render,
2615                )
2616            },
2617        );
2618}
2619
2620#[allow(clippy::too_many_lines)] // One cohesive Rhai component transaction; splitting hides ordering.
2621fn execute_component_render(
2622    call: &rhai::NativeCallContext<'_>,
2623    exports: &ComponentExportCollector,
2624    active: &ActiveComponentRenderState,
2625    id: ImmutableString,
2626    props: Map,
2627    render: &FnPtr,
2628) -> Result<UiNode, Box<EvalAltResult>> {
2629    let generation = active
2630        .try_borrow()
2631        .map_err(|_| {
2632            Box::new(component_render_error(
2633                "component render stack is already borrowed",
2634            ))
2635        })?
2636        .as_ref()
2637        .ok_or_else(|| {
2638            Box::new(component_render_error(
2639                "formal component rendering may run only inside view",
2640            ))
2641        })?
2642        .generation;
2643    let (component, mut invocation) = resolve_component_invocation(exports, id, props, generation)?;
2644    if render.is_anonymous() || render.is_curried() {
2645        return Err(Box::new(component_render_error(
2646            "formal component render must be an uncurried named function",
2647        )));
2648    }
2649    let part_styles = component_part_styles(&invocation.props);
2650    let recipe_props = invocation.retained_props.clone();
2651    let declared_key = invocation.key.clone();
2652    let render_recipe = render.clone();
2653    let (path, caller_context) = reserve_component_path(call, &component, &invocation, active)?;
2654    if let Some(node) = try_reuse_component_subtree(
2655        active,
2656        &path,
2657        &caller_context,
2658        &component,
2659        &invocation,
2660        render,
2661    )? {
2662        return Ok(node);
2663    }
2664    let native_context = crate::invocation::ScriptInvocationContext::capture_retained(call);
2665    let caller_binding = caller_component_binding(call, &caller_context);
2666    bind_component_callback_props(
2667        &mut invocation.props,
2668        &component.schema.props,
2669        &caller_binding,
2670    );
2671    bind_component_node_props(
2672        &mut invocation.props,
2673        &component.schema.props,
2674        &caller_binding,
2675    );
2676    let event_callbacks = component_event_callbacks(&component, &invocation.props);
2677    let recipe_event_callbacks = event_callbacks.clone();
2678    let script_props = invocation.props.clone();
2679    let context = enter_component_render(
2680        &path,
2681        &component,
2682        &invocation,
2683        native_context.clone(),
2684        active,
2685    )?;
2686    let recipe_component_context = context.clone();
2687    let result = render.call_within_context::<UiNode>(call, (context, invocation.props));
2688    if result.is_err() {
2689        record_component_render_failure(active, &path);
2690    }
2691    register_component_event_callbacks(
2692        active,
2693        &path,
2694        &caller_context,
2695        event_callbacks,
2696        &native_context,
2697    )?;
2698    let (environment, reusable) = component_render_metadata(active, &recipe_component_context)?;
2699    leave_component_render(active)?;
2700    let mut node = result?;
2701    node = node.with_owned_part_styles(part_styles.clone());
2702    let snapshot = crate::node::ComponentOwnedSnapshot::default();
2703    node = node.with_component_root_snapshot(path.clone(), snapshot.reference());
2704    node.bind_component_scope(
2705        &path,
2706        recipe_component_context.component_incarnation(),
2707        &component.schema.events,
2708        Some(&native_context),
2709    );
2710    register_component_invocation(
2711        active,
2712        ComponentInvocationRecipe {
2713            path: path.clone(),
2714            parent: caller_context.component_path().clone(),
2715            component: component.metadata.id.clone(),
2716            export: component.metadata.export.clone(),
2717            declared_key,
2718            props: recipe_props,
2719            script_props,
2720            component_context: recipe_component_context,
2721            caller_context: caller_context.clone(),
2722            part_styles,
2723            event_callbacks: recipe_event_callbacks,
2724            component_events: component.schema.events.clone(),
2725            declared_effects: component.schema.effects.clone(),
2726            render: render_recipe,
2727            snapshot,
2728            context: native_context.clone(),
2729            generation,
2730            environment,
2731            reusable,
2732        },
2733    )?;
2734    Ok(node)
2735}
2736
2737fn caller_component_binding(
2738    call: &rhai::NativeCallContext<'_>,
2739    caller_context: &UiContext,
2740) -> ComponentCallbackBinding {
2741    ComponentCallbackBinding {
2742        component: caller_context.callback_component_path().clone(),
2743        incarnation: caller_context.callback_component_incarnation(),
2744        events: caller_context.callback_event_schemas().clone(),
2745        context: Some(caller_context.native_context().cloned().unwrap_or_else(|| {
2746            crate::invocation::ScriptInvocationContext::capture_entry_retained(call)
2747        })),
2748    }
2749}
2750
2751fn component_render_metadata(
2752    active: &ActiveComponentRenderState,
2753    context: &UiContext,
2754) -> Result<(ComponentRenderEnvironment, bool), Box<EvalAltResult>> {
2755    let guard = active.try_borrow().map_err(|_| {
2756        Box::new(component_render_error(
2757            "component render stack is already borrowed",
2758        ))
2759    })?;
2760    let render = guard.as_ref().ok_or_else(|| {
2761        Box::new(component_render_error(
2762            "formal component rendering may run only inside view",
2763        ))
2764    })?;
2765    Ok((render.environment, context.component_render_is_reusable()))
2766}
2767
2768fn record_component_render_failure(
2769    active: &ActiveComponentRenderState,
2770    component: &ComponentInstancePath,
2771) {
2772    if let Ok(mut active) = active.try_borrow_mut()
2773        && let Some(active) = active.as_mut()
2774        && active.failed_component.is_none()
2775    {
2776        active.failed_component = Some(component.clone());
2777    }
2778}
2779
2780fn try_reuse_component_subtree(
2781    shared: &ActiveComponentRenderState,
2782    path: &ComponentInstancePath,
2783    caller_context: &UiContext,
2784    component: &crate::ComponentDefinition,
2785    invocation: &crate::ComponentInvocation,
2786    render: &FnPtr,
2787) -> Result<Option<UiNode>, Box<EvalAltResult>> {
2788    let mut guard = shared.try_borrow_mut().map_err(|_| {
2789        Box::new(component_render_error(
2790            "component render stack is already borrowed",
2791        ))
2792    })?;
2793    let active = guard.as_mut().ok_or_else(|| {
2794        Box::new(component_render_error(
2795            "formal component rendering may run only inside view",
2796        ))
2797    })?;
2798    let Some(scope) =
2799        reusable_component_scope(active, path, caller_context, component, invocation, render)
2800    else {
2801        return Ok(None);
2802    };
2803
2804    let reused_components = scope.invocations.len();
2805    for candidate in scope.invocations.keys() {
2806        let retained = active.transaction.retain_existing(candidate);
2807        debug_assert!(
2808            retained,
2809            "reuse candidates were checked against the state snapshot"
2810        );
2811    }
2812    active.seen.extend(scope.invocations.keys().cloned());
2813    active.invocations.extend(scope.invocations);
2814    active.event_handlers.extend(scope.event_handlers);
2815    active.effects.extend(scope.resources.effects);
2816    active.timers.extend(scope.resources.timers);
2817    active.signals.extend(scope.resources.signals);
2818    active.element_refs.extend(scope.resources.element_refs);
2819    active
2820        .virtual_collections
2821        .extend(scope.resources.virtual_collections);
2822    active
2823        .reused
2824        .push((component.metadata.id.to_string(), reused_components));
2825    Ok(Some(scope.node))
2826}
2827
2828fn prune_virtual_read_contributions(
2829    context: &UiContext,
2830    id: &crate::VirtualCollectionId,
2831    data: &crate::VirtualCollectionData,
2832    indices: &BTreeSet<usize>,
2833    retained: &UiNode,
2834) -> Result<(), RuntimeError> {
2835    let mut active = retained.virtual_read_contributions();
2836    active.extend(
2837        indices
2838            .iter()
2839            .filter_map(|index| data.key(*index))
2840            .map(
2841                |key| crate::read_dependency::ReadContribution::VirtualItem {
2842                    collection: id.clone(),
2843                    key: key.to_owned(),
2844                },
2845            ),
2846    );
2847    context
2848        .runtime()
2849        .try_borrow_mut()
2850        .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".into()))?
2851        .retain_virtual_read_contributions(context.component_path(), &active);
2852    Ok(())
2853}
2854
2855fn retain_virtual_component_manifest(
2856    shared: &ActiveComponentRenderState,
2857) -> Result<(), RuntimeError> {
2858    let mut guard = shared.try_borrow_mut().map_err(|_| {
2859        RuntimeError::ComponentRuntime("component render stack is already borrowed".to_owned())
2860    })?;
2861    let active = guard.as_mut().ok_or_else(|| {
2862        RuntimeError::ComponentRuntime("virtual render has no active transaction".to_owned())
2863    })?;
2864    let Some(reuse) = active.reuse.as_ref() else {
2865        return Ok(());
2866    };
2867    let mut retained = reuse
2868        .invocations
2869        .keys()
2870        .filter(|path| reuse.subtrees.contains(path))
2871        .cloned()
2872        .collect::<BTreeSet<_>>();
2873    // A transparent component can share its display root with its parent.
2874    // Follow committed invocation ownership, not only display-root markers.
2875    let mut children = BTreeMap::<ComponentInstancePath, Vec<ComponentInstancePath>>::new();
2876    for (path, recipe) in &reuse.invocations {
2877        children
2878            .entry(recipe.parent.clone())
2879            .or_default()
2880            .push(path.clone());
2881    }
2882    let mut pending = retained.iter().cloned().collect::<Vec<_>>();
2883    while let Some(parent) = pending.pop() {
2884        for child in children.get(&parent).into_iter().flatten() {
2885            if retained.insert(child.clone()) {
2886                pending.push(child.clone());
2887            }
2888        }
2889    }
2890    for path in &retained {
2891        if !active.transaction.retain_existing(path) {
2892            return Err(RuntimeError::ComponentRuntime(format!(
2893                "retained virtual component `{path}` has no committed state"
2894            )));
2895        }
2896    }
2897    active.seen.extend(retained.iter().cloned());
2898    active.invocations.extend(
2899        reuse
2900            .invocations
2901            .iter()
2902            .filter(|(path, _)| retained.contains(*path))
2903            .map(|(path, recipe)| (path.clone(), recipe.clone())),
2904    );
2905    active.event_handlers.extend(
2906        reuse
2907            .event_handlers
2908            .iter()
2909            .filter(|((path, _), _)| retained.contains(path))
2910            .map(|(id, callback)| (id.clone(), callback.clone())),
2911    );
2912    active.effects.extend(
2913        reuse
2914            .effects
2915            .iter()
2916            .filter(|(id, _)| retained.contains(id.component()))
2917            .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2918    );
2919    active.timers.extend(
2920        reuse
2921            .timers
2922            .iter()
2923            .filter(|(id, _)| retained.contains(id.component()))
2924            .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2925    );
2926    active.signals.extend(
2927        reuse
2928            .signals
2929            .iter()
2930            .filter(|(id, _)| retained.contains(id.component()))
2931            .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2932    );
2933    active.element_refs.extend(
2934        reuse
2935            .element_refs
2936            .iter()
2937            .filter(|id| retained.contains(id.component()))
2938            .cloned(),
2939    );
2940    active.virtual_collections.extend(
2941        reuse
2942            .virtual_collections
2943            .iter()
2944            .filter(|(id, _)| reuse.previous_root.virtual_collection_spec(id).is_some())
2945            .map(|(id, recipe)| (id.clone(), recipe.clone())),
2946    );
2947    Ok(())
2948}
2949
2950fn reusable_component_scope(
2951    active: &ActiveComponentRender,
2952    path: &ComponentInstancePath,
2953    caller_context: &UiContext,
2954    component: &crate::ComponentDefinition,
2955    invocation: &crate::ComponentInvocation,
2956    render: &FnPtr,
2957) -> Option<ReusedComponentScope> {
2958    let reuse = active.reuse.as_ref()?;
2959    if reuse.dirty.iter().any(|dirty| dirty.is_within(path)) {
2960        return None;
2961    }
2962    let recipe = reuse.invocations.get(path)?;
2963    if !recipe.reusable
2964        || recipe.generation != active.generation
2965        || recipe.environment != active.environment
2966        || recipe.parent != *caller_context.component_path()
2967        || recipe.component != component.metadata.id
2968        || recipe.export != component.metadata.export
2969        || recipe.declared_key != invocation.key
2970        || recipe.render.fn_name() != render.fn_name()
2971        || !recipe.props.reusable_eq(&invocation.retained_props)
2972    {
2973        return None;
2974    }
2975    let invocations = reuse
2976        .invocations
2977        .range(path.clone()..)
2978        .take_while(|(candidate, _)| candidate.is_within(path))
2979        .map(|(candidate, recipe)| (candidate.clone(), recipe.clone()))
2980        .collect::<BTreeMap<_, _>>();
2981    if invocations.is_empty()
2982        || invocations.values().any(|recipe| !recipe.reusable)
2983        || invocations
2984            .keys()
2985            .any(|candidate| !active.state_snapshot.contains_instance(candidate))
2986    {
2987        return None;
2988    }
2989    let node = if let Some(snapshot) = recipe.snapshot.current() {
2990        let mut node = snapshot.as_ref().clone();
2991        node.hydrate_component_subtrees();
2992        node
2993    } else {
2994        reuse.subtrees.get(&reuse.previous_root, path)?.clone()
2995    };
2996    Some(ReusedComponentScope {
2997        node,
2998        invocations,
2999        event_handlers: reuse
3000            .event_handlers
3001            .range((path.clone(), String::new())..)
3002            .take_while(|((candidate, _), _)| candidate.is_within(path))
3003            .map(|(id, callback)| (id.clone(), callback.clone()))
3004            .collect(),
3005        resources: reused_component_resources(reuse, path),
3006    })
3007}
3008
3009fn reused_component_resources(
3010    reuse: &ComponentReuseSnapshot,
3011    path: &ComponentInstancePath,
3012) -> ReusedComponentResources {
3013    ReusedComponentResources {
3014        effects: reuse
3015            .effects
3016            .iter()
3017            .filter(|(id, _)| id.component().is_within(path))
3018            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
3019            .collect(),
3020        timers: reuse
3021            .timers
3022            .iter()
3023            .filter(|(id, _)| id.component().is_within(path))
3024            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
3025            .collect(),
3026        signals: reuse
3027            .signals
3028            .iter()
3029            .filter(|(id, _)| id.component().is_within(path))
3030            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
3031            .collect(),
3032        element_refs: reuse
3033            .element_refs
3034            .iter()
3035            .filter(|id| id.component().is_within(path))
3036            .cloned()
3037            .collect(),
3038        virtual_collections: reuse
3039            .virtual_collections
3040            .iter()
3041            .filter(|(id, _)| id.component.is_within(path))
3042            .map(|(id, recipe)| (id.clone(), recipe.clone()))
3043            .collect(),
3044    }
3045}
3046
3047fn register_component_invocation(
3048    active: &ActiveComponentRenderState,
3049    recipe: ComponentInvocationRecipe,
3050) -> Result<(), Box<EvalAltResult>> {
3051    let mut guard = active.try_borrow_mut().map_err(|_| {
3052        Box::new(component_render_error(
3053            "component render stack is already borrowed",
3054        ))
3055    })?;
3056    let active = guard.as_mut().ok_or_else(|| {
3057        Box::new(component_render_error(
3058            "formal component rendering may run only inside view",
3059        ))
3060    })?;
3061    active.invocations.insert(recipe.path.clone(), recipe);
3062    Ok(())
3063}
3064
3065fn resolve_component_invocation(
3066    exports: &ComponentExportCollector,
3067    id: ImmutableString,
3068    props: Map,
3069    generation: ScriptGeneration,
3070) -> Result<(crate::ComponentDefinition, crate::ComponentInvocation), Box<EvalAltResult>> {
3071    let id: String = id.into();
3072    let id =
3073        ModuleId::parse(id).map_err(|error| Box::new(component_render_error(error.to_string())))?;
3074    let registry = exports
3075        .snapshot()
3076        .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3077    let component = registry.get(&id).cloned().ok_or_else(|| {
3078        Box::new(component_render_error(format!(
3079            "component `{id}` is not exported"
3080        )))
3081    })?;
3082    let key = props.get("key").and_then(|value| {
3083        value
3084            .is::<ImmutableString>()
3085            .then(|| value.clone_cast::<ImmutableString>().to_string())
3086    });
3087    let invocation = component
3088        .invoke_with_generation(key, props, generation)
3089        .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3090    Ok((component, invocation))
3091}
3092
3093fn reserve_component_path(
3094    call: &rhai::NativeCallContext<'_>,
3095    component: &crate::ComponentDefinition,
3096    invocation: &crate::ComponentInvocation,
3097    shared: &ActiveComponentRenderState,
3098) -> Result<(ComponentInstancePath, UiContext), Box<EvalAltResult>> {
3099    let mut guard = shared.try_borrow_mut().map_err(|_| {
3100        Box::new(component_render_error(
3101            "component render stack is already borrowed",
3102        ))
3103    })?;
3104    let active = guard.as_mut().ok_or_else(|| {
3105        Box::new(component_render_error(
3106            "formal component rendering may run only inside view",
3107        ))
3108    })?;
3109    let caller_context = active.contexts.last().cloned().ok_or_else(|| {
3110        Box::new(component_render_error(
3111            "component render context stack has no caller",
3112        ))
3113    })?;
3114    let parent = active.stack.last().cloned().ok_or_else(|| {
3115        Box::new(component_render_error(
3116            "component render stack has no application root",
3117        ))
3118    })?;
3119    let key = invocation.key.clone().unwrap_or_else(|| {
3120        let callsite = format!(
3121            "{}@{}",
3122            call.call_source().unwrap_or("<script>"),
3123            call.call_position()
3124        );
3125        let occurrence = active
3126            .positions
3127            .entry(format!("{parent}/{}/{callsite}", component.metadata.export))
3128            .or_default();
3129        let key = format!("{callsite}#{occurrence}");
3130        *occurrence = occurrence.saturating_add(1);
3131        key
3132    });
3133    let path = parent.child(&component.metadata.export, key);
3134    if !active.seen.insert(path.clone()) {
3135        return Err(Box::new(component_render_error(format!(
3136            "duplicate component instance path `{path}`; add a stable key"
3137        ))));
3138    }
3139    Ok((path, caller_context))
3140}
3141
3142fn enter_component_render(
3143    path: &ComponentInstancePath,
3144    component: &crate::ComponentDefinition,
3145    invocation: &crate::ComponentInvocation,
3146    native_context: crate::invocation::ScriptInvocationContext,
3147    shared: &ActiveComponentRenderState,
3148) -> Result<UiContext, Box<EvalAltResult>> {
3149    let mut guard = shared.try_borrow_mut().map_err(|_| {
3150        Box::new(component_render_error(
3151            "component render stack is already borrowed",
3152        ))
3153    })?;
3154    let active = guard.as_mut().ok_or_else(|| {
3155        Box::new(component_render_error(
3156            "formal component rendering may run only inside view",
3157        ))
3158    })?;
3159    active
3160        .transaction
3161        .mount(path.clone(), &component.schema.state)
3162        .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3163    let global_part_styles = {
3164        let mut runtime = active
3165            .root_context
3166            .runtime()
3167            .try_borrow_mut()
3168            .map_err(|_| Box::new(component_render_error("UI state is already borrowed")))?;
3169        runtime
3170            .component_state
3171            .mount_instance(path.clone(), &component.schema.state)
3172            .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3173        runtime
3174            .component_styles()
3175            .component(&component.metadata.id)
3176            .cloned()
3177            .unwrap_or_default()
3178    };
3179    let context = active
3180        .root_context
3181        .for_component(path.clone(), component.schema.events.clone())
3182        .with_component_styles(
3183            global_part_styles,
3184            component_root_style(&invocation.props),
3185            component_part_styles(&invocation.props),
3186        )
3187        .with_native_context(Some(native_context))
3188        .with_generation(active.generation);
3189    active.stack.push(path.clone());
3190    active.contexts.push(context.clone());
3191    active
3192        .effect_keys
3193        .push(Some(component.schema.effects.clone()));
3194    Ok(context)
3195}
3196
3197fn leave_component_render(active: &ActiveComponentRenderState) -> Result<(), Box<EvalAltResult>> {
3198    let mut guard = active.try_borrow_mut().map_err(|_| {
3199        Box::new(component_render_error(
3200            "component render stack is already borrowed",
3201        ))
3202    })?;
3203    if let Some(active) = guard.as_mut() {
3204        active.stack.pop();
3205        active.contexts.pop();
3206        active.effect_keys.pop();
3207    }
3208    Ok(())
3209}
3210
3211fn register_effect_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3212    let active = Rc::clone(active);
3213    FuncRegistration::new("effect")
3214        .in_global_namespace()
3215        .register_into_engine(
3216            engine,
3217            move |call: rhai::NativeCallContext<'_>,
3218                  key: ImmutableString,
3219                  dependencies: Dynamic,
3220                  start: FnPtr,
3221                  cleanup: FnPtr|
3222                  -> Result<(), Box<EvalAltResult>> {
3223                let key = key.to_string();
3224                let (component, context, generation, declared) = {
3225                    let guard = active.try_borrow().map_err(|_| {
3226                        Box::new(component_render_error(
3227                            "component render stack is already borrowed",
3228                        ))
3229                    })?;
3230                    let render = guard.as_ref().ok_or_else(|| {
3231                        Box::new(component_render_error(
3232                            "effect may run only during formal component render",
3233                        ))
3234                    })?;
3235                    let context = render.contexts.last().cloned().ok_or_else(|| {
3236                        Box::new(component_render_error(
3237                            "component render context stack is empty",
3238                        ))
3239                    })?;
3240                    let declared =
3241                        render
3242                            .effect_keys
3243                            .last()
3244                            .cloned()
3245                            .flatten()
3246                            .ok_or_else(|| {
3247                                Box::new(component_render_error(
3248                                    "effects may be declared only by formal components",
3249                                ))
3250                            })?;
3251                    (
3252                        context.component_path().clone(),
3253                        context,
3254                        render.generation,
3255                        declared,
3256                    )
3257                };
3258                if !declared.contains(&key) {
3259                    return Err(Box::new(component_render_error(
3260                        crate::EffectError::Undeclared { component, key }.to_string(),
3261                    )));
3262                }
3263                let id = crate::EffectId::new(component.clone(), key.clone())
3264                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3265                let dependencies = UiValue::from_dynamic(dependencies)
3266                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3267                let native_context =
3268                    crate::invocation::ScriptInvocationContext::capture_retained(&call);
3269                let mut start = ScriptCallback::try_from_fn_ptr(start, generation)
3270                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3271                start.bind_component_scope_if_unset(
3272                    &component,
3273                    context.component_incarnation(),
3274                    context.event_schemas().clone(),
3275                );
3276                start.bind_native_context_if_unset(native_context.clone());
3277                let mut cleanup = ScriptCallback::try_from_fn_ptr(cleanup, generation)
3278                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3279                cleanup.bind_component_scope_if_unset(
3280                    &component,
3281                    context.component_incarnation(),
3282                    context.event_schemas().clone(),
3283                );
3284                cleanup.bind_native_context_if_unset(native_context);
3285                let descriptor =
3286                    crate::EffectDescriptor::new(id.clone(), dependencies, start, cleanup);
3287                let mut guard = active.try_borrow_mut().map_err(|_| {
3288                    Box::new(component_render_error(
3289                        "component render stack is already borrowed",
3290                    ))
3291                })?;
3292                let render = guard.as_mut().ok_or_else(|| {
3293                    Box::new(component_render_error(
3294                        "effect may run only during formal component render",
3295                    ))
3296                })?;
3297                if render.effects.insert(id, descriptor).is_some() {
3298                    return Err(Box::new(component_render_error(
3299                        crate::EffectError::Duplicate { component, key }.to_string(),
3300                    )));
3301                }
3302                Ok(())
3303            },
3304        );
3305}
3306
3307fn register_timer_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3308    let active = Rc::clone(active);
3309    FuncRegistration::new("timeout")
3310        .in_global_namespace()
3311        .register_into_engine(
3312            engine,
3313            move |call: rhai::NativeCallContext<'_>,
3314                  key: ImmutableString,
3315                  delay_ms: rhai::INT,
3316                  paused: bool,
3317                  callback: FnPtr,
3318                  payload: Dynamic|
3319                  -> Result<(), Box<EvalAltResult>> {
3320                let (component, context, generation) = {
3321                    let guard = active.try_borrow().map_err(|_| {
3322                        Box::new(component_render_error(
3323                            "component render stack is already borrowed",
3324                        ))
3325                    })?;
3326                    let render = guard.as_ref().ok_or_else(|| {
3327                        Box::new(component_render_error(
3328                            "timeout may run only during formal component render",
3329                        ))
3330                    })?;
3331                    let context = render.contexts.last().cloned().ok_or_else(|| {
3332                        Box::new(component_render_error(
3333                            "component render context stack is empty",
3334                        ))
3335                    })?;
3336                    (context.component_path().clone(), context, render.generation)
3337                };
3338                let delay_ms = u64::try_from(delay_ms).map_err(|_| {
3339                    Box::new(component_render_error(
3340                        "timeout delay must be a positive integer",
3341                    ))
3342                })?;
3343                let id = crate::TimerId::new(component.clone(), key.to_string())
3344                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3345                let payload = UiValue::from_dynamic(payload)
3346                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3347                let native_context =
3348                    crate::invocation::ScriptInvocationContext::capture_retained(&call);
3349                let mut callback = ScriptCallback::try_from_fn_ptr(callback, generation)
3350                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3351                callback.bind_component_scope_if_unset(
3352                    &component,
3353                    context.component_incarnation(),
3354                    context.event_schemas().clone(),
3355                );
3356                callback.bind_native_context_if_unset(native_context);
3357                let descriptor = crate::TimerDescriptor::new(
3358                    id.clone(),
3359                    Duration::from_millis(delay_ms),
3360                    paused,
3361                    callback,
3362                    payload,
3363                )
3364                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3365                let mut guard = active.try_borrow_mut().map_err(|_| {
3366                    Box::new(component_render_error(
3367                        "component render stack is already borrowed",
3368                    ))
3369                })?;
3370                let render = guard.as_mut().ok_or_else(|| {
3371                    Box::new(component_render_error(
3372                        "timeout may run only during formal component render",
3373                    ))
3374                })?;
3375                if render.timers.insert(id.clone(), descriptor).is_some() {
3376                    return Err(Box::new(component_render_error(format!(
3377                        "timeout `{}` is declared more than once in `{}`",
3378                        id.key(),
3379                        id.component()
3380                    ))));
3381                }
3382                Ok(())
3383            },
3384        );
3385}
3386
3387fn register_signal_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3388    let scalar_active = Rc::clone(active);
3389    FuncRegistration::new("signal")
3390        .in_global_namespace()
3391        .register_into_engine(
3392            engine,
3393            move |key: ImmutableString,
3394                  initial: Dynamic|
3395                  -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3396                let key = key.to_string();
3397                let initial = crate::SignalValue::from_dynamic(initial)
3398                    .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3399                declare_signal(&scalar_active, key, initial)
3400            },
3401        );
3402
3403    let optional_active = Rc::clone(active);
3404    FuncRegistration::new("optional_float_signal")
3405        .in_global_namespace()
3406        .register_into_engine(
3407            engine,
3408            move |key: ImmutableString| -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3409                declare_signal(
3410                    &optional_active,
3411                    key.to_string(),
3412                    crate::SignalValue::OptionalFloat(None),
3413                )
3414            },
3415        );
3416
3417    let parent_active = Rc::clone(active);
3418    FuncRegistration::new("bind_parent_signal")
3419        .in_global_namespace()
3420        .register_into_engine(
3421            engine,
3422            move |node: &mut UiNode,
3423                  context: UiContext,
3424                  property: ImmutableString,
3425                  key: ImmutableString|
3426                  -> Result<UiNode, Box<EvalAltResult>> {
3427                let property = crate::SignalProperty::parse(property.as_str())
3428                    .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3429                if property.signal_kind() != crate::SignalKind::OptionalFloat {
3430                    return Err(Box::new(crate::signal::signal_runtime_error(
3431                        &"parent signal binding currently supports optional-float properties",
3432                    )));
3433                }
3434                // An owner rendering in this pass has declared its signals but
3435                // not committed them yet.
3436                let pending = parent_active
3437                    .try_borrow()
3438                    .ok()
3439                    .and_then(|guard| {
3440                        guard
3441                            .as_ref()
3442                            .map(|render| render.signals.keys().cloned().collect::<Vec<_>>())
3443                    })
3444                    .unwrap_or_default();
3445                let signal = context
3446                    .parent_optional_float_signal_ref(key.as_str(), &pending)
3447                    .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3448                node.clone()
3449                    .with_signal_binding(property, signal)
3450                    .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))
3451            },
3452        );
3453}
3454
3455fn declare_signal(
3456    active: &ActiveComponentRenderState,
3457    key: String,
3458    initial: crate::SignalValue,
3459) -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3460    let mut guard = active.try_borrow_mut().map_err(|_| {
3461        Box::new(crate::signal::signal_runtime_error(
3462            &"component render stack is already borrowed",
3463        ))
3464    })?;
3465    let render = guard.as_mut().ok_or_else(|| {
3466        Box::new(crate::signal::signal_runtime_error(
3467            &"signal may run only during formal component render",
3468        ))
3469    })?;
3470    if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3471        return Err(Box::new(crate::signal::signal_runtime_error(
3472            &"signals may be declared only by formal components",
3473        )));
3474    }
3475    let context = render.contexts.last().ok_or_else(|| {
3476        Box::new(crate::signal::signal_runtime_error(
3477            &"component render context stack is empty",
3478        ))
3479    })?;
3480    let component = context.component_path().clone();
3481    let incarnation = context.component_incarnation();
3482    if render
3483        .signals
3484        .keys()
3485        .any(|id| id.component() == &component && id.key() == key)
3486    {
3487        return Err(Box::new(crate::signal::signal_runtime_error(
3488            &crate::SignalError::Duplicate { component, key },
3489        )));
3490    }
3491    let id = crate::SignalId::new_scoped(component, incarnation, key, initial.kind())
3492        .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3493    let signal = crate::NativeSignal::new(id.clone());
3494    render
3495        .signals
3496        .insert(id, crate::signal::SignalDescriptor::new(initial));
3497    Ok(signal)
3498}
3499
3500fn register_element_ref_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3501    let active = Rc::clone(active);
3502    FuncRegistration::new("element_ref")
3503        .in_global_namespace()
3504        .register_into_engine(
3505            engine,
3506            move |key: ImmutableString| -> Result<crate::ElementRef, Box<EvalAltResult>> {
3507                let key = key.to_string();
3508                let mut guard = active.try_borrow_mut().map_err(|_| {
3509                    Box::new(component_render_error(
3510                        "component render stack is already borrowed",
3511                    ))
3512                })?;
3513                let render = guard.as_mut().ok_or_else(|| {
3514                    Box::new(component_render_error(
3515                        "element_ref may run only during formal component render",
3516                    ))
3517                })?;
3518                if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3519                    return Err(Box::new(component_render_error(
3520                        "element refs may be declared only by formal components",
3521                    )));
3522                }
3523                let component = render
3524                    .contexts
3525                    .last()
3526                    .ok_or_else(|| {
3527                        Box::new(component_render_error(
3528                            "component render context stack is empty",
3529                        ))
3530                    })?
3531                    .component_path()
3532                    .clone();
3533                let id = crate::ElementRefId::new(component.clone(), key.clone())
3534                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3535                if !render.element_refs.insert(id.clone()) {
3536                    return Err(Box::new(component_render_error(format!(
3537                        "element ref `{key}` is declared more than once by component `{component}`"
3538                    ))));
3539                }
3540                Ok(crate::ElementRef::new(id))
3541            },
3542        );
3543}
3544
3545#[derive(Debug)]
3546struct DecodedVirtualCollection {
3547    key: String,
3548    label: String,
3549    data: crate::VirtualCollectionData,
3550    estimated_height: f64,
3551    height: Option<f64>,
3552    overdraw_pixels: f64,
3553    bottom_align: bool,
3554    follow_tail: bool,
3555    reveal_key: Option<String>,
3556    sticky_headers: Arc<BTreeSet<usize>>,
3557}
3558
3559impl DecodedVirtualCollection {
3560    fn into_spec(
3561        self,
3562        id: crate::VirtualCollectionId,
3563        realized: BTreeMap<usize, UiNode>,
3564    ) -> crate::VirtualCollectionNodeSpec {
3565        crate::VirtualCollectionNodeSpec {
3566            id,
3567            label: self.label,
3568            data: self.data,
3569            realized,
3570            estimated_height: self.estimated_height,
3571            height: self.height,
3572            overdraw_pixels: self.overdraw_pixels,
3573            bottom_align: self.bottom_align,
3574            follow_tail: self.follow_tail,
3575            reveal_key: self.reveal_key,
3576            sticky_headers: self.sticky_headers,
3577            inherited_motion_group: None,
3578            inherited_motion_scope: None,
3579        }
3580    }
3581}
3582
3583fn decode_virtual_collection(
3584    mut config: Map,
3585) -> Result<DecodedVirtualCollection, Box<EvalAltResult>> {
3586    let key = collection_string(&mut config, "key")?;
3587    let label = collection_optional_string(&mut config, "label")?.unwrap_or_default();
3588    let estimated_height = collection_positive(&mut config, "estimated_height")?;
3589    let height = collection_optional_number(&mut config, "height")?;
3590    let fill_height = collection_optional_bool(&mut config, "fill_height")?.unwrap_or(false);
3591    let height = match (height, fill_height) {
3592        (Some(_), true) => {
3593            return Err(Box::new(component_render_error(
3594                "virtual collection must specify either `height` or `fill_height`, not both",
3595            )));
3596        }
3597        (None, false) => {
3598            return Err(Box::new(component_render_error(
3599                "virtual collection requires `height` or `fill_height: true`",
3600            )));
3601        }
3602        (None, true) => None,
3603        (Some(height), false) if height.is_finite() && height > 0.0 => Some(height),
3604        (Some(_), false) => {
3605            return Err(Box::new(component_render_error(
3606                "virtual collection `height` must be finite and positive",
3607            )));
3608        }
3609    };
3610    let overdraw_pixels = collection_optional_number(&mut config, "overdraw_pixels")?
3611        .unwrap_or(estimated_height * 2.0);
3612    if !overdraw_pixels.is_finite() || !(0.0..=10_000.0).contains(&overdraw_pixels) {
3613        return Err(Box::new(component_render_error(
3614            "virtual collection overdraw_pixels must be between 0 and 10000",
3615        )));
3616    }
3617    let alignment =
3618        collection_optional_string(&mut config, "alignment")?.unwrap_or_else(|| "top".to_owned());
3619    let bottom_align = match alignment.as_str() {
3620        "top" => false,
3621        "bottom" => true,
3622        _ => {
3623            return Err(Box::new(component_render_error(
3624                "virtual collection alignment must be `top` or `bottom`",
3625            )));
3626        }
3627    };
3628    let follow_tail = collection_optional_bool(&mut config, "follow_tail")?.unwrap_or(false);
3629    let data = collection_data(&mut config)?;
3630    let reveal_key = collection_optional_string(&mut config, "reveal_key")?;
3631    if reveal_key.as_deref() == Some("") {
3632        return Err(Box::new(component_render_error(
3633            "virtual collection `reveal_key` must not be empty",
3634        )));
3635    }
3636    if let Some(reveal_key) = reveal_key.as_deref()
3637        && !(0..data.len()).any(|index| data.key(index) == Some(reveal_key))
3638    {
3639        return Err(Box::new(component_render_error(format!(
3640            "virtual collection reveal key `{reveal_key}` is not present in its data"
3641        ))));
3642    }
3643    if follow_tail && reveal_key.is_some() {
3644        return Err(Box::new(component_render_error(
3645            "virtual collection must not combine `follow_tail` with `reveal_key`",
3646        )));
3647    }
3648    let sticky_headers = collection_optional_indices(&mut config, "sticky_headers")?
3649        .map_or_else(|| data.sticky_headers(), Arc::new);
3650    if let Some(index) = sticky_headers.iter().find(|index| **index >= data.len()) {
3651        return Err(Box::new(component_render_error(format!(
3652            "virtual collection sticky header index {index} is outside {} items",
3653            data.len()
3654        ))));
3655    }
3656    if bottom_align && !sticky_headers.is_empty() {
3657        return Err(Box::new(component_render_error(
3658            "sticky headers require top-aligned virtual collections",
3659        )));
3660    }
3661    if let Some((unknown, _)) = config.into_iter().next() {
3662        return Err(Box::new(component_render_error(format!(
3663            "unknown virtual collection field `{unknown}`"
3664        ))));
3665    }
3666    Ok(DecodedVirtualCollection {
3667        key,
3668        label,
3669        data,
3670        estimated_height,
3671        height,
3672        overdraw_pixels,
3673        bottom_align,
3674        follow_tail,
3675        reveal_key,
3676        sticky_headers,
3677    })
3678}
3679
3680fn register_virtual_collection_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3681    let active = Rc::clone(active);
3682    FuncRegistration::new("virtual_collection")
3683        .in_global_namespace()
3684        .register_into_engine(
3685            engine,
3686            move |call: rhai::NativeCallContext<'_>,
3687                  config: Map,
3688                  renderer: FnPtr|
3689                  -> Result<UiNode, Box<EvalAltResult>> {
3690                validate_virtual_renderer(&renderer)?;
3691                let decoded = decode_virtual_collection(config)?;
3692
3693                let VirtualCollectionContext {
3694                    component,
3695                    context,
3696                    generation,
3697                } = virtual_collection_context(&active)?;
3698                let id = crate::VirtualCollectionId {
3699                    component: component.clone(),
3700                    key: decoded.key.clone(),
3701                };
3702                let collection_context = context
3703                    .for_structural_scope(
3704                        component.child("VirtualCollection", decoded.key.clone()),
3705                        BTreeMap::new(),
3706                    )
3707                    .with_generation(generation);
3708                let native_context =
3709                    crate::invocation::ScriptInvocationContext::capture_retained(&call);
3710                let realized = realize_seeded_virtual_collection(
3711                    &call,
3712                    &renderer,
3713                    &collection_context,
3714                    &active,
3715                    &id,
3716                    &decoded,
3717                )?;
3718                let recipe = VirtualCollectionRecipe {
3719                    id: id.clone(),
3720                    data: decoded.data.clone(),
3721                    renderer,
3722                    renderer_context: native_context,
3723                    context: collection_context,
3724                    generation,
3725                };
3726                let mut guard = active.try_borrow_mut().map_err(|_| {
3727                    Box::new(component_render_error(
3728                        "component render stack is already borrowed",
3729                    ))
3730                })?;
3731                let render = guard.as_mut().ok_or_else(|| {
3732                    Box::new(component_render_error(
3733                        "virtual_collection may run only during view render",
3734                    ))
3735                })?;
3736                if render
3737                    .virtual_collections
3738                    .insert(id.clone(), recipe)
3739                    .is_some()
3740                {
3741                    return Err(Box::new(component_render_error(format!(
3742                        "virtual collection `{}` is declared more than once in `{}`",
3743                        decoded.key, id.component
3744                    ))));
3745                }
3746                Ok(UiNode::virtual_collection(decoded.into_spec(id, realized)))
3747            },
3748        );
3749}
3750
3751fn validate_virtual_renderer(renderer: &FnPtr) -> Result<(), Box<EvalAltResult>> {
3752    if renderer.is_anonymous() {
3753        return Err(Box::new(component_render_error(
3754            "virtual collection item renderer must be a named function",
3755        )));
3756    }
3757    for (index, value) in renderer.iter_curry().enumerate() {
3758        validate_virtual_renderer_curry(value).map_err(|error| {
3759            Box::new(component_render_error(format!(
3760                "virtual collection renderer curry {index} is not durable presentation data: {error}"
3761            )))
3762        })?;
3763    }
3764    Ok(())
3765}
3766
3767fn validate_virtual_renderer_curry(value: &Dynamic) -> Result<(), crate::UiValueError> {
3768    if value.is::<UiNode>() || value.is::<crate::NativeSignal>() {
3769        return Ok(());
3770    }
3771    if value.is::<Array>() {
3772        for value in value.clone_cast::<Array>() {
3773            validate_virtual_renderer_curry(&value)?;
3774        }
3775        return Ok(());
3776    }
3777    if value.is::<Map>() {
3778        for value in value.clone_cast::<Map>().values() {
3779            validate_virtual_renderer_curry(value)?;
3780        }
3781        return Ok(());
3782    }
3783    UiValue::from_dynamic(value.clone()).map(|_| ())
3784}
3785
3786/// The context an item renderer runs in: a structural `Item[<key>]` scope under
3787/// the collection, so components inside an item are named by the item, not by
3788/// how many items the batch rendered before it.
3789fn virtual_item_context(
3790    context: &UiContext,
3791    id: &crate::VirtualCollectionId,
3792    key: &str,
3793) -> UiContext {
3794    context
3795        .for_structural_scope(context.component_path().child("Item", key), BTreeMap::new())
3796        .for_virtual_item(id, key)
3797}
3798
3799fn realize_initial_collection(
3800    call: &rhai::NativeCallContext<'_>,
3801    renderer: &FnPtr,
3802    context: &UiContext,
3803    active: &ActiveComponentRenderState,
3804    id: &crate::VirtualCollectionId,
3805    data: &crate::VirtualCollectionData,
3806    indices: &BTreeSet<usize>,
3807) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3808    let mut realized = BTreeMap::new();
3809    for index in indices.iter().copied().filter(|index| *index < data.len()) {
3810        let item = data
3811            .item(index)
3812            .map_err(|error| Box::new(component_render_error(error.to_string())))?
3813            .ok_or_else(|| {
3814                Box::new(component_render_error(format!(
3815                    "virtual collection item {index} disappeared"
3816                )))
3817            })?;
3818        let (item_key, mut payload) = collection_payload(&item, index)?;
3819        add_collection_neighbors(data, index, &mut payload);
3820        let item_context = virtual_item_context(context, id, &item_key);
3821        enter_virtual_collection_scope(active, item_context.clone())?;
3822        let node = renderer.call_within_context::<UiNode>(call, (item_context, payload));
3823        leave_component_render(active)?;
3824        realized.insert(index, node?.with_key(item_key));
3825    }
3826    Ok(realized)
3827}
3828
3829fn realize_seeded_virtual_collection(
3830    call: &rhai::NativeCallContext<'_>,
3831    renderer: &FnPtr,
3832    context: &UiContext,
3833    active: &ActiveComponentRenderState,
3834    id: &crate::VirtualCollectionId,
3835    decoded: &DecodedVirtualCollection,
3836) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3837    let previous_metrics = context
3838        .runtime()
3839        .try_borrow()
3840        .map_err(|_| {
3841            Box::new(component_render_error(
3842                "UI state is already borrowed while seeding a virtual collection",
3843            ))
3844        })?
3845        .virtual_requests
3846        .metrics(id);
3847    let viewport = decoded.height.unwrap_or_else(|| {
3848        previous_metrics
3849            .as_ref()
3850            .map_or(decoded.estimated_height, |metrics| {
3851                if metrics.viewport_height > 0.0 {
3852                    metrics.viewport_height
3853                } else {
3854                    decoded.estimated_height
3855                }
3856            })
3857    });
3858    let count = nonnegative_usize(
3859        ((viewport + decoded.overdraw_pixels) / decoded.estimated_height).ceil() + 1.0,
3860    )
3861    .min(decoded.data.len());
3862    let indices =
3863        virtual_collection_seed_indices(active, id, decoded, count, previous_metrics.as_ref())?;
3864    enter_virtual_collection_scope(active, context.clone())?;
3865    let realized =
3866        realize_initial_collection(call, renderer, context, active, id, &decoded.data, &indices);
3867    leave_component_render(active)?;
3868    realized
3869}
3870
3871fn virtual_collection_seed_indices(
3872    active: &ActiveComponentRenderState,
3873    id: &crate::VirtualCollectionId,
3874    decoded: &DecodedVirtualCollection,
3875    count: usize,
3876    metrics: Option<&crate::VirtualCollectionMetrics>,
3877) -> Result<BTreeSet<usize>, Box<EvalAltResult>> {
3878    let mut indices = BTreeSet::new();
3879    let guard = active.try_borrow().map_err(|_| {
3880        Box::new(component_render_error(
3881            "component render stack is already borrowed",
3882        ))
3883    })?;
3884    let reuse = guard.as_ref().and_then(|render| render.reuse.as_ref());
3885    let previous = reuse.and_then(|reuse| reuse.previous_root.virtual_collection_spec(id));
3886    let preserves_scroll = previous.is_some_and(|previous| {
3887        previous.estimated_height.to_bits() == decoded.estimated_height.to_bits()
3888            && previous.overdraw_pixels.to_bits() == decoded.overdraw_pixels.to_bits()
3889            && previous.bottom_align == decoded.bottom_align
3890            && same_collection_key_order(&previous.data, &decoded.data)
3891    });
3892
3893    if preserves_scroll {
3894        if let Some(previous) = previous {
3895            indices.extend(previous.realized.keys().copied());
3896        }
3897        if let Some(metrics) = metrics {
3898            extend_virtual_window(
3899                &mut indices,
3900                metrics.scroll_item,
3901                count,
3902                decoded.data.len(),
3903                virtual_overdraw_items(decoded),
3904            );
3905        }
3906    } else if decoded.reveal_key.is_none() && !decoded.bottom_align && !decoded.follow_tail {
3907        extend_virtual_window(&mut indices, 0, count, decoded.data.len(), 0);
3908    }
3909    drop(guard);
3910
3911    if let Some(reveal) = decoded.reveal_key.as_deref()
3912        && let Some(index) =
3913            (0..decoded.data.len()).find(|index| decoded.data.key(*index) == Some(reveal))
3914    {
3915        extend_virtual_window(
3916            &mut indices,
3917            index,
3918            count,
3919            decoded.data.len(),
3920            virtual_overdraw_items(decoded),
3921        );
3922        if let Some(header) = decoded.sticky_headers.range(..=index).next_back() {
3923            indices.insert(*header);
3924        }
3925    }
3926    if (decoded.bottom_align || decoded.follow_tail) && !decoded.data.is_empty() {
3927        let start = decoded.data.len().saturating_sub(count);
3928        indices.extend(start..decoded.data.len());
3929    }
3930    Ok(indices)
3931}
3932
3933fn same_collection_key_order(
3934    current: &crate::VirtualCollectionData,
3935    next: &crate::VirtualCollectionData,
3936) -> bool {
3937    current.len() == next.len()
3938        && (0..current.len()).all(|index| current.key(index) == next.key(index))
3939}
3940
3941fn virtual_overdraw_items(decoded: &DecodedVirtualCollection) -> usize {
3942    nonnegative_usize((decoded.overdraw_pixels / decoded.estimated_height).ceil())
3943}
3944
3945fn extend_virtual_window(
3946    indices: &mut BTreeSet<usize>,
3947    anchor: usize,
3948    count: usize,
3949    len: usize,
3950    leading: usize,
3951) {
3952    let start = anchor.saturating_sub(leading).min(len);
3953    let end = anchor.saturating_add(count).min(len);
3954    indices.extend(start..end);
3955}
3956
3957struct VirtualCollectionContext {
3958    component: ComponentInstancePath,
3959    context: UiContext,
3960    generation: ScriptGeneration,
3961}
3962
3963fn enter_virtual_collection_scope(
3964    active: &ActiveComponentRenderState,
3965    context: UiContext,
3966) -> Result<(), Box<EvalAltResult>> {
3967    let mut guard = active.try_borrow_mut().map_err(|_| {
3968        Box::new(component_render_error(
3969            "component render stack is already borrowed",
3970        ))
3971    })?;
3972    let render = guard.as_mut().ok_or_else(|| {
3973        Box::new(component_render_error(
3974            "virtual_collection may run only during view render",
3975        ))
3976    })?;
3977    render.stack.push(context.component_path().clone());
3978    render.contexts.push(context);
3979    render.effect_keys.push(None);
3980    Ok(())
3981}
3982
3983fn virtual_collection_context(
3984    active: &ActiveComponentRenderState,
3985) -> Result<VirtualCollectionContext, Box<EvalAltResult>> {
3986    let guard = active.try_borrow().map_err(|_| {
3987        Box::new(component_render_error(
3988            "component render stack is already borrowed",
3989        ))
3990    })?;
3991    let render = guard.as_ref().ok_or_else(|| {
3992        Box::new(component_render_error(
3993            "virtual_collection may run only during view render",
3994        ))
3995    })?;
3996    let context = render.contexts.last().cloned().ok_or_else(|| {
3997        Box::new(component_render_error(
3998            "component render context stack is empty",
3999        ))
4000    })?;
4001    Ok(VirtualCollectionContext {
4002        component: context.component_path().clone(),
4003        context: context.clone(),
4004        generation: render.generation,
4005    })
4006}
4007
4008fn collection_string(config: &mut Map, name: &str) -> Result<String, Box<EvalAltResult>> {
4009    config
4010        .remove(name)
4011        .and_then(Dynamic::try_cast::<ImmutableString>)
4012        .map(|value| value.to_string())
4013        .ok_or_else(|| {
4014            Box::new(component_render_error(format!(
4015                "virtual collection `{name}` must be a string"
4016            )))
4017        })
4018}
4019
4020fn collection_optional_string(
4021    config: &mut Map,
4022    name: &str,
4023) -> Result<Option<String>, Box<EvalAltResult>> {
4024    config
4025        .remove(name)
4026        .map(|value| {
4027            value
4028                .try_cast::<ImmutableString>()
4029                .map(|value| value.to_string())
4030                .ok_or_else(|| {
4031                    Box::new(component_render_error(format!(
4032                        "virtual collection `{name}` must be a string"
4033                    )))
4034                })
4035        })
4036        .transpose()
4037}
4038
4039fn collection_optional_number(
4040    config: &mut Map,
4041    name: &str,
4042) -> Result<Option<f64>, Box<EvalAltResult>> {
4043    config
4044        .remove(name)
4045        .map(|value| {
4046            if value.is::<rhai::FLOAT>() {
4047                Ok(value.cast::<rhai::FLOAT>())
4048            } else if value.is::<rhai::INT>() {
4049                value
4050                    .cast::<rhai::INT>()
4051                    .to_string()
4052                    .parse::<f64>()
4053                    .map_err(|_| Box::new(component_render_error("invalid collection number")))
4054            } else {
4055                Err(Box::new(component_render_error(format!(
4056                    "virtual collection `{name}` must be a number"
4057                ))))
4058            }
4059        })
4060        .transpose()
4061}
4062
4063fn collection_positive(config: &mut Map, name: &str) -> Result<f64, Box<EvalAltResult>> {
4064    let value = collection_optional_number(config, name)?.ok_or_else(|| {
4065        Box::new(component_render_error(format!(
4066            "virtual collection `{name}` is required"
4067        )))
4068    })?;
4069    if value.is_finite() && value > 0.0 {
4070        Ok(value)
4071    } else {
4072        Err(Box::new(component_render_error(format!(
4073            "virtual collection `{name}` must be finite and positive"
4074        ))))
4075    }
4076}
4077
4078fn collection_optional_bool(
4079    config: &mut Map,
4080    name: &str,
4081) -> Result<Option<bool>, Box<EvalAltResult>> {
4082    config
4083        .remove(name)
4084        .map(|value| {
4085            value.try_cast::<bool>().ok_or_else(|| {
4086                Box::new(component_render_error(format!(
4087                    "virtual collection `{name}` must be bool"
4088                )))
4089            })
4090        })
4091        .transpose()
4092}
4093
4094fn collection_optional_indices(
4095    config: &mut Map,
4096    name: &str,
4097) -> Result<Option<BTreeSet<usize>>, Box<EvalAltResult>> {
4098    let Some(value) = config.remove(name) else {
4099        return Ok(None);
4100    };
4101    let values = value.try_cast::<Array>().ok_or_else(|| {
4102        Box::new(component_render_error(format!(
4103            "virtual collection `{name}` must be an array of non-negative integers"
4104        )))
4105    })?;
4106    let mut indices = BTreeSet::new();
4107    for (position, value) in values.into_iter().enumerate() {
4108        let index = value.try_cast::<rhai::INT>().ok_or_else(|| {
4109            Box::new(component_render_error(format!(
4110                "virtual collection `{name}` item {position} must be an integer"
4111            )))
4112        })?;
4113        let index = usize::try_from(index).map_err(|_| {
4114            Box::new(component_render_error(format!(
4115                "virtual collection `{name}` item {position} must be non-negative"
4116            )))
4117        })?;
4118        if !indices.insert(index) {
4119            return Err(Box::new(component_render_error(format!(
4120                "virtual collection `{name}` contains duplicate index {index}"
4121            ))));
4122        }
4123    }
4124    Ok(Some(indices))
4125}
4126
4127fn collection_data(config: &mut Map) -> Result<crate::VirtualCollectionData, Box<EvalAltResult>> {
4128    let data = config.remove("data").ok_or_else(|| {
4129        Box::new(component_render_error(
4130            "virtual collection `data` is required",
4131        ))
4132    })?;
4133    if data.is::<crate::NativeCollection>() {
4134        return Ok(crate::VirtualCollectionData::Native(
4135            data.cast::<crate::NativeCollection>(),
4136        ));
4137    }
4138    let data = data.try_cast::<Array>().ok_or_else(|| {
4139        Box::new(component_render_error(
4140            "virtual collection `data` must be an array or NativeCollection",
4141        ))
4142    })?;
4143    let data = data
4144        .into_iter()
4145        .map(|value| {
4146            UiValue::from_dynamic(value)
4147                .map_err(|error| Box::new(component_render_error(error.to_string())))
4148        })
4149        .collect::<Result<Vec<_>, _>>()?;
4150    let mut keys = BTreeSet::new();
4151    for (index, item) in data.iter().enumerate() {
4152        let (key, _) = collection_payload(item, index)?;
4153        if !keys.insert(key.clone()) {
4154            return Err(Box::new(component_render_error(format!(
4155                "virtual collection data key `{key}` is duplicated"
4156            ))));
4157        }
4158    }
4159    Ok(crate::VirtualCollectionData::Values(data))
4160}
4161
4162fn collection_payload(item: &UiValue, index: usize) -> Result<(String, Map), Box<EvalAltResult>> {
4163    let UiValue::Map(map) = item else {
4164        return Err(Box::new(component_render_error(
4165            "virtual collection data items must be maps with a string `key`",
4166        )));
4167    };
4168    let key = match map.get("key") {
4169        Some(UiValue::String(key)) if !key.is_empty() => key.clone(),
4170        _ => {
4171            return Err(Box::new(component_render_error(
4172                "virtual collection data item `key` must be a non-empty string",
4173            )));
4174        }
4175    };
4176    Ok((
4177        key.clone(),
4178        Map::from_iter([
4179            ("key".into(), Dynamic::from(key)),
4180            (
4181                "index".into(),
4182                Dynamic::from_int(rhai::INT::try_from(index).unwrap_or(rhai::INT::MAX)),
4183            ),
4184            ("item".into(), item.clone().into_dynamic()),
4185        ]),
4186    ))
4187}
4188
4189fn add_collection_neighbors(data: &crate::VirtualCollectionData, index: usize, payload: &mut Map) {
4190    let key = |index: Option<usize>| {
4191        index
4192            .and_then(|index| data.key(index))
4193            .map_or(Dynamic::UNIT, |key| Dynamic::from(key.to_owned()))
4194    };
4195    payload.insert("previous_key".into(), key(index.checked_sub(1)));
4196    payload.insert(
4197        "next_key".into(),
4198        key(index.checked_add(1).filter(|index| *index < data.len())),
4199    );
4200    payload.insert("first_key".into(), key((!data.is_empty()).then_some(0)));
4201    payload.insert("last_key".into(), key(data.len().checked_sub(1)));
4202}
4203
4204fn nonnegative_usize(value: f64) -> usize {
4205    value.to_string().parse().unwrap_or(usize::MAX)
4206}
4207
4208fn component_event_callbacks(
4209    component: &crate::ComponentDefinition,
4210    props: &Map,
4211) -> Vec<(String, FnPtr)> {
4212    component
4213        .schema
4214        .events
4215        .keys()
4216        .filter_map(|event| {
4217            props
4218                .get(format!("on_{event}").as_str())
4219                .filter(|value| value.is::<FnPtr>())
4220                .map(|value| (event.clone(), value.clone_cast::<FnPtr>()))
4221        })
4222        .collect()
4223}
4224
4225fn bind_component_callback_props(
4226    props: &mut Map,
4227    schema: &BTreeMap<String, crate::ObjectField>,
4228    binding: &ComponentCallbackBinding,
4229) {
4230    for (name, field) in schema {
4231        if schema_contains_callback(&field.schema)
4232            && let Some(value) = props.get_mut(name.as_str())
4233        {
4234            bind_component_callback_value(&field.schema, value, binding);
4235        }
4236    }
4237}
4238
4239fn schema_contains_callback(schema: &crate::ValueSchema) -> bool {
4240    match schema {
4241        crate::ValueSchema::Callback => true,
4242        crate::ValueSchema::Array { items, .. }
4243        | crate::ValueSchema::Map { values: items }
4244        | crate::ValueSchema::Optional { value: items } => schema_contains_callback(items),
4245        crate::ValueSchema::Object { fields, .. } => fields
4246            .values()
4247            .any(|field| schema_contains_callback(&field.schema)),
4248        crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_callback),
4249        _ => false,
4250    }
4251}
4252
4253fn bind_component_callback_value(
4254    schema: &crate::ValueSchema,
4255    value: &mut Dynamic,
4256    binding: &ComponentCallbackBinding,
4257) {
4258    match schema {
4259        crate::ValueSchema::Callback if value.is::<FnPtr>() => {
4260            let mut function = value.clone_cast::<FnPtr>();
4261            if !function
4262                .iter_curry()
4263                .any(Dynamic::is::<ComponentCallbackBinding>)
4264            {
4265                function.add_curry(Dynamic::from(binding.clone()));
4266                *value = Dynamic::from(function);
4267            }
4268        }
4269        crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4270            bind_component_callback_value(inner, value, binding);
4271        }
4272        crate::ValueSchema::OneOf { variants } => {
4273            if let Some(variant) = variants
4274                .iter()
4275                .find(|variant| variant.validate(value).is_ok())
4276            {
4277                bind_component_callback_value(variant, value, binding);
4278            }
4279        }
4280        crate::ValueSchema::Array { items, .. }
4281            if schema_contains_callback(items) && value.is::<Array>() =>
4282        {
4283            let mut values = value.clone_cast::<Array>();
4284            for value in &mut values {
4285                bind_component_callback_value(items, value, binding);
4286            }
4287            *value = Dynamic::from_array(values);
4288        }
4289        crate::ValueSchema::Map {
4290            values: item_schema,
4291        } if schema_contains_callback(item_schema) && value.is::<Map>() => {
4292            let mut values = value.clone_cast::<Map>();
4293            for value in values.values_mut() {
4294                bind_component_callback_value(item_schema, value, binding);
4295            }
4296            *value = Dynamic::from_map(values);
4297        }
4298        crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4299            let mut values = value.clone_cast::<Map>();
4300            for (name, field) in fields {
4301                if schema_contains_callback(&field.schema)
4302                    && let Some(value) = values.get_mut(name.as_str())
4303                {
4304                    bind_component_callback_value(&field.schema, value, binding);
4305                }
4306            }
4307            *value = Dynamic::from_map(values);
4308        }
4309        _ => {}
4310    }
4311}
4312
4313fn bind_component_node_props(
4314    props: &mut Map,
4315    schema: &BTreeMap<String, crate::ObjectField>,
4316    binding: &ComponentCallbackBinding,
4317) {
4318    for (name, field) in schema {
4319        if schema_contains_node(&field.schema)
4320            && let Some(value) = props.get_mut(name.as_str())
4321        {
4322            bind_component_node_value(&field.schema, value, binding);
4323        }
4324    }
4325}
4326
4327fn schema_contains_node(schema: &crate::ValueSchema) -> bool {
4328    match schema {
4329        crate::ValueSchema::Node => true,
4330        crate::ValueSchema::Array { items, .. }
4331        | crate::ValueSchema::Map { values: items }
4332        | crate::ValueSchema::Optional { value: items } => schema_contains_node(items),
4333        crate::ValueSchema::Object { fields, .. } => fields
4334            .values()
4335            .any(|field| schema_contains_node(&field.schema)),
4336        crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_node),
4337        _ => false,
4338    }
4339}
4340
4341fn bind_component_node_value(
4342    schema: &crate::ValueSchema,
4343    value: &mut Dynamic,
4344    binding: &ComponentCallbackBinding,
4345) {
4346    match schema {
4347        crate::ValueSchema::Node if value.is::<UiNode>() => {
4348            let mut node = value.clone_cast::<UiNode>();
4349            node.bind_component_scope(
4350                &binding.component,
4351                binding.incarnation,
4352                &binding.events,
4353                binding.context.as_ref(),
4354            );
4355            node.activate_component_snapshots();
4356            *value = Dynamic::from(node);
4357        }
4358        crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4359            bind_component_node_value(inner, value, binding);
4360        }
4361        crate::ValueSchema::OneOf { variants } => {
4362            if let Some(variant) = variants
4363                .iter()
4364                .find(|variant| variant.validate(value).is_ok())
4365            {
4366                bind_component_node_value(variant, value, binding);
4367            }
4368        }
4369        crate::ValueSchema::Array { items, .. }
4370            if schema_contains_node(items) && value.is::<Array>() =>
4371        {
4372            let mut values = value.clone_cast::<Array>();
4373            for value in &mut values {
4374                bind_component_node_value(items, value, binding);
4375            }
4376            *value = Dynamic::from_array(values);
4377        }
4378        crate::ValueSchema::Map {
4379            values: item_schema,
4380        } if schema_contains_node(item_schema) && value.is::<Map>() => {
4381            let mut values = value.clone_cast::<Map>();
4382            for value in values.values_mut() {
4383                bind_component_node_value(item_schema, value, binding);
4384            }
4385            *value = Dynamic::from_map(values);
4386        }
4387        crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4388            let mut values = value.clone_cast::<Map>();
4389            for (name, field) in fields {
4390                if schema_contains_node(&field.schema)
4391                    && let Some(value) = values.get_mut(name.as_str())
4392                {
4393                    bind_component_node_value(&field.schema, value, binding);
4394                }
4395            }
4396            *value = Dynamic::from_map(values);
4397        }
4398        _ => {}
4399    }
4400}
4401
4402fn hydrate_replayed_node_value(value: &mut Dynamic) {
4403    if value.is::<UiNode>() {
4404        let mut node = value.clone_cast::<UiNode>();
4405        node.hydrate_component_subtrees();
4406        *value = Dynamic::from(node);
4407    } else if value.is::<Array>() {
4408        let mut values = value.clone_cast::<Array>();
4409        for value in &mut values {
4410            hydrate_replayed_node_value(value);
4411        }
4412        *value = Dynamic::from_array(values);
4413    } else if value.is::<Map>() {
4414        let mut values = value.clone_cast::<Map>();
4415        for value in values.values_mut() {
4416            hydrate_replayed_node_value(value);
4417        }
4418        *value = Dynamic::from_map(values);
4419    }
4420}
4421
4422fn component_part_styles(props: &Map) -> BTreeMap<String, crate::Style> {
4423    props
4424        .get("part_styles")
4425        .filter(|value| value.is::<Map>())
4426        .map(Dynamic::clone_cast::<Map>)
4427        .unwrap_or_default()
4428        .into_iter()
4429        .filter_map(|(name, value)| {
4430            value
4431                .is::<crate::Style>()
4432                .then(|| (name.to_string(), value.cast::<crate::Style>()))
4433        })
4434        .collect()
4435}
4436
4437fn component_root_style(props: &Map) -> Option<crate::Style> {
4438    props
4439        .get("style")
4440        .filter(|value| value.is::<crate::Style>())
4441        .map(Dynamic::clone_cast::<crate::Style>)
4442}
4443
4444fn register_component_event_callbacks(
4445    active: &ActiveComponentRenderState,
4446    component: &ComponentInstancePath,
4447    caller: &UiContext,
4448    callbacks: Vec<(String, FnPtr)>,
4449    native_context: &crate::invocation::ScriptInvocationContext,
4450) -> Result<(), Box<EvalAltResult>> {
4451    let mut guard = active.try_borrow_mut().map_err(|_| {
4452        Box::new(component_render_error(
4453            "component render stack is already borrowed",
4454        ))
4455    })?;
4456    let active = guard.as_mut().ok_or_else(|| {
4457        Box::new(component_render_error(
4458            "formal component rendering may run only inside view",
4459        ))
4460    })?;
4461    for (event, function) in callbacks {
4462        let mut callback = ScriptCallback::try_from_fn_ptr(function, active.generation)
4463            .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4464        callback.bind_component_scope_if_unset(
4465            caller.component_path(),
4466            caller.component_incarnation(),
4467            caller.event_schemas().clone(),
4468        );
4469        callback.bind_native_context_if_unset(native_context.clone());
4470        active
4471            .event_handlers
4472            .insert((component.clone(), event), callback);
4473    }
4474    Ok(())
4475}
4476
4477fn component_render_error(message: impl Into<String>) -> EvalAltResult {
4478    EvalAltResult::ErrorRuntime(Dynamic::from(message.into()), Position::NONE)
4479}
4480
4481#[cfg(feature = "charts")]
4482fn register_chart_validation_api(engine: &mut Engine) {
4483    engine.register_fn(
4484        "chart_validate",
4485        |spec: Map, data: Dynamic, key_dimension: Dynamic| -> Result<(), Box<EvalAltResult>> {
4486            let spec = UiValue::from_dynamic(Dynamic::from_map(spec))
4487                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4488            crate::ChartSpec::from_ui_value(&spec)
4489                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4490            let key_dimension = if key_dimension.is_unit() {
4491                None
4492            } else if key_dimension.is::<ImmutableString>() {
4493                Some(key_dimension.cast::<ImmutableString>().to_string())
4494            } else {
4495                return Err(Box::new(component_render_error(
4496                    "chart key_dimension must be a string or null",
4497                )));
4498            };
4499            if data.is::<crate::NativeChartData>() {
4500                return Ok(());
4501            }
4502            if !data.is::<Array>() {
4503                return Err(Box::new(component_render_error(
4504                    "chart data must be an array of objects or NativeChartData",
4505                )));
4506            }
4507            let data = UiValue::from_dynamic(Dynamic::from_array(data.cast::<Array>()))
4508                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4509            let UiValue::Array(rows) = data else {
4510                unreachable!()
4511            };
4512            if rows.len() > 10_000 {
4513                return Err(Box::new(component_render_error(
4514                    "inline chart data is limited to 10000 rows; use NativeChartData",
4515                )));
4516            }
4517            let rows = rows
4518                .into_iter()
4519                .enumerate()
4520                .map(|(index, row)| match row {
4521                    UiValue::Map(row) => Ok(row),
4522                    _ => Err(Box::new(component_render_error(format!(
4523                        "chart inline row {index} must be an object"
4524                    )))),
4525                })
4526                .collect::<Result<Vec<_>, _>>()?;
4527            crate::ChartDataset::from_rows(
4528                "main",
4529                &rows,
4530                key_dimension,
4531                crate::ChartDataLimits::default(),
4532            )
4533            .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4534            Ok(())
4535        },
4536    );
4537}
4538
4539#[cfg(test)]
4540mod tests {
4541    use super::*;
4542
4543    fn virtual_config(sticky: &[i64], alignment: &str) -> Map {
4544        let mut item = Map::new();
4545        item.insert("key".into(), Dynamic::from("header"));
4546        let mut config = Map::new();
4547        config.insert("key".into(), Dynamic::from("sections"));
4548        config.insert(
4549            "data".into(),
4550            Dynamic::from_array(vec![Dynamic::from_map(item)]),
4551        );
4552        config.insert("estimated_height".into(), Dynamic::from_float(30.0));
4553        config.insert("height".into(), Dynamic::from_float(120.0));
4554        config.insert("alignment".into(), Dynamic::from(alignment.to_owned()));
4555        config.insert(
4556            "sticky_headers".into(),
4557            Dynamic::from_array(sticky.iter().copied().map(Dynamic::from_int).collect()),
4558        );
4559        config
4560    }
4561
4562    #[test]
4563    fn sticky_virtual_collection_indices_are_bounded_unique_and_top_aligned() {
4564        let decoded = decode_virtual_collection(virtual_config(&[0], "top")).unwrap();
4565        assert_eq!(decoded.sticky_headers.as_ref(), &BTreeSet::from([0]));
4566
4567        for (sticky, alignment, expected) in [
4568            (&[1][..], "top", "outside 1 items"),
4569            (&[0, 0][..], "top", "duplicate index 0"),
4570            (&[0][..], "bottom", "require top-aligned"),
4571        ] {
4572            let error = decode_virtual_collection(virtual_config(sticky, alignment)).unwrap_err();
4573            assert!(error.to_string().contains(expected), "{error}");
4574        }
4575    }
4576
4577    #[test]
4578    fn virtual_collection_reveal_key_is_known_and_has_one_scroll_owner() {
4579        let mut config = virtual_config(&[], "top");
4580        config.insert("reveal_key".into(), Dynamic::from("header"));
4581        let decoded = decode_virtual_collection(config).unwrap();
4582        assert_eq!(decoded.reveal_key.as_deref(), Some("header"));
4583
4584        let mut unknown = virtual_config(&[], "top");
4585        unknown.insert("reveal_key".into(), Dynamic::from("missing"));
4586        let error = decode_virtual_collection(unknown).unwrap_err();
4587        assert!(error.to_string().contains("is not present"), "{error}");
4588
4589        let mut competing = virtual_config(&[], "top");
4590        competing.insert("reveal_key".into(), Dynamic::from("header"));
4591        competing.insert("follow_tail".into(), Dynamic::from(true));
4592        let error = decode_virtual_collection(competing).unwrap_err();
4593        assert!(error.to_string().contains("must not combine"), "{error}");
4594    }
4595
4596    #[test]
4597    fn script_builds_a_declarative_tree() {
4598        let mut runtime = RuntimeEngine::new();
4599        let compiled = runtime
4600            .compile(
4601                r#"
4602                    fn view() {
4603                        column([
4604                            text("Hello").with_key("greeting"),
4605                            text("from Rhai")
4606                        ])
4607                    }
4608                "#,
4609            )
4610            .expect("script should compile");
4611
4612        let root = runtime.render(&compiled).expect("view should render");
4613        assert_eq!(
4614            root.source().map(|source| source.module.as_str()),
4615            Some("ui/main.rhai")
4616        );
4617        let crate::UiNodeKind::Box { children } = root.kind() else {
4618            panic!("column must render a box");
4619        };
4620        assert_eq!(children.len(), 2);
4621        assert_eq!(
4622            children[0].key().map(crate::NodeKey::as_str),
4623            Some("greeting")
4624        );
4625        assert!(children.iter().all(|child| child.source().is_some()));
4626    }
4627
4628    #[test]
4629    fn runtime_emits_official_rhai_language_server_definitions() {
4630        let runtime = RuntimeEngine::new();
4631        let definitions = runtime.definition_source();
4632        assert!(definitions.starts_with("module static;"));
4633        assert!(definitions.contains("fn box"));
4634        assert!(definitions.contains("fn timeout"));
4635        assert!(definitions.contains("fn canvas_fill_path"));
4636        assert!(definitions.contains("UiNode"));
4637    }
4638
4639    #[test]
4640    fn retained_callbacks_reject_capturing_functions_and_non_data_curry() {
4641        let mut runtime = RuntimeEngine::new();
4642        let compiled = runtime
4643            .compile(r#"fn view() { text("x").on_click(|| ()) }"#)
4644            .unwrap();
4645        let error = runtime.render(&compiled).unwrap_err();
4646        assert!(
4647            error
4648                .to_string()
4649                .contains("anonymous or capturing functions")
4650        );
4651
4652        let mut callback = FnPtr::new("clicked").unwrap();
4653        callback.add_curry(Dynamic::from(UiNode::text("not durable data")));
4654        assert!(matches!(
4655            ScriptCallback::try_from_fn_ptr(callback, ScriptGeneration::initial()),
4656            Err(ScriptCallbackDefinitionError::InvalidCurry { index: 0, .. })
4657        ));
4658    }
4659
4660    #[test]
4661    fn replay_hydrates_formal_subtrees_inside_nested_dynamic_node_shapes() {
4662        let root = ComponentInstancePath::root("View", "main");
4663        let outer = root.child("Outer", "outer");
4664        let inner = root.child("Inner", "inner");
4665        let inner_snapshot = crate::node::ComponentOwnedSnapshot::default();
4666        let mut current_inner = UiNode::text("current")
4667            .with_component_root_snapshot(inner.clone(), inner_snapshot.reference());
4668        current_inner.activate_component_snapshots();
4669        let outer_snapshot = crate::node::ComponentOwnedSnapshot::default();
4670        let mut current_outer = UiNode::box_node(vec![
4671            UiNode::text("outer"),
4672            UiNode::text("stale-inner")
4673                .with_component_root_snapshot(inner, inner_snapshot.reference()),
4674        ])
4675        .with_component_root_snapshot(outer.clone(), outer_snapshot.reference());
4676        current_outer.activate_component_snapshots();
4677        let stale = UiNode::box_node(vec![
4678            UiNode::text("stale-outer")
4679                .with_component_root_snapshot(outer, outer_snapshot.reference())
4680                .with_attribute("presentation", UiValue::Bool(true)),
4681        ]);
4682        let mut nested = Map::new();
4683        nested.insert(
4684            "object".into(),
4685            Dynamic::from_map(Map::from_iter([(
4686                "nodes".into(),
4687                Dynamic::from_array(vec![Dynamic::from(stale)]),
4688            )])),
4689        );
4690        let mut value = Dynamic::from_map(nested);
4691
4692        hydrate_replayed_node_value(&mut value);
4693
4694        let nested = value.cast::<Map>();
4695        let object = nested["object"].clone_cast::<Map>();
4696        let nodes = object["nodes"].clone_cast::<Array>();
4697        let node = nodes[0].clone_cast::<UiNode>();
4698        let crate::UiNodeKind::Box { children } = node.kind() else {
4699            panic!("nested node shape must preserve its raw wrapper");
4700        };
4701        assert_eq!(
4702            children[0].attributes().get("presentation"),
4703            Some(&UiValue::Bool(true))
4704        );
4705        let crate::UiNodeKind::Box { children } = children[0].kind() else {
4706            panic!("outer component must hydrate its owned snapshot");
4707        };
4708        assert!(
4709            matches!(children[1].kind(), crate::UiNodeKind::Text { text } if text == "current")
4710        );
4711    }
4712
4713    #[test]
4714    fn final_component_api_registers_render_once_and_invokes_by_id() {
4715        let mut runtime = RuntimeEngine::new();
4716        let compiled = runtime
4717            .compile(
4718                r#"
4719                    define_component(#{
4720                        metadata: #{
4721                            id: "components/message", "export": "Message", version: "0.1.0",
4722                            runtime_api: #{ min_inclusive: 3, max_exclusive: 4 },
4723                            dependencies: [], capabilities: #{}
4724                        },
4725                        schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
4726                        render: Fn("render_Message")
4727                    });
4728                    fn Message(props) { render_component("components/message", props) }
4729                    fn render_Message(ctx, props) { text("final component api") }
4730                    fn view() { Message(#{ key: "message" }) }
4731                "#,
4732            )
4733            .unwrap();
4734        let root = runtime.render(&compiled).unwrap();
4735        assert!(matches!(
4736            root.kind(),
4737            crate::UiNodeKind::Text { text } if text == "final component api"
4738        ));
4739        let recipe = runtime.component_invocations().next().unwrap();
4740        assert_eq!(recipe.render_name(), "render_Message");
4741    }
4742
4743    #[test]
4744    fn self_contained_compile_rejects_dynamic_imports_before_evaluation() {
4745        let mut runtime = RuntimeEngine::new();
4746        assert!(matches!(
4747            runtime.compile_self_contained_named(
4748                "ui/dynamic_import.rhai",
4749                "let path = \"components/button\"; import path as button;",
4750            ),
4751            Err(RuntimeError::Import(_))
4752        ));
4753    }
4754
4755    #[test]
4756    fn script_builds_an_error_boundary() {
4757        let mut runtime = RuntimeEngine::new();
4758        let compiled = runtime
4759            .compile(
4760                r#"
4761                    fn view() {
4762                        error_boundary(text("content"), text("fallback"))
4763                    }
4764                "#,
4765            )
4766            .unwrap();
4767        let root = runtime.render(&compiled).unwrap();
4768        assert!(matches!(
4769            root.kind(),
4770            crate::UiNodeKind::ErrorBoundary { .. }
4771        ));
4772    }
4773
4774    #[test]
4775    fn lazy_error_boundary_catches_rhai_evaluation_failure() {
4776        let mut runtime = RuntimeEngine::new();
4777        let compiled = runtime
4778            .compile(
4779                r#"
4780                    fn broken() { throw "boom"; }
4781                    fn fallback() { text("fallback") }
4782                    fn view() { error_boundary_lazy(Fn("broken"), Fn("fallback")) }
4783                "#,
4784            )
4785            .unwrap();
4786        let root = runtime.render(&compiled).unwrap();
4787        assert!(matches!(root.kind(), crate::UiNodeKind::Text { text } if text == "fallback"));
4788        assert!(matches!(
4789            root.attributes().get("boundary_error"),
4790            Some(crate::UiValue::String(message)) if message.contains("boom")
4791        ));
4792    }
4793
4794    #[test]
4795    fn script_builds_a_typed_controlled_overlay() {
4796        let mut runtime = RuntimeEngine::new();
4797        let compiled = runtime
4798            .compile(
4799                r#"
4800                    fn changed(ctx, open) { () }
4801                    fn view() {
4802                        overlay(
4803                            text("trigger"),
4804                            text("content"),
4805                            #{
4806                                id: "account",
4807                                kind: "popover",
4808                                placement: "bottom",
4809                                open: true,
4810                                gap: 6
4811                            }
4812                        ).on_open_change(Fn("changed"))
4813                    }
4814                "#,
4815            )
4816            .unwrap();
4817        let root = runtime.render(&compiled).unwrap();
4818        assert!(matches!(
4819            root.kind(),
4820            crate::UiNodeKind::Overlay { spec, .. }
4821                if spec.id.as_str() == "account"
4822                    && spec.open
4823                    && (spec.gap - 6.0).abs() < f64::EPSILON
4824                    && spec.placement == crate::OverlayPlacement::Bottom
4825        ));
4826        assert_eq!(
4827            root.handler("open_change")
4828                .unwrap()
4829                .as_script()
4830                .unwrap()
4831                .generation(),
4832            compiled.generation()
4833        );
4834    }
4835
4836    #[test]
4837    fn click_handlers_can_carry_typed_semantic_payloads() {
4838        let mut runtime = RuntimeEngine::new();
4839        let compiled = runtime
4840            .compile(
4841                r#"
4842                    fn changed(ctx, value) { () }
4843                    fn view() {
4844                        text("toggle").on_click_value(
4845                            Fn("changed"), #{ checked: true, source: "pointer" }
4846                        )
4847                    }
4848                "#,
4849            )
4850            .unwrap();
4851        let root = runtime.render(&compiled).unwrap();
4852        assert_eq!(
4853            root.event_handlers("click")[0].value(),
4854            Some(&crate::UiValue::Map(BTreeMap::from([
4855                ("checked".to_owned(), crate::UiValue::Bool(true)),
4856                (
4857                    "source".to_owned(),
4858                    crate::UiValue::String("pointer".to_owned()),
4859                ),
4860            ])))
4861        );
4862        assert!(root.handlers().contains_key("click"));
4863    }
4864
4865    #[test]
4866    fn rhai_nodes_retain_ordered_handlers_for_all_event_phases() {
4867        let mut runtime = RuntimeEngine::new();
4868        let compiled = runtime
4869            .compile(
4870                r#"
4871                    fn first(ctx, event) { event_response().prevent_default() }
4872                    fn second(ctx, event) { event_response().stop_immediate() }
4873                    fn capture(ctx, event) { propagate() }
4874                    fn view() {
4875                        text("drag")
4876                            .on_capture("pointer_down", Fn("capture"))
4877                            .on("pointer_down", Fn("first"))
4878                            .on("pointer_down", Fn("second"))
4879                    }
4880                "#,
4881            )
4882            .unwrap();
4883        let root = runtime.render(&compiled).unwrap();
4884        let bindings = root.event_handlers("pointer_down");
4885        assert_eq!(bindings.len(), 3);
4886        assert_eq!(bindings[0].phase(), crate::EventPhase::Capture);
4887        assert_eq!(bindings[1].phase(), crate::EventPhase::Target);
4888        assert_eq!(bindings[2].phase(), crate::EventPhase::Target);
4889        assert_eq!(bindings[2].handler().as_script().unwrap().name(), "second");
4890    }
4891
4892    #[test]
4893    fn rhai_nodes_attach_only_registered_schema_checked_native_handlers() {
4894        let mut runtime = RuntimeEngine::new();
4895        let id = crate::NativeHandlerId::parse("timeline.drag").unwrap();
4896        runtime
4897            .register_native_handler(
4898                crate::NativeHandlerDescriptor::new(
4899                    id,
4900                    BTreeMap::from([("pointer_down".to_owned(), crate::ValueSchema::UiValue)]),
4901                )
4902                .unwrap(),
4903                |_, _, _, _| Ok(crate::EventResponse::new().capture_pointer()),
4904            )
4905            .unwrap();
4906        let compiled = runtime
4907            .compile(
4908                r#"
4909                    fn view() {
4910                        text("drag").on(
4911                            "pointer_down", native_handler("timeline.drag")
4912                        )
4913                    }
4914                "#,
4915            )
4916            .unwrap();
4917        let root = runtime.render(&compiled).unwrap();
4918        assert!(matches!(
4919            root.handler("pointer_down"),
4920            Some(crate::UiEventHandler::Native(reference))
4921                if reference.descriptor().id.as_str() == "timeline.drag"
4922        ));
4923
4924        let input = runtime
4925            .compile(
4926                r#"
4927                    fn view() {
4928                        gpui_rhai::TextInputPrimitive(#{
4929                            key: "native-input", value: "", placeholder: "Edit",
4930                            disabled: false, read_only: false, typography: "body",
4931                            on_change: native_handler("timeline.drag")
4932                        })
4933                    }
4934                "#,
4935            )
4936            .unwrap();
4937        assert!(
4938            runtime.render(&input).is_ok(),
4939            "native handlers must cross formal component and primitive callback props"
4940        );
4941
4942        let missing = runtime
4943            .compile(
4944                "fn view() { text(\"x\").on(\"pointer_down\", native_handler(\"app.missing\")) }",
4945            )
4946            .unwrap();
4947        assert!(runtime.render(&missing).is_err());
4948    }
4949
4950    #[test]
4951    fn final_box_and_fragment_atoms_are_distinct_snapshots() {
4952        let mut runtime = RuntimeEngine::new();
4953        let compiled = runtime
4954            .compile(
4955                r#"
4956                    fn view() {
4957                        box([text("a"), fragment([text("b"), text("c")])])
4958                    }
4959                "#,
4960            )
4961            .unwrap();
4962        let root = runtime.render(&compiled).unwrap();
4963        let crate::UiNodeKind::Box { children } = root.kind() else {
4964            panic!("root must be the final Box atom");
4965        };
4966        assert!(matches!(
4967            children[1].kind(),
4968            crate::UiNodeKind::Fragment { children } if children.len() == 2
4969        ));
4970    }
4971
4972    #[test]
4973    fn text_accepts_typed_inline_span_runs() {
4974        let mut runtime = RuntimeEngine::new();
4975        let compiled = runtime
4976            .compile(
4977                r#"
4978                    fn view() {
4979                        text([
4980                            span("Hello ").bold(),
4981                            span("world").color(rgb(0x22cc88)).italic()
4982                        ])
4983                    }
4984                "#,
4985            )
4986            .unwrap();
4987        let root = runtime.render(&compiled).unwrap();
4988        let crate::UiNodeKind::RichText { text, spans } = root.kind() else {
4989            panic!("typed spans must produce rich text");
4990        };
4991        assert_eq!(text.as_str(), "Hello world");
4992        assert_eq!(spans.len(), 2);
4993        assert!(spans[0].is_bold());
4994        assert!(spans[1].is_italic());
4995    }
4996
4997    #[test]
4998    fn canvas_scene_is_keyed_validated_and_script_constructible() {
4999        let mut runtime = RuntimeEngine::new();
5000        let compiled = runtime
5001            .compile(
5002                r#"
5003                    fn view() {
5004                        canvas(canvas_scene([
5005                            canvas_rect("panel", 0.0, 0.0, 100.0, 40.0, rgb(0x112233)),
5006                            canvas_circle("dot", 20.0, 20.0, 8.0, rgb(0xffcc00)),
5007                            canvas_line("axis", 0.0, 39.0, 100.0, 39.0, 1.0, rgb(0xffffff))
5008                        ]))
5009                    }
5010                "#,
5011            )
5012            .unwrap();
5013        let root = runtime.render(&compiled).unwrap();
5014        assert!(matches!(
5015            root.kind(),
5016            crate::UiNodeKind::Canvas { scene } if scene.commands().len() == 3
5017        ));
5018    }
5019
5020    #[test]
5021    fn inline_svg_atom_is_bounded_and_self_contained() {
5022        let mut runtime = RuntimeEngine::new();
5023        let compiled = runtime
5024            .compile(
5025                r#"
5026                    fn view() {
5027                        svg("<svg viewBox='0 0 10 10'><path fill='currentColor' d='M0 0L10 10'/></svg>")
5028                    }
5029                "#,
5030            )
5031            .unwrap();
5032        let root = runtime.render(&compiled).unwrap();
5033        assert!(matches!(
5034            root.kind(),
5035            crate::UiNodeKind::Svg { source } if source.as_str().contains("currentColor")
5036        ));
5037
5038        let rejected = runtime
5039            .compile(r#"fn view() { svg("<svg><image href='https://example.com/a.png'/></svg>") }"#)
5040            .unwrap();
5041        assert!(runtime.render(&rejected).is_err());
5042    }
5043
5044    #[test]
5045    fn recompilation_invalidates_previous_generation() {
5046        let mut runtime = RuntimeEngine::new();
5047        let first = runtime.compile("fn view() { text(\"first\") }").unwrap();
5048        runtime.render(&first).unwrap();
5049        let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
5050
5051        assert!(runtime.is_current(first.generation()));
5052        assert!(!runtime.is_current(second.generation()));
5053        runtime.render(&second).unwrap();
5054        assert!(!runtime.is_current(first.generation()));
5055        assert!(runtime.is_current(second.generation()));
5056    }
5057
5058    #[test]
5059    fn callbacks_are_rejected_after_reload() {
5060        let mut runtime = RuntimeEngine::new();
5061        let first = runtime
5062            .compile(
5063                r#"
5064                    fn view() { text("first") }
5065                    fn add_suffix(value) { value + "!" }
5066                "#,
5067            )
5068            .unwrap();
5069        runtime.render(&first).unwrap();
5070        let callback = runtime.callback(&first, "add_suffix").unwrap();
5071
5072        let value = runtime
5073            .invoke_callback(&first, &callback, ("hello",))
5074            .unwrap();
5075        assert_eq!(value.into_immutable_string().unwrap(), "hello!");
5076
5077        let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
5078        runtime.render(&second).unwrap();
5079        assert!(matches!(
5080            runtime.invoke_callback(&second, &callback, ("hello",)),
5081            Err(RuntimeError::StaleCallback { .. })
5082        ));
5083    }
5084
5085    #[test]
5086    fn failed_candidate_keeps_last_good_generation_active() {
5087        let mut runtime = RuntimeEngine::new();
5088        let active = runtime
5089            .compile(
5090                r#"
5091                    fn view() { text("active") }
5092                    fn action(value) { value }
5093                "#,
5094            )
5095            .unwrap();
5096        runtime.render(&active).unwrap();
5097        let callback = runtime.callback(&active, "action").unwrap();
5098
5099        let candidate = runtime
5100            .compile("fn view() { throw \"render failed\"; }")
5101            .unwrap();
5102        assert!(runtime.render(&candidate).is_err());
5103        assert!(runtime.is_current(active.generation()));
5104        let _ = runtime
5105            .invoke_callback(&active, &callback, ("still active",))
5106            .unwrap();
5107    }
5108
5109    #[test]
5110    fn failed_nested_component_records_the_deepest_component_path() {
5111        let mut runtime = RuntimeEngine::new();
5112        let compiled = runtime
5113            .compile(
5114                r#"
5115                    define_component(#{
5116                        metadata: #{ id: "components/broken", "export": "Broken",
5117                            version: "0.1.3",
5118                            runtime_api: #{ min_inclusive: 3, max_exclusive: 4 },
5119                            dependencies: [], capabilities: #{} },
5120                        schema: #{ props: #{}, state: #{ fields: #{} }, events: #{},
5121                            slots: #{}, parts: [] },
5122                        render: Fn("render_Broken")
5123                    });
5124                    fn Broken(props) { render_component("components/broken", props) }
5125                    fn render_Broken(ctx, props) { throw "nested failure"; }
5126                    fn view() { Broken(#{ key: "inner" }) }
5127                "#,
5128            )
5129            .unwrap();
5130
5131        assert!(runtime.render(&compiled).is_err());
5132        assert_eq!(
5133            runtime
5134                .last_failed_component()
5135                .as_ref()
5136                .map(ToString::to_string),
5137            Some("/App[root]/Broken[inner]".to_owned())
5138        );
5139        assert!(runtime.last_failed_timing().is_some());
5140
5141        let recovered = runtime
5142            .compile("fn view() { text(\"recovered\") }")
5143            .unwrap();
5144        runtime.render(&recovered).unwrap();
5145        assert_eq!(runtime.last_failed_component(), None);
5146        assert_eq!(runtime.last_failed_timing(), None);
5147    }
5148
5149    #[test]
5150    fn execution_timings_cover_compile_and_render() {
5151        let mut runtime = RuntimeEngine::new();
5152        runtime.set_slow_threshold(Duration::ZERO);
5153        let compiled = runtime
5154            .compile(
5155                r#"
5156                    fn view() { text("timed") }
5157                    fn counted() { let total = 0; for value in 0..100 { total += value; } total }
5158                "#,
5159            )
5160            .unwrap();
5161        runtime.render(&compiled).unwrap();
5162        let callback = runtime.callback(&compiled, "counted").unwrap();
5163        let _ = runtime.invoke_callback(&compiled, &callback, ()).unwrap();
5164        let timings = runtime.take_timings();
5165        assert_eq!(timings.len(), 3);
5166        assert_eq!(timings[0].operation, ExecutionOperation::Compile);
5167        assert_eq!(timings[1].operation, ExecutionOperation::Render);
5168        assert_eq!(
5169            timings[2].operation,
5170            ExecutionOperation::Callback("counted".to_owned())
5171        );
5172        assert_eq!(timings[0].operations, 0);
5173        assert!(timings[1].operations > 0);
5174        assert!(timings[2].operations > timings[1].operations);
5175        assert!(timings.iter().all(|timing| timing.slow && timing.succeeded));
5176    }
5177
5178    #[test]
5179    fn independent_candidates_never_share_callback_authority() {
5180        let mut runtime = RuntimeEngine::new();
5181        let first = runtime
5182            .compile("fn view() { text(\"first\") } fn action() { 1 }")
5183            .unwrap();
5184        let callback = runtime.callback(&first, "action").unwrap();
5185        let second = runtime
5186            .compile("fn view() { text(\"second\") } fn action() { 2 }")
5187            .unwrap();
5188        assert_ne!(first.generation(), second.generation());
5189        runtime.render(&second).unwrap();
5190        assert!(matches!(
5191            runtime.invoke_callback(&second, &callback, ()),
5192            Err(RuntimeError::StaleCallback { .. })
5193        ));
5194    }
5195
5196    #[test]
5197    fn callbacks_cannot_cross_runtime_engines() {
5198        let mut first_engine = RuntimeEngine::new();
5199        let first = first_engine
5200            .compile("fn view() { text(\"first\") } fn action() { 1 }")
5201            .unwrap();
5202        first_engine.render(&first).unwrap();
5203        let callback = first_engine.callback(&first, "action").unwrap();
5204
5205        let mut second_engine = RuntimeEngine::new();
5206        let second = second_engine
5207            .compile("fn view() { text(\"second\") } fn action() { 2 }")
5208            .unwrap();
5209        second_engine.render(&second).unwrap();
5210        assert!(matches!(
5211            second_engine.invoke_callback(&second, &callback, ()),
5212            Err(RuntimeError::StaleCallback { .. })
5213        ));
5214    }
5215
5216    #[test]
5217    fn one_program_preparation_shares_identity_across_module_and_entry_compilation() {
5218        let mut runtime = RuntimeEngine::new();
5219        let (module, entry) = runtime
5220            .with_program_preparation(|runtime| {
5221                Ok::<_, RuntimeError>((
5222                    runtime.compile_named("module", "fn helper() { 1 }")?,
5223                    runtime.compile_named("entry", "fn view() { text(\"entry\") }")?,
5224                ))
5225            })
5226            .unwrap();
5227        assert_eq!(module.generation(), entry.generation());
5228        let next = runtime.compile("fn view() { text(\"next\") }").unwrap();
5229        assert_ne!(entry.generation(), next.generation());
5230    }
5231
5232    #[test]
5233    fn unsafe_const_container_assignment_is_rejected_before_evaluation() {
5234        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5235            RuntimeEngine::new()
5236                .compile("fn view() { const m = #{ x: 1 }; m.x = 2; text(\"done\") }")
5237        }));
5238        assert!(matches!(
5239            result,
5240            Ok(Err(RuntimeError::InvalidAssignmentTarget(_)))
5241        ));
5242    }
5243
5244    #[test]
5245    fn default_runtime_rejects_filesystem_imports() {
5246        let directory = tempfile::tempdir().unwrap();
5247        let module = directory.path().join("external.rhai");
5248        std::fs::write(&module, "fn value() { \"external\" }").unwrap();
5249        let mut runtime = RuntimeEngine::new();
5250        let source = format!(
5251            "import {:?} as external; fn view() {{ text(external::value()) }}",
5252            module.with_extension("").to_string_lossy()
5253        );
5254        let compiled = runtime.compile(&source).unwrap();
5255        assert!(runtime.render(&compiled).is_err());
5256    }
5257
5258    #[test]
5259    fn nested_component_work_consumes_one_shared_operation_budget() {
5260        let module = r#"
5261            define_component(#{
5262                metadata: #{ id: "components/heavy", "export": "Heavy", version: "0.1.1",
5263                    runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5264                schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5265                render: Fn("render_Heavy")
5266            });
5267            fn Heavy(props) { render_component("components/heavy", props) }
5268            fn render_Heavy(ctx, props) {
5269                let n = 0;
5270                for i in 0..200000 { n += 1; }
5271                text(`${n}`)
5272            }
5273        "#;
5274        let mut resolver = crate::RestrictedModuleResolver::new();
5275        resolver.insert("components/heavy", module).unwrap();
5276        let mut runtime = RuntimeEngine::new();
5277        runtime.set_module_resolver(resolver);
5278        let compiled = runtime
5279            .compile_self_contained_named(
5280                "budget",
5281                r#"import "components/heavy" as h;
5282                    fn view() { column([h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{} )]) }"#,
5283            )
5284            .unwrap();
5285        let result = runtime.render(&compiled);
5286        assert!(result.is_err(), "nested work must exceed the shared budget");
5287        assert!(
5288            runtime
5289                .take_timings()
5290                .iter()
5291                .any(|timing| timing.operations > DEFAULT_SCRIPT_OPERATION_LIMIT)
5292        );
5293    }
5294
5295    #[test]
5296    fn operation_limit_is_host_configurable() {
5297        let heavy = r"fn view() { let n = 0; for i in 0..400000 { n += 1; } text(`${n}`) }";
5298        let mut runtime = RuntimeEngine::new();
5299        assert_eq!(runtime.operation_limit(), DEFAULT_SCRIPT_OPERATION_LIMIT);
5300        let compiled = runtime
5301            .compile_self_contained_named("limit", heavy)
5302            .unwrap();
5303        assert!(
5304            runtime.render(&compiled).is_err(),
5305            "this finite workload actually exceeds the default budget"
5306        );
5307        assert!(runtime.last_failed_timing().unwrap().operations > DEFAULT_SCRIPT_OPERATION_LIMIT);
5308
5309        runtime.set_operation_limit(10_000);
5310        let compiled = runtime
5311            .compile_self_contained_named("limit-low", heavy)
5312            .unwrap();
5313        assert!(
5314            runtime.render(&compiled).is_err(),
5315            "a lowered budget rejects the same script"
5316        );
5317
5318        runtime.set_operation_limit(50_000_000);
5319        let compiled = runtime
5320            .compile_self_contained_named("limit-high", heavy)
5321            .unwrap();
5322        assert!(
5323            runtime.render(&compiled).is_ok(),
5324            "a raised budget admits heavier one-shot work"
5325        );
5326    }
5327
5328    #[test]
5329    fn host_policies_are_independent_and_reload_candidates_do_not_share_mutable_quota() {
5330        let mut engine = RuntimeEngine::new();
5331        assert_eq!(engine.expression_depth_limits(), (64, 32));
5332        engine.set_operation_limit(2_000_000);
5333        engine.set_expression_depth_limits(80, 64);
5334        assert_eq!(engine.engine().max_call_levels(), 64);
5335        assert_eq!(engine.engine().max_array_size(), RHAI_MAX_ARRAY_SIZE);
5336        #[cfg(feature = "dev-reload")]
5337        {
5338            let mut candidate = engine.candidate_engine();
5339            assert_eq!(candidate.operation_limit(), 2_000_000);
5340            assert_eq!(candidate.expression_depth_limits(), (80, 64));
5341            candidate.set_operation_limit(10);
5342            candidate.set_expression_depth_limits(20, 10);
5343            assert_eq!(engine.operation_limit(), 2_000_000);
5344            assert_eq!(engine.expression_depth_limits(), (80, 64));
5345        }
5346        engine.set_operation_limit(0);
5347        engine.set_expression_depth_limits(0, 0);
5348        assert_eq!(engine.operation_limit(), 1);
5349        assert_eq!(engine.expression_depth_limits(), (1, 1));
5350    }
5351
5352    #[test]
5353    fn retained_component_budget_ignores_parent_history_without_resetting_session_total() {
5354        let module = r#"
5355            define_component(#{
5356                metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5357                    runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5358                schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5359                    state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5360                    events: #{}, slots: #{}, parts: ["root"] },
5361                render: Fn("render_Probe")
5362            });
5363            fn Probe(props) { render_component("components/probe", props) }
5364            fn render_Probe(ctx, props) {
5365                let n = 0;
5366                if ctx.get_state("heavy") { for i in 0..100000 { n += 1; } }
5367                text(`${n}`)
5368            }
5369        "#;
5370        let mut measured = Vec::new();
5371        for parent_work in [0, 250_000] {
5372            let mut resolver = crate::RestrictedModuleResolver::new();
5373            resolver.insert("components/probe", module).unwrap();
5374            let mut engine = RuntimeEngine::new();
5375            engine.set_module_resolver(resolver);
5376            let source = format!(
5377                r#"import "components/probe" as probe;
5378                    fn view(ctx) {{
5379                        let n = 0;
5380                        for i in 0..{parent_work} {{ n += 1; }}
5381                        probe::Probe(#{{ key: "probe" }})
5382                    }}"#
5383            );
5384            let compiled = engine
5385                .compile_self_contained_named("incremental-budget", &source)
5386                .unwrap();
5387            let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5388            let mut lifecycle = crate::ScriptLifecycle::new(
5389                compiled,
5390                Rc::clone(&runtime),
5391                ComponentInstancePath::root("App", "budget"),
5392                None,
5393                BTreeMap::new(),
5394                &ComponentStateSchema::default(),
5395            )
5396            .unwrap();
5397            lifecycle.start(&mut engine).unwrap();
5398            let owner = engine
5399                .component_invocations()
5400                .next()
5401                .unwrap()
5402                .path()
5403                .clone();
5404            let _ = engine.take_timings();
5405            runtime
5406                .borrow_mut()
5407                .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5408                .unwrap();
5409            assert!(lifecycle.render_dirty(&mut engine).unwrap());
5410            let operations = engine
5411                .take_timings()
5412                .into_iter()
5413                .filter(|timing| timing.operation == ExecutionOperation::Render)
5414                .map(|timing| timing.operations)
5415                .sum::<u64>();
5416            assert!(
5417                operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5418                "{operations}"
5419            );
5420            measured.push(operations);
5421        }
5422        assert_eq!(measured[0], measured[1]);
5423    }
5424
5425    #[test]
5426    fn multiple_dirty_components_share_one_incremental_budget() {
5427        let module = r#"
5428            define_component(#{
5429                metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5430                    runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5431                schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5432                    state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5433                    events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Probe") }
5434            );
5435            fn Probe(props) { render_component("components/probe", props) }
5436            fn render_Probe(ctx, props) {
5437                let n = 0;
5438                if ctx.get_state("heavy") { for i in 0..200000 { n += 1; } }
5439                text(`${n}`)
5440            }
5441        "#;
5442        let mut resolver = crate::RestrictedModuleResolver::new();
5443        resolver.insert("components/probe", module).unwrap();
5444        let mut engine = RuntimeEngine::new();
5445        engine.set_module_resolver(resolver);
5446        let compiled = engine
5447            .compile_self_contained_named(
5448                "incremental-aggregate-budget",
5449                r#"import "components/probe" as probe;
5450                    fn view(ctx) {
5451                        column([
5452                            probe::Probe(#{ key: "first" }),
5453                            probe::Probe(#{ key: "second" }),
5454                        ])
5455                    }"#,
5456            )
5457            .unwrap();
5458        let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5459        let mut lifecycle = crate::ScriptLifecycle::new(
5460            compiled,
5461            Rc::clone(&runtime),
5462            ComponentInstancePath::root("App", "budget"),
5463            None,
5464            BTreeMap::new(),
5465            &ComponentStateSchema::default(),
5466        )
5467        .unwrap();
5468        lifecycle.start(&mut engine).unwrap();
5469        let owners = engine
5470            .component_invocations()
5471            .map(|recipe| recipe.path().clone())
5472            .collect::<Vec<_>>();
5473        assert_eq!(owners.len(), 2);
5474        for owner in owners {
5475            runtime
5476                .borrow_mut()
5477                .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5478                .unwrap();
5479        }
5480        assert!(lifecycle.render_dirty(&mut engine).is_err());
5481    }
5482
5483    #[test]
5484    fn retained_virtual_renderer_budget_ignores_parent_history() {
5485        let mut measured = Vec::new();
5486        for parent_work in [0, 250_000] {
5487            let mut engine = RuntimeEngine::new();
5488            let source = format!(
5489                r#"
5490                    fn render_item(ctx, payload) {{
5491                        let n = 0;
5492                        if payload.item.heavy {{ for i in 0..100000 {{ n += 1; }} }}
5493                        text(`${{n}}`).with_key(payload.key)
5494                    }}
5495                    fn view(ctx) {{
5496                        let n = 0;
5497                        for i in 0..{parent_work} {{ n += 1; }}
5498                        let data = [];
5499                        for index in 0..32 {{
5500                            data.push(#{{ key: `row-${{index}}`, heavy: index == 31 }});
5501                        }}
5502                        virtual_collection(#{{
5503                            key: "rows", label: "Rows", data: data,
5504                            estimated_height: 24, height: 24, overdraw_pixels: 0,
5505                            alignment: "top", follow_tail: false,
5506                        }}, Fn("render_item"))
5507                    }}
5508                "#
5509            );
5510            let compiled = engine.compile_named("virtual-budget", &source).unwrap();
5511            let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5512            let root_path = ComponentInstancePath::root("App", "budget");
5513            let mut lifecycle = crate::ScriptLifecycle::new(
5514                compiled,
5515                Rc::clone(&runtime),
5516                root_path.clone(),
5517                None,
5518                BTreeMap::new(),
5519                &ComponentStateSchema::default(),
5520            )
5521            .unwrap();
5522            lifecycle.start(&mut engine).unwrap();
5523            let id = engine
5524                .virtual_collection_ids_in_scope(&root_path)
5525                .into_iter()
5526                .next()
5527                .unwrap();
5528            let _ = engine.take_timings();
5529            runtime.borrow().virtual_requests.request(id, [31]);
5530            assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5531            let operations = engine
5532                .take_timings()
5533                .into_iter()
5534                .filter(|timing| {
5535                    matches!(timing.operation, ExecutionOperation::VirtualCollection(_))
5536                })
5537                .map(|timing| timing.operations)
5538                .sum::<u64>();
5539            assert!(
5540                operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5541                "{operations}"
5542            );
5543            measured.push(operations);
5544        }
5545        assert_eq!(measured[0], measured[1]);
5546    }
5547
5548    #[test]
5549    fn retained_virtual_renderer_replays_curried_node_presentation() {
5550        let mut engine = RuntimeEngine::new();
5551        let compiled = engine
5552            .compile_named(
5553                "virtual-rich-presentation",
5554                r#"
5555                    fn render_item(content, ctx, payload) {
5556                        content[payload.key]
5557                    }
5558                    fn view(ctx) {
5559                        let data = [];
5560                        let content = #{};
5561                        for index in 0..32 {
5562                            let key = `row-${index}`;
5563                            data.push(#{ key: key });
5564                            content[key] = text(`Rich ${index}`);
5565                        }
5566                        virtual_collection(#{
5567                            key: "rows", label: "Rows", data: data,
5568                            estimated_height: 24, height: 24, overdraw_pixels: 0,
5569                            alignment: "top", follow_tail: false,
5570                        }, Fn("render_item").curry(content))
5571                    }
5572                "#,
5573            )
5574            .unwrap();
5575        let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5576        let root_path = ComponentInstancePath::root("App", "rich");
5577        let mut lifecycle = crate::ScriptLifecycle::new(
5578            compiled,
5579            Rc::clone(&runtime),
5580            root_path.clone(),
5581            None,
5582            BTreeMap::new(),
5583            &ComponentStateSchema::default(),
5584        )
5585        .unwrap();
5586        lifecycle.start(&mut engine).unwrap();
5587        let id = engine
5588            .virtual_collection_ids_in_scope(&root_path)
5589            .into_iter()
5590            .next()
5591            .unwrap();
5592        runtime.borrow().virtual_requests.request(id, [31]);
5593        assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5594        let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5595            unreachable!()
5596        };
5597        assert!(matches!(
5598            spec.realized[&31].kind(),
5599            crate::UiNodeKind::Text { text } if text == "Rich 31"
5600        ));
5601    }
5602
5603    #[test]
5604    #[allow(clippy::too_many_lines)]
5605    fn virtual_row_formal_component_preserves_the_real_callback_owner() {
5606        let mut engine = RuntimeEngine::new();
5607        let compiled = engine
5608            .compile_named(
5609                "virtual-callback-owner",
5610                r#"
5611                    define_component(#{
5612                        metadata: #{ id: "test/action", "export": "Action", version: "0.1.8",
5613                            runtime_api: #{ min_inclusive: 3, max_exclusive: 4 },
5614                            dependencies: [], capabilities: #{} },
5615                        schema: #{ props: #{
5616                                key: #{ schema: #{ type: "string" }, required: true, sensitive: false },
5617                                on_action: #{ schema: #{ type: "callback" }, required: true, sensitive: false },
5618                            }, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
5619                        render: Fn("render_Action")
5620                    });
5621                    fn Action(props) { render_component("test/action", props) }
5622                    fn render_Action(ctx, props) { text("Action").on_click(props.on_action) }
5623                    fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5624                    fn render_item(ctx, payload) {
5625                        Action(#{ key: payload.key, on_action: Fn("increment") })
5626                    }
5627                    fn view(ctx) {
5628                        let data=[];for index in 0..32 {data.push(#{key:`row-${index}`});}
5629                        virtual_collection(#{
5630                            key: "rows", label: "Rows", data: data,
5631                            estimated_height: 24, height: 24, overdraw_pixels: 0,
5632                            alignment: "top", follow_tail: false,
5633                        }, Fn("render_item"))
5634                    }
5635                "#,
5636            )
5637            .unwrap();
5638        let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5639        let root_path = ComponentInstancePath::root("App", "virtual-callback");
5640        let schema = ComponentStateSchema::new(BTreeMap::from([(
5641            "count".to_owned(),
5642            crate::StateField::new(
5643                crate::ValueSchema::Integer {
5644                    min: None,
5645                    max: None,
5646                },
5647                UiValue::Integer(0),
5648            ),
5649        )]))
5650        .unwrap();
5651        let mut lifecycle = crate::ScriptLifecycle::new(
5652            compiled,
5653            Rc::clone(&runtime),
5654            root_path.clone(),
5655            None,
5656            BTreeMap::new(),
5657            &schema,
5658        )
5659        .unwrap();
5660        lifecycle.start(&mut engine).unwrap();
5661        let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5662            unreachable!()
5663        };
5664        let crate::UiEventHandler::Script(callback) =
5665            spec.realized[&0].handlers()["click"][0].handler()
5666        else {
5667            panic!("formal row component must retain its callback");
5668        };
5669        let _ = lifecycle
5670            .invoke_callback_transactional(&engine, callback, UiValue::Null)
5671            .unwrap();
5672        assert_eq!(
5673            runtime.borrow().component_state.get(&root_path, "count"),
5674            Some(&UiValue::Integer(1))
5675        );
5676        let id = engine
5677            .virtual_collection_ids_in_scope(&root_path)
5678            .into_iter()
5679            .next()
5680            .unwrap();
5681        assert!(
5682            !lifecycle
5683                .root()
5684                .unwrap()
5685                .virtual_collection_items(&id)
5686                .unwrap()
5687                .contains_key(&31)
5688        );
5689        runtime.borrow().virtual_requests.request(id.clone(), [31]);
5690        lifecycle.realize_virtual_requests(&mut engine).unwrap();
5691        let callback = lifecycle
5692            .root()
5693            .unwrap()
5694            .virtual_collection_items(&id)
5695            .unwrap()[&31]
5696            .handler("click")
5697            .unwrap()
5698            .as_script()
5699            .unwrap();
5700        let _ = lifecycle
5701            .invoke_callback_transactional(&engine, callback, UiValue::Null)
5702            .unwrap();
5703        assert_eq!(
5704            runtime.borrow().component_state.get(&root_path, "count"),
5705            Some(&UiValue::Integer(2))
5706        );
5707    }
5708
5709    #[test]
5710    fn invalid_state_default_reports_the_field_and_tagged_encoding() {
5711        let mut engine = RuntimeEngine::new();
5712        let compiled = engine.compile(r#"
5713            fn state_schema() {#{fields:#{current_theme:#{schema:#{type:"map",values:#{type:"ui_value"}},
5714                "default":#{type:"map",value:#{family:"Default"}}}}}}
5715            fn view(ctx) {text("view")}
5716        "#).unwrap();
5717        let error = engine.root_state_schema(&compiled).unwrap_err().to_string();
5718        assert!(
5719            error.contains("state_schema.fields.current_theme.default"),
5720            "{error}"
5721        );
5722        assert!(error.contains("recursively tagged UiValue"), "{error}");
5723    }
5724
5725    #[test]
5726    fn delayed_component_callback_starts_with_a_fresh_operation_budget() {
5727        let module = r#"
5728            define_component(#{
5729                metadata: #{ id: "components/counter", "export": "Counter", version: "0.1.1",
5730                    runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5731                schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5732                render: Fn("render_Counter")
5733            });
5734            fn Counter(props) { render_component("components/counter", props) }
5735            fn render_Counter(ctx, props) { text("counter").on_click(Fn("clicked")) }
5736            fn clicked(ctx, payload) {
5737                let n = 0;
5738                for i in 0..100000 { n += 1; }
5739                n
5740            }
5741        "#;
5742        let mut resolver = crate::RestrictedModuleResolver::new();
5743        resolver.insert("components/counter", module).unwrap();
5744        let mut engine = RuntimeEngine::new();
5745        engine.set_module_resolver(resolver);
5746        let compiled = engine
5747            .compile_self_contained_named(
5748                "callback-budget",
5749                r#"import "components/counter" as c;
5750                    fn view(ctx) {
5751                        let n = 0;
5752                        for i in 0..250000 { n += 1; }
5753                        c::Counter(#{} )
5754                    }"#,
5755            )
5756            .unwrap();
5757        let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5758        let mut lifecycle = crate::ScriptLifecycle::new(
5759            compiled,
5760            runtime,
5761            ComponentInstancePath::root("App", "budget"),
5762            None,
5763            BTreeMap::new(),
5764            &ComponentStateSchema::default(),
5765        )
5766        .unwrap();
5767        let root = lifecycle.start(&mut engine).unwrap();
5768        let crate::UiEventHandler::Script(callback) = root.handlers()["click"][0].handler() else {
5769            panic!("component must expose a script click callback");
5770        };
5771        let callback = callback.clone();
5772        assert!(
5773            lifecycle
5774                .invoke_callback_transactional(&engine, &callback, UiValue::Null)
5775                .is_ok()
5776        );
5777    }
5778
5779    #[test]
5780    fn old_component_handles_cannot_target_a_same_key_remount() {
5781        let module = r#"
5782            define_component(#{
5783                metadata: #{ id: "components/incarnation", "export": "Counter", version: "0.1.1",
5784                    runtime_api: #{ min_inclusive: 3, max_exclusive: 4 }, dependencies: [], capabilities: #{} },
5785                schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5786                    state: #{ fields: #{ count: #{ schema: #{ type: "integer" }, "default": #{ type: "integer", value: 0 } } } },
5787                    events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Counter")
5788            });
5789            fn Counter(props) { render_component("components/incarnation", props) }
5790            fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5791            fn render_Counter(ctx, props) {
5792                let progress = signal("progress", 0.0);
5793                text(`${ctx.get_state("count")}`).bind_signal("opacity", progress).on_click(Fn("increment"))
5794            }
5795        "#;
5796        let mut resolver = crate::RestrictedModuleResolver::new();
5797        resolver.insert("components/incarnation", module).unwrap();
5798        let mut engine = RuntimeEngine::new();
5799        engine.set_module_resolver(resolver);
5800        let compiled = engine
5801            .compile_self_contained_named(
5802                "incarnation",
5803                r#"import "components/incarnation" as c;
5804                    fn view(ctx) {
5805                        if ctx.get_state("visible") { c::Counter(#{ key: "same" }) }
5806                        else { text("hidden") }
5807                    }"#,
5808            )
5809            .unwrap();
5810        let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5811        let root_path = ComponentInstancePath::root("App", "incarnation");
5812        let schema = ComponentStateSchema::new(BTreeMap::from([(
5813            "visible".to_owned(),
5814            crate::StateField::new(crate::ValueSchema::Bool, UiValue::Bool(true)),
5815        )]))
5816        .unwrap();
5817        let mut lifecycle = crate::ScriptLifecycle::new(
5818            compiled,
5819            Rc::clone(&runtime),
5820            root_path.clone(),
5821            None,
5822            BTreeMap::new(),
5823            &schema,
5824        )
5825        .unwrap();
5826        lifecycle.start(&mut engine).unwrap();
5827        let component = engine
5828            .component_invocations()
5829            .next()
5830            .unwrap()
5831            .path()
5832            .clone();
5833        let crate::UiEventHandler::Script(old_callback) =
5834            lifecycle.root().unwrap().handlers()["click"][0].handler()
5835        else {
5836            panic!("component must expose its callback");
5837        };
5838        let old_callback = old_callback.clone();
5839        let old_signal = runtime
5840            .borrow()
5841            .signals
5842            .resolve(&component, "progress")
5843            .unwrap();
5844        runtime
5845            .borrow_mut()
5846            .set_component_state_from_host(&root_path, "visible", UiValue::Bool(false))
5847            .unwrap();
5848        lifecycle.render_dirty(&mut engine).unwrap();
5849        runtime
5850            .borrow_mut()
5851            .set_component_state_from_host(&root_path, "visible", UiValue::Bool(true))
5852            .unwrap();
5853        lifecycle.render_dirty(&mut engine).unwrap();
5854        assert!(matches!(
5855            lifecycle.invoke_callback_transactional(&engine, &old_callback, UiValue::Null),
5856            Err(crate::LifecycleError::Runtime(
5857                RuntimeError::StaleComponentCallback { .. }
5858            ))
5859        ));
5860        assert!(matches!(
5861            runtime
5862                .borrow_mut()
5863                .signals
5864                .write(&old_signal, crate::SignalValue::Float(0.5)),
5865            Err(crate::SignalError::Stale(_))
5866        ));
5867    }
5868}