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 = invocation.call::<UiNode>(
1592                    self.engine(),
1593                    &recipe.renderer,
1594                    (
1595                        context.for_virtual_item(id, &key),
1596                        Dynamic::from_map(payload),
1597                    ),
1598                );
1599                self.record_timing(
1600                    ExecutionOperation::VirtualCollection(id.key.clone()),
1601                    id.key.as_str(),
1602                    started,
1603                    item.is_ok(),
1604                );
1605                let mut node = item.map_err(RuntimeError::Evaluate)?.with_key(key);
1606                node.bind_generation(recipe.generation);
1607                node.bind_component_scope(
1608                    recipe.context.callback_component_path(),
1609                    recipe.context.callback_component_incarnation(),
1610                    recipe.context.callback_event_schemas(),
1611                    Some(&recipe.renderer_context),
1612                );
1613                realized.insert(index, node);
1614            }
1615            Ok(realized)
1616        })();
1617        if result.is_err() {
1618            record_component_render_failure(&self.component_render, context.component_path());
1619        }
1620        self.finish_component_render(result.is_ok())?;
1621        result
1622    }
1623
1624    pub(crate) fn update_virtual_collection_snapshot(
1625        &mut self,
1626        id: &crate::VirtualCollectionId,
1627        items: &BTreeMap<usize, UiNode>,
1628    ) {
1629        // Structural collection scopes are not executable component owners.
1630        // Update every containing invocation snapshot so a later parent bailout
1631        // cannot resurrect an older realized window (including nested lists).
1632        let mut path = Some(id.component.clone());
1633        while let Some(current_path) = path {
1634            if let Some(owner) = self.component_invocations.get_mut(&current_path)
1635                && let Some(current) = owner.snapshot.current()
1636                && current.virtual_collection_spec(id).is_some()
1637            {
1638                let mut rendered = current.as_ref().clone();
1639                rendered.replace_virtual_collection_items(id, items.clone());
1640                owner.snapshot.replace_if_active(Rc::new(rendered));
1641            }
1642            path = current_path.parent();
1643        }
1644    }
1645
1646    /// Invoke an optional one-argument lifecycle function.
1647    ///
1648    /// # Errors
1649    ///
1650    /// Returns [`RuntimeError::Evaluate`] when the function exists but fails.
1651    pub fn call_optional_lifecycle(
1652        &self,
1653        compiled: &CompiledUi,
1654        function: &str,
1655        context: UiContext,
1656    ) -> Result<bool, RuntimeError> {
1657        if !compiled.has_function(function, 1) {
1658            return Ok(false);
1659        }
1660        self.evaluation_generation.set(compiled.generation);
1661        self.begin_execution_session();
1662        let started = self.begin_timing();
1663        let result = AstInterpreter::call_fn::<Dynamic, _>(
1664            &self.engine,
1665            &compiled.ast,
1666            &mut Scope::new(),
1667            function,
1668            (context,),
1669        );
1670        self.record_timing(
1671            ExecutionOperation::Lifecycle(function.to_owned()),
1672            compiled.ast.source().unwrap_or("<script>"),
1673            started,
1674            result.is_ok(),
1675        );
1676        let _ = result.map_err(RuntimeError::Evaluate)?;
1677        Ok(true)
1678    }
1679
1680    pub(crate) fn call_optional_lifecycle_with_value(
1681        &self,
1682        compiled: &CompiledUi,
1683        function: &str,
1684        context: UiContext,
1685        value: UiValue,
1686    ) -> Result<bool, RuntimeError> {
1687        if !compiled.has_function(function, 2) {
1688            return Ok(false);
1689        }
1690        self.evaluation_generation.set(compiled.generation);
1691        self.begin_execution_session();
1692        let started = self.begin_timing();
1693        let result = AstInterpreter::call_fn::<Dynamic, _>(
1694            &self.engine,
1695            &compiled.ast,
1696            &mut Scope::new(),
1697            function,
1698            (context, value.into_dynamic()),
1699        );
1700        self.record_timing(
1701            ExecutionOperation::Lifecycle(function.to_owned()),
1702            compiled.ast.source().unwrap_or("<script>"),
1703            started,
1704            result.is_ok(),
1705        );
1706        let _ = result.map_err(RuntimeError::Evaluate)?;
1707        Ok(true)
1708    }
1709
1710    /// Decode optional root `state_schema()` from an application entry.
1711    ///
1712    /// # Errors
1713    ///
1714    /// Returns [`RuntimeError::Evaluate`] when the function fails or cannot be
1715    /// decoded as [`ComponentStateSchema`].
1716    pub fn root_state_schema(
1717        &self,
1718        compiled: &CompiledUi,
1719    ) -> Result<ComponentStateSchema, RuntimeError> {
1720        if !compiled.has_function("state_schema", 0) {
1721            return Ok(ComponentStateSchema::default());
1722        }
1723        self.begin_execution_session();
1724        let started = self.begin_timing();
1725        let result = (|| {
1726            let raw: Dynamic = self
1727                .engine
1728                .call_fn(&mut Scope::new(), &compiled.ast, "state_schema", ())
1729                .map_err(RuntimeError::Evaluate)?;
1730            validate_state_default_encoding(&raw, "state_schema")
1731                .map_err(RuntimeError::Evaluate)?;
1732            rhai::serde::from_dynamic(&raw).map_err(RuntimeError::Evaluate)
1733        })();
1734        self.record_timing(
1735            ExecutionOperation::Lifecycle("state_schema".to_owned()),
1736            compiled.ast.source().unwrap_or("<script>"),
1737            started,
1738            result.is_ok(),
1739        );
1740        result
1741    }
1742
1743    /// Create a generation-bound callback for a script function.
1744    ///
1745    /// # Errors
1746    ///
1747    /// Returns [`RuntimeError::CallbackDefinition`] when the function name is
1748    /// not a valid Rhai function pointer name.
1749    pub fn callback(
1750        &self,
1751        compiled: &CompiledUi,
1752        function_name: &str,
1753    ) -> Result<ScriptCallback, RuntimeError> {
1754        let function =
1755            FnPtr::new(function_name).map_err(|source| RuntimeError::CallbackDefinition {
1756                name: function_name.to_owned(),
1757                source,
1758            })?;
1759        ScriptCallback::try_from_fn_ptr(function, compiled.generation).map_err(|source| {
1760            RuntimeError::RetainedCallback {
1761                name: function_name.to_owned(),
1762                source,
1763            }
1764        })
1765    }
1766
1767    /// Invoke a callback only when it belongs to the current compiled generation.
1768    ///
1769    /// # Errors
1770    ///
1771    /// Returns [`RuntimeError::StaleCallback`] for obsolete callbacks and
1772    /// [`RuntimeError::Evaluate`] when the Rhai function fails.
1773    pub fn invoke_callback(
1774        &self,
1775        compiled: &CompiledUi,
1776        callback: &ScriptCallback,
1777        args: impl FuncArgs,
1778    ) -> Result<Dynamic, RuntimeError> {
1779        if !self.is_current(callback.generation) || callback.generation != compiled.generation {
1780            return Err(RuntimeError::StaleCallback {
1781                name: callback.name().to_owned(),
1782                callback_generation: callback.generation,
1783                current_generation: self.generation,
1784            });
1785        }
1786
1787        self.invoke_callback_for_generation(compiled, callback, args)
1788    }
1789
1790    pub(crate) fn invoke_callback_for_generation(
1791        &self,
1792        compiled: &CompiledUi,
1793        callback: &ScriptCallback,
1794        args: impl FuncArgs,
1795    ) -> Result<Dynamic, RuntimeError> {
1796        if callback.generation != compiled.generation {
1797            return Err(RuntimeError::StaleCallback {
1798                name: callback.name().to_owned(),
1799                callback_generation: callback.generation,
1800                current_generation: compiled.generation,
1801            });
1802        }
1803        self.evaluation_generation.set(compiled.generation);
1804        let operation_base = callback.native_context.as_ref().map_or(
1805            0,
1806            crate::invocation::ScriptInvocationContext::operation_base,
1807        );
1808        self.begin_execution_session_from(operation_base);
1809        let started = self.begin_timing();
1810        #[allow(deprecated)]
1811        let result = if let Some(context) = callback.native_context.as_ref() {
1812            context.call(self.engine(), &callback.function, args)
1813        } else {
1814            callback.function.call(self.engine(), &compiled.ast, args)
1815        };
1816        self.record_timing(
1817            ExecutionOperation::Callback(callback.name().to_owned()),
1818            compiled.ast.source().unwrap_or("<script>"),
1819            started,
1820            result.is_ok(),
1821        );
1822        result.map_err(RuntimeError::Evaluate)
1823    }
1824
1825    #[must_use]
1826    pub fn is_current(&self, generation: ScriptGeneration) -> bool {
1827        generation == self.generation
1828    }
1829
1830    #[must_use]
1831    pub fn engine(&self) -> &Engine {
1832        &self.engine
1833    }
1834
1835    /// Generate the official Rhai language-server definition source for every
1836    /// function/type/module registered on this engine.
1837    ///
1838    /// Standard packages are omitted because the language server supplies them.
1839    #[must_use]
1840    pub fn definition_source(&self) -> String {
1841        self.engine
1842            .definitions()
1843            .include_standard_packages(false)
1844            .single_file()
1845    }
1846
1847    /// Validate statically visible direct and method call arities in an entry
1848    /// script and its installed modules.
1849    ///
1850    /// This complements real lifecycle execution because Rhai resolves
1851    /// functions dynamically and an initial render cannot cover every branch.
1852    /// The catalog comes from the same registered Engine metadata used by
1853    /// [`Self::definition_source`].
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns a metadata or parse error if the lint catalog cannot be built.
1858    pub fn lint_known_calls(
1859        &mut self,
1860        entry_name: &str,
1861        entry_source: &str,
1862        modules: &BTreeMap<ModuleId, String>,
1863    ) -> Result<Vec<crate::KnownCallDiagnostic>, crate::KnownCallLintError> {
1864        crate::script_lint::lint_known_calls(&mut self.engine, entry_name, entry_source, modules)
1865    }
1866
1867    /// Return a snapshot of components exported through this engine.
1868    ///
1869    /// # Errors
1870    ///
1871    /// Returns [`ComponentExportError`] if the export registry was poisoned by
1872    /// a prior panic.
1873    pub fn component_exports(&self) -> Result<ComponentRegistry, ComponentExportError> {
1874        self.component_exports.snapshot()
1875    }
1876
1877    pub fn component_invocations(
1878        &self,
1879    ) -> impl ExactSizeIterator<Item = &ComponentInvocationRecipe> {
1880        self.component_invocations.values()
1881    }
1882
1883    pub(crate) fn component_renderer_snapshot(
1884        &self,
1885    ) -> Result<BTreeMap<ModuleId, RegisteredComponentRender>, ComponentExportError> {
1886        self.component_renderers
1887            .try_borrow()
1888            .map(|renderers| renderers.clone())
1889            .map_err(|_| ComponentExportError::Borrowed)
1890    }
1891
1892    pub(crate) fn restore_component_renderers(
1893        &self,
1894        renderers: BTreeMap<ModuleId, RegisteredComponentRender>,
1895    ) -> Result<(), ComponentExportError> {
1896        *self
1897            .component_renderers
1898            .try_borrow_mut()
1899            .map_err(|_| ComponentExportError::Borrowed)? = renderers;
1900        Ok(())
1901    }
1902
1903    /// Clear component exports before evaluating a hot-reload candidate.
1904    ///
1905    /// # Errors
1906    ///
1907    /// Returns [`ComponentExportError`] if the collector is unavailable.
1908    pub fn clear_component_exports(&self) -> Result<(), ComponentExportError> {
1909        self.component_renderers
1910            .try_borrow_mut()
1911            .map_err(|_| ComponentExportError::Borrowed)?
1912            .clear();
1913        self.component_exports.clear()
1914    }
1915
1916    /// Restore a prior export snapshot after a rejected candidate.
1917    ///
1918    /// # Errors
1919    ///
1920    /// Returns [`ComponentExportError`] if the collector is unavailable.
1921    pub fn restore_component_exports(
1922        &self,
1923        registry: ComponentRegistry,
1924    ) -> Result<(), ComponentExportError> {
1925        self.component_exports.replace(registry)
1926    }
1927
1928    /// Register a namespaced Rust custom primitive and its Rhai constructor.
1929    ///
1930    /// # Errors
1931    ///
1932    /// Returns [`PrimitiveError`] when the descriptor conflicts or is invalid.
1933    pub fn register_primitive(
1934        &mut self,
1935        descriptor: PrimitiveDescriptor,
1936        handler: impl PrimitiveHandler + 'static,
1937    ) -> Result<(), PrimitiveError> {
1938        self.primitives.register(descriptor.clone(), handler)?;
1939
1940        let namespace = descriptor.id.namespace().to_owned();
1941        let export = descriptor.export.clone();
1942        let id = descriptor.id;
1943        let primitives = self.primitives.clone();
1944        let generation = Rc::clone(&self.evaluation_generation);
1945        let module = self.primitive_modules.entry(namespace.clone()).or_default();
1946        FuncRegistration::new(export).set_into_module(
1947            module,
1948            move |mut props: Map| -> Result<UiNode, Box<EvalAltResult>> {
1949                let key = props
1950                    .remove("key")
1951                    .map(|value| {
1952                        if value.is::<ImmutableString>() {
1953                            Ok(value.cast::<ImmutableString>().to_string())
1954                        } else {
1955                            Err(Box::new(EvalAltResult::ErrorRuntime(
1956                                "primitive key must be a string".into(),
1957                                Position::NONE,
1958                            )))
1959                        }
1960                    })
1961                    .transpose()?;
1962                primitives
1963                    .create_node(&id, key, &props, generation.get())
1964                    .map_err(|error| {
1965                        Box::new(EvalAltResult::ErrorRuntime(
1966                            error.to_string().into(),
1967                            Position::NONE,
1968                        ))
1969                    })
1970            },
1971        );
1972        self.engine
1973            .register_static_module(namespace, module.clone().into());
1974        Ok(())
1975    }
1976
1977    #[must_use]
1978    pub fn primitive_registry(&self) -> PrimitiveRegistry {
1979        self.primitives.clone()
1980    }
1981
1982    /// Register a trusted Rust handler attachable by Rhai through `native_handler`.
1983    ///
1984    /// # Errors
1985    ///
1986    /// Returns descriptor/duplicate registry errors.
1987    pub fn register_native_handler(
1988        &self,
1989        descriptor: crate::NativeHandlerDescriptor,
1990        handler: impl FnMut(
1991            crate::NativeEvent,
1992            &mut UiRuntimeState,
1993            &mut gpui::Window,
1994            &mut gpui::App,
1995        ) -> Result<crate::EventResponse, String>
1996        + 'static,
1997    ) -> Result<(), crate::NativeHandlerError> {
1998        self.native_handlers.register(descriptor, handler)
1999    }
2000
2001    #[must_use]
2002    pub fn native_handler_registry(&self) -> crate::NativeHandlerRegistry {
2003        self.native_handlers.clone()
2004    }
2005
2006    #[must_use]
2007    pub fn syntax_registry(&self) -> crate::SyntaxRegistry {
2008        self.syntax_registry.clone()
2009    }
2010
2011    #[must_use]
2012    pub fn document_runtime_config(&self) -> crate::DocumentRuntimeConfig {
2013        self.document_runtime.clone()
2014    }
2015
2016    /// Register one trusted Rust chart transform.
2017    ///
2018    /// # Errors
2019    ///
2020    /// Returns an invalid or duplicate transform ID diagnostic.
2021    #[cfg(feature = "charts")]
2022    pub fn register_chart_transform(
2023        &self,
2024        id: impl Into<String>,
2025        transform: impl crate::HostChartTransform + 'static,
2026    ) -> Result<(), crate::ChartTransformError> {
2027        self.chart_transforms.register(id, transform)
2028    }
2029
2030    /// Register Host-owned GeoJSON/SVG geometry or typed map features.
2031    ///
2032    /// # Errors
2033    ///
2034    /// Returns an invalid or duplicate map diagnostic.
2035    #[cfg(feature = "charts")]
2036    pub fn register_chart_map(&self, map: crate::ChartGeoMap) -> Result<(), crate::ChartGeoError> {
2037        self.chart_geo.register_map(map)
2038    }
2039
2040    /// Register one trusted Rust geo projection.
2041    ///
2042    /// # Errors
2043    ///
2044    /// Returns an invalid or duplicate projection ID diagnostic.
2045    #[cfg(feature = "charts")]
2046    pub fn register_chart_projection(
2047        &self,
2048        id: impl Into<String>,
2049        projection: impl crate::ChartGeoProjection + 'static,
2050    ) -> Result<(), crate::ChartGeoError> {
2051        self.chart_geo.register_projection(id, projection)
2052    }
2053
2054    /// Register one trusted Rust custom chart series renderer.
2055    ///
2056    /// # Errors
2057    ///
2058    /// Returns an invalid or duplicate renderer ID diagnostic.
2059    #[cfg(feature = "charts")]
2060    pub fn register_chart_series(
2061        &self,
2062        id: impl Into<String>,
2063        renderer: impl crate::HostChartSeries + 'static,
2064    ) -> Result<(), crate::ChartSeriesExtensionError> {
2065        self.chart_series.register(id, renderer)
2066    }
2067
2068    /// Register one trusted Rust chart value formatter.
2069    ///
2070    /// # Errors
2071    ///
2072    /// Returns an invalid or duplicate formatter ID diagnostic.
2073    #[cfg(feature = "charts")]
2074    pub fn register_chart_formatter(
2075        &self,
2076        id: impl Into<String>,
2077        formatter: impl crate::HostChartFormatter + 'static,
2078    ) -> Result<(), crate::ChartFormatterError> {
2079        self.chart_formatters.register(id, formatter)
2080    }
2081
2082    #[cfg(feature = "charts")]
2083    #[must_use]
2084    pub fn chart_transform_registry(&self) -> crate::ChartTransformRegistry {
2085        self.chart_transforms.clone()
2086    }
2087
2088    #[cfg(feature = "charts")]
2089    #[must_use]
2090    pub fn chart_geo_registry(&self) -> crate::ChartGeoRegistry {
2091        self.chart_geo.clone()
2092    }
2093
2094    #[cfg(feature = "charts")]
2095    #[must_use]
2096    pub fn chart_series_registry(&self) -> crate::ChartSeriesRegistry {
2097        self.chart_series.clone()
2098    }
2099
2100    #[cfg(feature = "charts")]
2101    #[must_use]
2102    pub fn chart_formatter_registry(&self) -> crate::ChartFormatterRegistry {
2103        self.chart_formatters.clone()
2104    }
2105
2106    pub fn set_slow_threshold(&mut self, threshold: Duration) {
2107        self.slow_threshold = threshold;
2108    }
2109
2110    /// Set the per-execution script operation budget.
2111    ///
2112    /// The default is [`DEFAULT_SCRIPT_OPERATION_LIMIT`]. This is a cumulative
2113    /// operation quota for each execution round, not a frame-time guarantee.
2114    /// A host that runs known one-shot, cacheable
2115    /// script phases (a layout pass, an analysis sweep) may raise it; the
2116    /// wall-clock slow threshold still flags anything that stalls the
2117    /// foreground. Scripts cannot change this limit; only the embedding
2118    /// application can.
2119    /// Zero normalizes to one; it never disables protection. Changes apply to
2120    /// subsequent calls. Native Rust work is not preempted by this quota.
2121    pub fn set_operation_limit(&mut self, limit: u64) {
2122        self.operation_limit.set(limit.max(1));
2123    }
2124
2125    /// The per-execution script operation budget in force.
2126    #[must_use]
2127    pub fn operation_limit(&self) -> u64 {
2128        self.operation_limit.get()
2129    }
2130
2131    /// Configure parser depth independently of execution/recursion/data limits.
2132    /// Defaults are global=64/function=32 in both debug and release. Zero
2133    /// normalizes to one rather than adopting Rhai's unlimited convention.
2134    pub fn set_expression_depth_limits(&mut self, global: usize, functions: usize) {
2135        self.engine
2136            .set_max_expr_depths(global.max(1), functions.max(1));
2137    }
2138
2139    #[must_use]
2140    pub fn expression_depth_limits(&self) -> (usize, usize) {
2141        (
2142            self.engine.max_expr_depth(),
2143            self.engine.max_function_expr_depth(),
2144        )
2145    }
2146
2147    #[must_use]
2148    pub fn take_timings(&self) -> Vec<ExecutionTiming> {
2149        std::mem::take(&mut *self.timings.borrow_mut())
2150    }
2151
2152    /// Return the failed timing from the current execution session, if any.
2153    /// Starting the next compile, render, callback, or lifecycle call clears it.
2154    #[must_use]
2155    pub fn last_failed_timing(&self) -> Option<ExecutionTiming> {
2156        self.last_failed_timing.borrow().clone()
2157    }
2158
2159    #[must_use]
2160    pub(crate) fn last_failed_component(&self) -> Option<ComponentInstancePath> {
2161        self.last_failed_component.borrow().clone()
2162    }
2163
2164    fn record_timing(
2165        &self,
2166        operation: ExecutionOperation,
2167        source: &str,
2168        started: ExecutionTimingStart,
2169        succeeded: bool,
2170    ) {
2171        let duration = started.instant.elapsed();
2172        let operations = self.operation_total().saturating_sub(started.operations);
2173        let timing = ExecutionTiming {
2174            operation,
2175            source: source.to_owned(),
2176            duration,
2177            operations,
2178            operation_limit: started.operation_limit,
2179            round_operations: self.operation_total(),
2180            operation_semantics: OPERATION_SEMANTICS_VERSION,
2181            slow: duration >= self.slow_threshold,
2182            succeeded,
2183        };
2184        if !succeeded {
2185            *self.last_failed_timing.borrow_mut() = Some(timing.clone());
2186        }
2187        self.timings.borrow_mut().push(timing);
2188    }
2189
2190    pub(crate) fn begin_execution_session(&self) {
2191        self.begin_execution_session_from(0);
2192    }
2193
2194    fn begin_execution_session_from(&self, inherited_base: u64) {
2195        self.last_failed_timing.borrow_mut().take();
2196        self.last_failed_component.borrow_mut().take();
2197        self.operation_tracker.begin(inherited_base);
2198    }
2199
2200    fn align_execution_session_to(&self, inherited_base: u64) {
2201        self.operation_tracker.align(inherited_base);
2202    }
2203
2204    fn operation_total(&self) -> u64 {
2205        self.operation_tracker.total.get()
2206    }
2207
2208    fn begin_timing(&self) -> ExecutionTimingStart {
2209        ExecutionTimingStart {
2210            instant: Instant::now(),
2211            operations: self.operation_total(),
2212            operation_limit: self.operation_limit(),
2213        }
2214    }
2215
2216    fn candidate_generation(&mut self) -> ScriptGeneration {
2217        self.preparation_generation
2218            .unwrap_or_else(Self::allocate_generation)
2219    }
2220
2221    fn allocate_generation() -> ScriptGeneration {
2222        static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
2223        ScriptGeneration(NEXT_GENERATION.fetch_add(1, Ordering::Relaxed))
2224    }
2225}
2226
2227pub(crate) fn validate_assignment_targets(ast: &AST) -> Result<(), RuntimeError> {
2228    let mut invalid = None;
2229    ast.walk(&mut |path| {
2230        let Some(ASTNode::Stmt(Stmt::Assignment(assignment))) = path.last() else {
2231            return true;
2232        };
2233        if !matches!(
2234            assignment.1.lhs,
2235            Expr::ThisPtr(_) | Expr::Variable(..) | Expr::Index(..) | Expr::Dot(..)
2236        ) {
2237            invalid = Some(assignment.1.lhs.position());
2238            return false;
2239        }
2240        true
2241    });
2242    invalid.map_or(Ok(()), |position| {
2243        Err(RuntimeError::InvalidAssignmentTarget(position))
2244    })
2245}
2246
2247fn validate_literal_imports(ast: &AST) -> Result<(), RuntimeError> {
2248    let mut dynamic = None;
2249    ast.walk(&mut |path| {
2250        let Some(ASTNode::Stmt(Stmt::Import(import, position))) = path.last() else {
2251            return true;
2252        };
2253        if matches!(import.0, Expr::StringConstant(..)) {
2254            true
2255        } else {
2256            dynamic = Some(*position);
2257            false
2258        }
2259    });
2260    dynamic.map_or(Ok(()), |position| {
2261        Err(RuntimeError::Import(format!(
2262            "Rhai imports must use a literal module string at {position}"
2263        )))
2264    })
2265}
2266
2267fn register_node_apis(engine: &mut Engine) {
2268    FuncRegistration::new("text")
2269        .in_global_namespace()
2270        .register_into_engine(engine, text_node);
2271    FuncRegistration::new("text")
2272        .in_global_namespace()
2273        .register_into_engine(engine, rich_text_node);
2274    FuncRegistration::new("span")
2275        .in_global_namespace()
2276        .register_into_engine(engine, span_value);
2277    FuncRegistration::new("canvas")
2278        .in_global_namespace()
2279        .register_into_engine(engine, canvas_node);
2280    FuncRegistration::new("svg")
2281        .in_global_namespace()
2282        .register_into_engine(engine, svg_node);
2283    FuncRegistration::new("box")
2284        .in_global_namespace()
2285        .register_into_engine(engine, box_node);
2286    FuncRegistration::new("fragment")
2287        .in_global_namespace()
2288        .register_into_engine(engine, fragment_node);
2289    FuncRegistration::new("motion_group")
2290        .in_global_namespace()
2291        .register_into_engine(engine, motion_group_node);
2292    FuncRegistration::new("stack")
2293        .in_global_namespace()
2294        .register_into_engine(engine, stack_node);
2295    FuncRegistration::new("handled")
2296        .in_global_namespace()
2297        .register_into_engine(engine, || crate::EventResponse::new().stop());
2298    FuncRegistration::new("propagate")
2299        .in_global_namespace()
2300        .register_into_engine(engine, crate::EventResponse::new);
2301    FuncRegistration::new("event_response")
2302        .in_global_namespace()
2303        .register_into_engine(engine, crate::EventResponse::new);
2304    FuncRegistration::new("column")
2305        .in_global_namespace()
2306        .register_into_engine(engine, column_node);
2307    FuncRegistration::new("row")
2308        .in_global_namespace()
2309        .register_into_engine(engine, row_node);
2310    FuncRegistration::new("error_boundary")
2311        .in_global_namespace()
2312        .register_into_engine(engine, error_boundary_node);
2313    FuncRegistration::new("error_boundary_lazy")
2314        .in_global_namespace()
2315        .register_into_engine(engine, lazy_error_boundary_node);
2316    FuncRegistration::new("asset")
2317        .in_global_namespace()
2318        .register_into_engine(engine, asset_id_from_script);
2319    FuncRegistration::new("image")
2320        .in_global_namespace()
2321        .register_into_engine(engine, image_node);
2322    FuncRegistration::new("image")
2323        .in_global_namespace()
2324        .register_into_engine(engine, asset_image_node);
2325    FuncRegistration::new("directional_image")
2326        .in_global_namespace()
2327        .register_into_engine(engine, directional_image_node);
2328    FuncRegistration::new("directional_image")
2329        .in_global_namespace()
2330        .register_into_engine(engine, directional_asset_image_node);
2331    FuncRegistration::new("image_source")
2332        .in_global_namespace()
2333        .register_into_engine(engine, generic_image_node);
2334    FuncRegistration::new("directional_image_source")
2335        .in_global_namespace()
2336        .register_into_engine(engine, generic_directional_image_node);
2337    FuncRegistration::new("overlay")
2338        .in_global_namespace()
2339        .register_into_engine(engine, overlay_node);
2340    FuncRegistration::new("layer")
2341        .in_global_namespace()
2342        .register_into_engine(engine, layer_node);
2343}
2344
2345fn register_native_handler_api(engine: &mut Engine, registry: &crate::NativeHandlerRegistry) {
2346    let registry = registry.clone();
2347    FuncRegistration::new("native_handler")
2348        .in_global_namespace()
2349        .register_into_engine(
2350            engine,
2351            move |id: ImmutableString| -> Result<crate::NativeHandlerRef, Box<EvalAltResult>> {
2352                let id = crate::NativeHandlerId::parse(id.to_string())
2353                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2354                registry
2355                    .resolve(&id)
2356                    .map_err(|error| Box::new(component_render_error(error.to_string())))
2357            },
2358        );
2359}
2360
2361pub(crate) const RHAI_MAX_ARRAY_SIZE: usize = 10_000;
2362pub(crate) const RHAI_MAX_MAP_SIZE: usize = 100_000;
2363pub(crate) const RHAI_MAX_STRING_SIZE: usize = 1_048_576;
2364pub(crate) const RHAI_MAX_DATA_DEPTH: usize = 64;
2365
2366fn configure_engine_limits(engine: &mut Engine) {
2367    engine.set_max_call_levels(64);
2368    // Explicit defaults avoid Rhai's profile-dependent parser defaults. A
2369    // trusted Host may configure these separately; ordinary applications do
2370    // not inherit another application's deeper expression policy.
2371    engine.set_max_expr_depths(64, 32);
2372    // Rhai's counter is evaluator-local and cloned into stored callback
2373    // contexts. The runtime's progress adapter enforces one cumulative budget
2374    // across nested evaluators and starts delayed callbacks with fresh quota.
2375    engine.set_max_operations(0);
2376    engine.set_max_array_size(RHAI_MAX_ARRAY_SIZE);
2377    engine.set_max_map_size(RHAI_MAX_MAP_SIZE);
2378    engine.set_max_string_size(RHAI_MAX_STRING_SIZE);
2379}
2380
2381fn register_define_component_api(
2382    engine: &mut Engine,
2383    exports: &ComponentExportCollector,
2384    renderers: &ComponentRenderRegistry,
2385    generation: &Rc<Cell<ScriptGeneration>>,
2386) {
2387    let exports = exports.clone();
2388    let renderers = Rc::clone(renderers);
2389    let generation = Rc::clone(generation);
2390    FuncRegistration::new("define_component")
2391        .in_global_namespace()
2392        .register_into_engine(
2393            engine,
2394            move |mut raw: Map| -> Result<(), Box<EvalAltResult>> {
2395                let render = raw.remove("render").ok_or_else(|| {
2396                    Box::new(component_render_error(
2397                        "define_component requires a named `render` function",
2398                    ))
2399                })?;
2400                if !render.is::<FnPtr>() {
2401                    return Err(Box::new(component_render_error(
2402                        "define_component `render` must be a function pointer",
2403                    )));
2404                }
2405                let render = render.cast::<FnPtr>();
2406                if render.is_anonymous() || render.is_curried() {
2407                    return Err(Box::new(component_render_error(
2408                        "formal component render must be an uncurried named function",
2409                    )));
2410                }
2411                if let Some(schema) = raw.get("schema")
2412                    && schema.is::<Map>()
2413                    && let Some(state) = schema.clone_cast::<Map>().get("state")
2414                {
2415                    validate_state_default_encoding(state, "component.schema.state")?;
2416                }
2417                let decoded: crate::ComponentDefinition =
2418                    rhai::serde::from_dynamic(&Dynamic::from_map(raw))?;
2419                let definition = crate::ComponentDefinition::new(decoded.metadata, decoded.schema)
2420                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2421                let id = definition.metadata.id.clone();
2422                exports
2423                    .register_definition(definition)
2424                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2425                renderers
2426                    .try_borrow_mut()
2427                    .map_err(|_| {
2428                        Box::new(component_render_error(
2429                            "component render registry is already borrowed",
2430                        ))
2431                    })?
2432                    .insert(
2433                        id,
2434                        RegisteredComponentRender {
2435                            generation: generation.get(),
2436                            render,
2437                        },
2438                    );
2439                Ok(())
2440            },
2441        );
2442}
2443
2444fn validate_state_default_encoding(raw: &Dynamic, path: &str) -> Result<(), Box<EvalAltResult>> {
2445    if !raw.is::<Map>() {
2446        return Ok(());
2447    }
2448    let raw = raw.clone_cast::<Map>();
2449    let Some(fields) = raw.get("fields").filter(|fields| fields.is::<Map>()) else {
2450        return Ok(());
2451    };
2452    for (name, field) in fields.clone_cast::<Map>() {
2453        if !field.is::<Map>() {
2454            continue;
2455        }
2456        let field = field.cast::<Map>();
2457        if let Some(value) = field.get("default") {
2458            rhai::serde::from_dynamic::<UiValue>(value).map_err(|source| {
2459                Box::new(component_render_error(format!(
2460                    "{path}.fields.{name}.default: expected recursively tagged UiValue; map entries and array items each require type/value descriptors ({source})"
2461                )))
2462            })?;
2463        }
2464    }
2465    Ok(())
2466}
2467
2468fn register_component_runtime_apis(
2469    engine: &mut Engine,
2470    exports: &ComponentExportCollector,
2471    generation: &Rc<Cell<ScriptGeneration>>,
2472) -> (ActiveComponentRenderState, ComponentRenderRegistry) {
2473    let active = Rc::new(RefCell::new(None));
2474    let renderers = Rc::new(RefCell::new(BTreeMap::new()));
2475    register_define_component_api(engine, exports, &renderers, generation);
2476    register_render_component_api(engine, exports, &renderers, &active);
2477    register_effect_api(engine, &active);
2478    register_timer_api(engine, &active);
2479    register_signal_api(engine, &active);
2480    register_element_ref_api(engine, &active);
2481    register_virtual_collection_api(engine, &active);
2482    (active, renderers)
2483}
2484
2485fn register_render_component_api(
2486    engine: &mut Engine,
2487    exports: &ComponentExportCollector,
2488    renderers: &ComponentRenderRegistry,
2489    active: &ActiveComponentRenderState,
2490) {
2491    let exports = exports.clone();
2492    let renderers = Rc::clone(renderers);
2493    let active = Rc::clone(active);
2494    FuncRegistration::new("render_component")
2495        .in_global_namespace()
2496        .register_into_engine(
2497            engine,
2498            move |call: rhai::NativeCallContext<'_>,
2499                  id: ImmutableString,
2500                  props: Map|
2501                  -> Result<UiNode, Box<EvalAltResult>> {
2502                let id = ModuleId::parse(id.to_string())
2503                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
2504                let generation = active
2505                    .try_borrow()
2506                    .map_err(|_| {
2507                        Box::new(component_render_error(
2508                            "component render stack is already borrowed",
2509                        ))
2510                    })?
2511                    .as_ref()
2512                    .ok_or_else(|| {
2513                        Box::new(component_render_error(
2514                            "render_component may run only inside view",
2515                        ))
2516                    })?
2517                    .generation;
2518                let registered = renderers
2519                    .try_borrow()
2520                    .map_err(|_| {
2521                        Box::new(component_render_error(
2522                            "component render registry is already borrowed",
2523                        ))
2524                    })?
2525                    .get(&id)
2526                    .cloned()
2527                    .ok_or_else(|| {
2528                        Box::new(component_render_error(format!(
2529                            "component `{id}` has no registered render function"
2530                        )))
2531                    })?;
2532                if registered.generation != generation {
2533                    return Err(Box::new(component_render_error(format!(
2534                        "component `{id}` render belongs to stale generation {}",
2535                        registered.generation
2536                    ))));
2537                }
2538                execute_component_render(
2539                    &call,
2540                    &exports,
2541                    &active,
2542                    id.as_str().into(),
2543                    props,
2544                    &registered.render,
2545                )
2546            },
2547        );
2548}
2549
2550#[allow(clippy::too_many_lines)] // One cohesive Rhai component transaction; splitting hides ordering.
2551fn execute_component_render(
2552    call: &rhai::NativeCallContext<'_>,
2553    exports: &ComponentExportCollector,
2554    active: &ActiveComponentRenderState,
2555    id: ImmutableString,
2556    props: Map,
2557    render: &FnPtr,
2558) -> Result<UiNode, Box<EvalAltResult>> {
2559    let generation = active
2560        .try_borrow()
2561        .map_err(|_| {
2562            Box::new(component_render_error(
2563                "component render stack is already borrowed",
2564            ))
2565        })?
2566        .as_ref()
2567        .ok_or_else(|| {
2568            Box::new(component_render_error(
2569                "formal component rendering may run only inside view",
2570            ))
2571        })?
2572        .generation;
2573    let (component, mut invocation) = resolve_component_invocation(exports, id, props, generation)?;
2574    if render.is_anonymous() || render.is_curried() {
2575        return Err(Box::new(component_render_error(
2576            "formal component render must be an uncurried named function",
2577        )));
2578    }
2579    let part_styles = component_part_styles(&invocation.props);
2580    let recipe_props = invocation.retained_props.clone();
2581    let declared_key = invocation.key.clone();
2582    let render_recipe = render.clone();
2583    let (path, caller_context) = reserve_component_path(call, &component, &invocation, active)?;
2584    if let Some(node) = try_reuse_component_subtree(
2585        active,
2586        &path,
2587        &caller_context,
2588        &component,
2589        &invocation,
2590        render,
2591    )? {
2592        return Ok(node);
2593    }
2594    let native_context = crate::invocation::ScriptInvocationContext::capture_retained(call);
2595    let caller_binding = caller_component_binding(call, &caller_context);
2596    bind_component_callback_props(
2597        &mut invocation.props,
2598        &component.schema.props,
2599        &caller_binding,
2600    );
2601    bind_component_node_props(
2602        &mut invocation.props,
2603        &component.schema.props,
2604        &caller_binding,
2605    );
2606    let event_callbacks = component_event_callbacks(&component, &invocation.props);
2607    let recipe_event_callbacks = event_callbacks.clone();
2608    let script_props = invocation.props.clone();
2609    let context = enter_component_render(
2610        &path,
2611        &component,
2612        &invocation,
2613        native_context.clone(),
2614        active,
2615    )?;
2616    let recipe_component_context = context.clone();
2617    let result = render.call_within_context::<UiNode>(call, (context, invocation.props));
2618    if result.is_err() {
2619        record_component_render_failure(active, &path);
2620    }
2621    register_component_event_callbacks(
2622        active,
2623        &path,
2624        &caller_context,
2625        event_callbacks,
2626        &native_context,
2627    )?;
2628    let (environment, reusable) = component_render_metadata(active, &recipe_component_context)?;
2629    leave_component_render(active)?;
2630    let mut node = result?;
2631    node = node.with_owned_part_styles(part_styles.clone());
2632    let snapshot = crate::node::ComponentOwnedSnapshot::default();
2633    node = node.with_component_root_snapshot(path.clone(), snapshot.reference());
2634    node.bind_component_scope(
2635        &path,
2636        recipe_component_context.component_incarnation(),
2637        &component.schema.events,
2638        Some(&native_context),
2639    );
2640    register_component_invocation(
2641        active,
2642        ComponentInvocationRecipe {
2643            path: path.clone(),
2644            parent: caller_context.component_path().clone(),
2645            component: component.metadata.id.clone(),
2646            export: component.metadata.export.clone(),
2647            declared_key,
2648            props: recipe_props,
2649            script_props,
2650            component_context: recipe_component_context,
2651            caller_context: caller_context.clone(),
2652            part_styles,
2653            event_callbacks: recipe_event_callbacks,
2654            component_events: component.schema.events.clone(),
2655            declared_effects: component.schema.effects.clone(),
2656            render: render_recipe,
2657            snapshot,
2658            context: native_context.clone(),
2659            generation,
2660            environment,
2661            reusable,
2662        },
2663    )?;
2664    Ok(node)
2665}
2666
2667fn caller_component_binding(
2668    call: &rhai::NativeCallContext<'_>,
2669    caller_context: &UiContext,
2670) -> ComponentCallbackBinding {
2671    ComponentCallbackBinding {
2672        component: caller_context.callback_component_path().clone(),
2673        incarnation: caller_context.callback_component_incarnation(),
2674        events: caller_context.callback_event_schemas().clone(),
2675        context: Some(caller_context.native_context().cloned().unwrap_or_else(|| {
2676            crate::invocation::ScriptInvocationContext::capture_entry_retained(call)
2677        })),
2678    }
2679}
2680
2681fn component_render_metadata(
2682    active: &ActiveComponentRenderState,
2683    context: &UiContext,
2684) -> Result<(ComponentRenderEnvironment, bool), Box<EvalAltResult>> {
2685    let guard = active.try_borrow().map_err(|_| {
2686        Box::new(component_render_error(
2687            "component render stack is already borrowed",
2688        ))
2689    })?;
2690    let render = guard.as_ref().ok_or_else(|| {
2691        Box::new(component_render_error(
2692            "formal component rendering may run only inside view",
2693        ))
2694    })?;
2695    Ok((render.environment, context.component_render_is_reusable()))
2696}
2697
2698fn record_component_render_failure(
2699    active: &ActiveComponentRenderState,
2700    component: &ComponentInstancePath,
2701) {
2702    if let Ok(mut active) = active.try_borrow_mut()
2703        && let Some(active) = active.as_mut()
2704        && active.failed_component.is_none()
2705    {
2706        active.failed_component = Some(component.clone());
2707    }
2708}
2709
2710fn try_reuse_component_subtree(
2711    shared: &ActiveComponentRenderState,
2712    path: &ComponentInstancePath,
2713    caller_context: &UiContext,
2714    component: &crate::ComponentDefinition,
2715    invocation: &crate::ComponentInvocation,
2716    render: &FnPtr,
2717) -> Result<Option<UiNode>, Box<EvalAltResult>> {
2718    let mut guard = shared.try_borrow_mut().map_err(|_| {
2719        Box::new(component_render_error(
2720            "component render stack is already borrowed",
2721        ))
2722    })?;
2723    let active = guard.as_mut().ok_or_else(|| {
2724        Box::new(component_render_error(
2725            "formal component rendering may run only inside view",
2726        ))
2727    })?;
2728    let Some(scope) =
2729        reusable_component_scope(active, path, caller_context, component, invocation, render)
2730    else {
2731        return Ok(None);
2732    };
2733
2734    let reused_components = scope.invocations.len();
2735    for candidate in scope.invocations.keys() {
2736        let retained = active.transaction.retain_existing(candidate);
2737        debug_assert!(
2738            retained,
2739            "reuse candidates were checked against the state snapshot"
2740        );
2741    }
2742    active.seen.extend(scope.invocations.keys().cloned());
2743    active.invocations.extend(scope.invocations);
2744    active.event_handlers.extend(scope.event_handlers);
2745    active.effects.extend(scope.resources.effects);
2746    active.timers.extend(scope.resources.timers);
2747    active.signals.extend(scope.resources.signals);
2748    active.element_refs.extend(scope.resources.element_refs);
2749    active
2750        .virtual_collections
2751        .extend(scope.resources.virtual_collections);
2752    active
2753        .reused
2754        .push((component.metadata.id.to_string(), reused_components));
2755    Ok(Some(scope.node))
2756}
2757
2758fn prune_virtual_read_contributions(
2759    context: &UiContext,
2760    id: &crate::VirtualCollectionId,
2761    data: &crate::VirtualCollectionData,
2762    indices: &BTreeSet<usize>,
2763    retained: &UiNode,
2764) -> Result<(), RuntimeError> {
2765    let mut active = retained.virtual_read_contributions();
2766    active.extend(
2767        indices
2768            .iter()
2769            .filter_map(|index| data.key(*index))
2770            .map(
2771                |key| crate::read_dependency::ReadContribution::VirtualItem {
2772                    collection: id.clone(),
2773                    key: key.to_owned(),
2774                },
2775            ),
2776    );
2777    context
2778        .runtime()
2779        .try_borrow_mut()
2780        .map_err(|_| RuntimeError::ComponentRuntime("UI state is already borrowed".into()))?
2781        .retain_virtual_read_contributions(context.component_path(), &active);
2782    Ok(())
2783}
2784
2785fn retain_virtual_component_manifest(
2786    shared: &ActiveComponentRenderState,
2787) -> Result<(), RuntimeError> {
2788    let mut guard = shared.try_borrow_mut().map_err(|_| {
2789        RuntimeError::ComponentRuntime("component render stack is already borrowed".to_owned())
2790    })?;
2791    let active = guard.as_mut().ok_or_else(|| {
2792        RuntimeError::ComponentRuntime("virtual render has no active transaction".to_owned())
2793    })?;
2794    let Some(reuse) = active.reuse.as_ref() else {
2795        return Ok(());
2796    };
2797    let mut retained = reuse
2798        .invocations
2799        .keys()
2800        .filter(|path| reuse.subtrees.contains(path))
2801        .cloned()
2802        .collect::<BTreeSet<_>>();
2803    // A transparent component can share its display root with its parent.
2804    // Follow committed invocation ownership, not only display-root markers.
2805    let mut children = BTreeMap::<ComponentInstancePath, Vec<ComponentInstancePath>>::new();
2806    for (path, recipe) in &reuse.invocations {
2807        children
2808            .entry(recipe.parent.clone())
2809            .or_default()
2810            .push(path.clone());
2811    }
2812    let mut pending = retained.iter().cloned().collect::<Vec<_>>();
2813    while let Some(parent) = pending.pop() {
2814        for child in children.get(&parent).into_iter().flatten() {
2815            if retained.insert(child.clone()) {
2816                pending.push(child.clone());
2817            }
2818        }
2819    }
2820    for path in &retained {
2821        if !active.transaction.retain_existing(path) {
2822            return Err(RuntimeError::ComponentRuntime(format!(
2823                "retained virtual component `{path}` has no committed state"
2824            )));
2825        }
2826    }
2827    active.seen.extend(retained.iter().cloned());
2828    active.invocations.extend(
2829        reuse
2830            .invocations
2831            .iter()
2832            .filter(|(path, _)| retained.contains(*path))
2833            .map(|(path, recipe)| (path.clone(), recipe.clone())),
2834    );
2835    active.event_handlers.extend(
2836        reuse
2837            .event_handlers
2838            .iter()
2839            .filter(|((path, _), _)| retained.contains(path))
2840            .map(|(id, callback)| (id.clone(), callback.clone())),
2841    );
2842    active.effects.extend(
2843        reuse
2844            .effects
2845            .iter()
2846            .filter(|(id, _)| retained.contains(id.component()))
2847            .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2848    );
2849    active.timers.extend(
2850        reuse
2851            .timers
2852            .iter()
2853            .filter(|(id, _)| retained.contains(id.component()))
2854            .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2855    );
2856    active.signals.extend(
2857        reuse
2858            .signals
2859            .iter()
2860            .filter(|(id, _)| retained.contains(id.component()))
2861            .map(|(id, descriptor)| (id.clone(), descriptor.clone())),
2862    );
2863    active.element_refs.extend(
2864        reuse
2865            .element_refs
2866            .iter()
2867            .filter(|id| retained.contains(id.component()))
2868            .cloned(),
2869    );
2870    active.virtual_collections.extend(
2871        reuse
2872            .virtual_collections
2873            .iter()
2874            .filter(|(id, _)| reuse.previous_root.virtual_collection_spec(id).is_some())
2875            .map(|(id, recipe)| (id.clone(), recipe.clone())),
2876    );
2877    Ok(())
2878}
2879
2880fn reusable_component_scope(
2881    active: &ActiveComponentRender,
2882    path: &ComponentInstancePath,
2883    caller_context: &UiContext,
2884    component: &crate::ComponentDefinition,
2885    invocation: &crate::ComponentInvocation,
2886    render: &FnPtr,
2887) -> Option<ReusedComponentScope> {
2888    let reuse = active.reuse.as_ref()?;
2889    if reuse.dirty.iter().any(|dirty| dirty.is_within(path)) {
2890        return None;
2891    }
2892    let recipe = reuse.invocations.get(path)?;
2893    if !recipe.reusable
2894        || recipe.generation != active.generation
2895        || recipe.environment != active.environment
2896        || recipe.parent != *caller_context.component_path()
2897        || recipe.component != component.metadata.id
2898        || recipe.export != component.metadata.export
2899        || recipe.declared_key != invocation.key
2900        || recipe.render.fn_name() != render.fn_name()
2901        || !recipe.props.reusable_eq(&invocation.retained_props)
2902    {
2903        return None;
2904    }
2905    let invocations = reuse
2906        .invocations
2907        .range(path.clone()..)
2908        .take_while(|(candidate, _)| candidate.is_within(path))
2909        .map(|(candidate, recipe)| (candidate.clone(), recipe.clone()))
2910        .collect::<BTreeMap<_, _>>();
2911    if invocations.is_empty()
2912        || invocations.values().any(|recipe| !recipe.reusable)
2913        || invocations
2914            .keys()
2915            .any(|candidate| !active.state_snapshot.contains_instance(candidate))
2916    {
2917        return None;
2918    }
2919    let node = if let Some(snapshot) = recipe.snapshot.current() {
2920        let mut node = snapshot.as_ref().clone();
2921        node.hydrate_component_subtrees();
2922        node
2923    } else {
2924        reuse.subtrees.get(&reuse.previous_root, path)?.clone()
2925    };
2926    Some(ReusedComponentScope {
2927        node,
2928        invocations,
2929        event_handlers: reuse
2930            .event_handlers
2931            .range((path.clone(), String::new())..)
2932            .take_while(|((candidate, _), _)| candidate.is_within(path))
2933            .map(|(id, callback)| (id.clone(), callback.clone()))
2934            .collect(),
2935        resources: reused_component_resources(reuse, path),
2936    })
2937}
2938
2939fn reused_component_resources(
2940    reuse: &ComponentReuseSnapshot,
2941    path: &ComponentInstancePath,
2942) -> ReusedComponentResources {
2943    ReusedComponentResources {
2944        effects: reuse
2945            .effects
2946            .iter()
2947            .filter(|(id, _)| id.component().is_within(path))
2948            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
2949            .collect(),
2950        timers: reuse
2951            .timers
2952            .iter()
2953            .filter(|(id, _)| id.component().is_within(path))
2954            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
2955            .collect(),
2956        signals: reuse
2957            .signals
2958            .iter()
2959            .filter(|(id, _)| id.component().is_within(path))
2960            .map(|(id, descriptor)| (id.clone(), descriptor.clone()))
2961            .collect(),
2962        element_refs: reuse
2963            .element_refs
2964            .iter()
2965            .filter(|id| id.component().is_within(path))
2966            .cloned()
2967            .collect(),
2968        virtual_collections: reuse
2969            .virtual_collections
2970            .iter()
2971            .filter(|(id, _)| id.component.is_within(path))
2972            .map(|(id, recipe)| (id.clone(), recipe.clone()))
2973            .collect(),
2974    }
2975}
2976
2977fn register_component_invocation(
2978    active: &ActiveComponentRenderState,
2979    recipe: ComponentInvocationRecipe,
2980) -> Result<(), Box<EvalAltResult>> {
2981    let mut guard = active.try_borrow_mut().map_err(|_| {
2982        Box::new(component_render_error(
2983            "component render stack is already borrowed",
2984        ))
2985    })?;
2986    let active = guard.as_mut().ok_or_else(|| {
2987        Box::new(component_render_error(
2988            "formal component rendering may run only inside view",
2989        ))
2990    })?;
2991    active.invocations.insert(recipe.path.clone(), recipe);
2992    Ok(())
2993}
2994
2995fn resolve_component_invocation(
2996    exports: &ComponentExportCollector,
2997    id: ImmutableString,
2998    props: Map,
2999    generation: ScriptGeneration,
3000) -> Result<(crate::ComponentDefinition, crate::ComponentInvocation), Box<EvalAltResult>> {
3001    let id: String = id.into();
3002    let id =
3003        ModuleId::parse(id).map_err(|error| Box::new(component_render_error(error.to_string())))?;
3004    let registry = exports
3005        .snapshot()
3006        .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3007    let component = registry.get(&id).cloned().ok_or_else(|| {
3008        Box::new(component_render_error(format!(
3009            "component `{id}` is not exported"
3010        )))
3011    })?;
3012    let key = props.get("key").and_then(|value| {
3013        value
3014            .is::<ImmutableString>()
3015            .then(|| value.clone_cast::<ImmutableString>().to_string())
3016    });
3017    let invocation = component
3018        .invoke_with_generation(key, props, generation)
3019        .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3020    Ok((component, invocation))
3021}
3022
3023fn reserve_component_path(
3024    call: &rhai::NativeCallContext<'_>,
3025    component: &crate::ComponentDefinition,
3026    invocation: &crate::ComponentInvocation,
3027    shared: &ActiveComponentRenderState,
3028) -> Result<(ComponentInstancePath, UiContext), Box<EvalAltResult>> {
3029    let mut guard = shared.try_borrow_mut().map_err(|_| {
3030        Box::new(component_render_error(
3031            "component render stack is already borrowed",
3032        ))
3033    })?;
3034    let active = guard.as_mut().ok_or_else(|| {
3035        Box::new(component_render_error(
3036            "formal component rendering may run only inside view",
3037        ))
3038    })?;
3039    let caller_context = active.contexts.last().cloned().ok_or_else(|| {
3040        Box::new(component_render_error(
3041            "component render context stack has no caller",
3042        ))
3043    })?;
3044    let parent = active.stack.last().cloned().ok_or_else(|| {
3045        Box::new(component_render_error(
3046            "component render stack has no application root",
3047        ))
3048    })?;
3049    let key = invocation.key.clone().unwrap_or_else(|| {
3050        let callsite = format!(
3051            "{}@{}",
3052            call.call_source().unwrap_or("<script>"),
3053            call.call_position()
3054        );
3055        let occurrence = active
3056            .positions
3057            .entry(format!("{parent}/{}/{callsite}", component.metadata.export))
3058            .or_default();
3059        let key = format!("{callsite}#{occurrence}");
3060        *occurrence = occurrence.saturating_add(1);
3061        key
3062    });
3063    let path = parent.child(&component.metadata.export, key);
3064    if !active.seen.insert(path.clone()) {
3065        return Err(Box::new(component_render_error(format!(
3066            "duplicate component instance path `{path}`; add a stable key"
3067        ))));
3068    }
3069    Ok((path, caller_context))
3070}
3071
3072fn enter_component_render(
3073    path: &ComponentInstancePath,
3074    component: &crate::ComponentDefinition,
3075    invocation: &crate::ComponentInvocation,
3076    native_context: crate::invocation::ScriptInvocationContext,
3077    shared: &ActiveComponentRenderState,
3078) -> Result<UiContext, Box<EvalAltResult>> {
3079    let mut guard = shared.try_borrow_mut().map_err(|_| {
3080        Box::new(component_render_error(
3081            "component render stack is already borrowed",
3082        ))
3083    })?;
3084    let active = guard.as_mut().ok_or_else(|| {
3085        Box::new(component_render_error(
3086            "formal component rendering may run only inside view",
3087        ))
3088    })?;
3089    active
3090        .transaction
3091        .mount(path.clone(), &component.schema.state)
3092        .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3093    let global_part_styles = {
3094        let mut runtime = active
3095            .root_context
3096            .runtime()
3097            .try_borrow_mut()
3098            .map_err(|_| Box::new(component_render_error("UI state is already borrowed")))?;
3099        runtime
3100            .component_state
3101            .mount_instance(path.clone(), &component.schema.state)
3102            .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3103        runtime
3104            .component_styles()
3105            .component(&component.metadata.id)
3106            .cloned()
3107            .unwrap_or_default()
3108    };
3109    let context = active
3110        .root_context
3111        .for_component(path.clone(), component.schema.events.clone())
3112        .with_component_styles(
3113            global_part_styles,
3114            component_root_style(&invocation.props),
3115            component_part_styles(&invocation.props),
3116        )
3117        .with_native_context(Some(native_context))
3118        .with_generation(active.generation);
3119    active.stack.push(path.clone());
3120    active.contexts.push(context.clone());
3121    active
3122        .effect_keys
3123        .push(Some(component.schema.effects.clone()));
3124    Ok(context)
3125}
3126
3127fn leave_component_render(active: &ActiveComponentRenderState) -> Result<(), Box<EvalAltResult>> {
3128    let mut guard = active.try_borrow_mut().map_err(|_| {
3129        Box::new(component_render_error(
3130            "component render stack is already borrowed",
3131        ))
3132    })?;
3133    if let Some(active) = guard.as_mut() {
3134        active.stack.pop();
3135        active.contexts.pop();
3136        active.effect_keys.pop();
3137    }
3138    Ok(())
3139}
3140
3141fn register_effect_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3142    let active = Rc::clone(active);
3143    FuncRegistration::new("effect")
3144        .in_global_namespace()
3145        .register_into_engine(
3146            engine,
3147            move |call: rhai::NativeCallContext<'_>,
3148                  key: ImmutableString,
3149                  dependencies: Dynamic,
3150                  start: FnPtr,
3151                  cleanup: FnPtr|
3152                  -> Result<(), Box<EvalAltResult>> {
3153                let key = key.to_string();
3154                let (component, context, generation, declared) = {
3155                    let guard = active.try_borrow().map_err(|_| {
3156                        Box::new(component_render_error(
3157                            "component render stack is already borrowed",
3158                        ))
3159                    })?;
3160                    let render = guard.as_ref().ok_or_else(|| {
3161                        Box::new(component_render_error(
3162                            "effect may run only during formal component render",
3163                        ))
3164                    })?;
3165                    let context = render.contexts.last().cloned().ok_or_else(|| {
3166                        Box::new(component_render_error(
3167                            "component render context stack is empty",
3168                        ))
3169                    })?;
3170                    let declared =
3171                        render
3172                            .effect_keys
3173                            .last()
3174                            .cloned()
3175                            .flatten()
3176                            .ok_or_else(|| {
3177                                Box::new(component_render_error(
3178                                    "effects may be declared only by formal components",
3179                                ))
3180                            })?;
3181                    (
3182                        context.component_path().clone(),
3183                        context,
3184                        render.generation,
3185                        declared,
3186                    )
3187                };
3188                if !declared.contains(&key) {
3189                    return Err(Box::new(component_render_error(
3190                        crate::EffectError::Undeclared { component, key }.to_string(),
3191                    )));
3192                }
3193                let id = crate::EffectId::new(component.clone(), key.clone())
3194                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3195                let dependencies = UiValue::from_dynamic(dependencies)
3196                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3197                let native_context =
3198                    crate::invocation::ScriptInvocationContext::capture_retained(&call);
3199                let mut start = ScriptCallback::try_from_fn_ptr(start, generation)
3200                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3201                start.bind_component_scope_if_unset(
3202                    &component,
3203                    context.component_incarnation(),
3204                    context.event_schemas().clone(),
3205                );
3206                start.bind_native_context_if_unset(native_context.clone());
3207                let mut cleanup = ScriptCallback::try_from_fn_ptr(cleanup, generation)
3208                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3209                cleanup.bind_component_scope_if_unset(
3210                    &component,
3211                    context.component_incarnation(),
3212                    context.event_schemas().clone(),
3213                );
3214                cleanup.bind_native_context_if_unset(native_context);
3215                let descriptor =
3216                    crate::EffectDescriptor::new(id.clone(), dependencies, start, cleanup);
3217                let mut guard = active.try_borrow_mut().map_err(|_| {
3218                    Box::new(component_render_error(
3219                        "component render stack is already borrowed",
3220                    ))
3221                })?;
3222                let render = guard.as_mut().ok_or_else(|| {
3223                    Box::new(component_render_error(
3224                        "effect may run only during formal component render",
3225                    ))
3226                })?;
3227                if render.effects.insert(id, descriptor).is_some() {
3228                    return Err(Box::new(component_render_error(
3229                        crate::EffectError::Duplicate { component, key }.to_string(),
3230                    )));
3231                }
3232                Ok(())
3233            },
3234        );
3235}
3236
3237fn register_timer_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3238    let active = Rc::clone(active);
3239    FuncRegistration::new("timeout")
3240        .in_global_namespace()
3241        .register_into_engine(
3242            engine,
3243            move |call: rhai::NativeCallContext<'_>,
3244                  key: ImmutableString,
3245                  delay_ms: rhai::INT,
3246                  paused: bool,
3247                  callback: FnPtr,
3248                  payload: Dynamic|
3249                  -> Result<(), Box<EvalAltResult>> {
3250                let (component, context, generation) = {
3251                    let guard = active.try_borrow().map_err(|_| {
3252                        Box::new(component_render_error(
3253                            "component render stack is already borrowed",
3254                        ))
3255                    })?;
3256                    let render = guard.as_ref().ok_or_else(|| {
3257                        Box::new(component_render_error(
3258                            "timeout may run only during formal component render",
3259                        ))
3260                    })?;
3261                    let context = render.contexts.last().cloned().ok_or_else(|| {
3262                        Box::new(component_render_error(
3263                            "component render context stack is empty",
3264                        ))
3265                    })?;
3266                    (context.component_path().clone(), context, render.generation)
3267                };
3268                let delay_ms = u64::try_from(delay_ms).map_err(|_| {
3269                    Box::new(component_render_error(
3270                        "timeout delay must be a positive integer",
3271                    ))
3272                })?;
3273                let id = crate::TimerId::new(component.clone(), key.to_string())
3274                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3275                let payload = UiValue::from_dynamic(payload)
3276                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3277                let native_context =
3278                    crate::invocation::ScriptInvocationContext::capture_retained(&call);
3279                let mut callback = ScriptCallback::try_from_fn_ptr(callback, generation)
3280                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3281                callback.bind_component_scope_if_unset(
3282                    &component,
3283                    context.component_incarnation(),
3284                    context.event_schemas().clone(),
3285                );
3286                callback.bind_native_context_if_unset(native_context);
3287                let descriptor = crate::TimerDescriptor::new(
3288                    id.clone(),
3289                    Duration::from_millis(delay_ms),
3290                    paused,
3291                    callback,
3292                    payload,
3293                )
3294                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3295                let mut guard = active.try_borrow_mut().map_err(|_| {
3296                    Box::new(component_render_error(
3297                        "component render stack is already borrowed",
3298                    ))
3299                })?;
3300                let render = guard.as_mut().ok_or_else(|| {
3301                    Box::new(component_render_error(
3302                        "timeout may run only during formal component render",
3303                    ))
3304                })?;
3305                if render.timers.insert(id.clone(), descriptor).is_some() {
3306                    return Err(Box::new(component_render_error(format!(
3307                        "timeout `{}` is declared more than once in `{}`",
3308                        id.key(),
3309                        id.component()
3310                    ))));
3311                }
3312                Ok(())
3313            },
3314        );
3315}
3316
3317fn register_signal_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3318    let scalar_active = Rc::clone(active);
3319    FuncRegistration::new("signal")
3320        .in_global_namespace()
3321        .register_into_engine(
3322            engine,
3323            move |key: ImmutableString,
3324                  initial: Dynamic|
3325                  -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3326                let key = key.to_string();
3327                let initial = crate::SignalValue::from_dynamic(initial)
3328                    .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3329                declare_signal(&scalar_active, key, initial)
3330            },
3331        );
3332
3333    let optional_active = Rc::clone(active);
3334    FuncRegistration::new("optional_float_signal")
3335        .in_global_namespace()
3336        .register_into_engine(
3337            engine,
3338            move |key: ImmutableString| -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3339                declare_signal(
3340                    &optional_active,
3341                    key.to_string(),
3342                    crate::SignalValue::OptionalFloat(None),
3343                )
3344            },
3345        );
3346}
3347
3348fn declare_signal(
3349    active: &ActiveComponentRenderState,
3350    key: String,
3351    initial: crate::SignalValue,
3352) -> Result<crate::NativeSignal, Box<EvalAltResult>> {
3353    let mut guard = active.try_borrow_mut().map_err(|_| {
3354        Box::new(crate::signal::signal_runtime_error(
3355            &"component render stack is already borrowed",
3356        ))
3357    })?;
3358    let render = guard.as_mut().ok_or_else(|| {
3359        Box::new(crate::signal::signal_runtime_error(
3360            &"signal may run only during formal component render",
3361        ))
3362    })?;
3363    if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3364        return Err(Box::new(crate::signal::signal_runtime_error(
3365            &"signals may be declared only by formal components",
3366        )));
3367    }
3368    let context = render.contexts.last().ok_or_else(|| {
3369        Box::new(crate::signal::signal_runtime_error(
3370            &"component render context stack is empty",
3371        ))
3372    })?;
3373    let component = context.component_path().clone();
3374    let incarnation = context.component_incarnation();
3375    if render
3376        .signals
3377        .keys()
3378        .any(|id| id.component() == &component && id.key() == key)
3379    {
3380        return Err(Box::new(crate::signal::signal_runtime_error(
3381            &crate::SignalError::Duplicate { component, key },
3382        )));
3383    }
3384    let id = crate::SignalId::new_scoped(component, incarnation, key, initial.kind())
3385        .map_err(|error| Box::new(crate::signal::signal_runtime_error(&error)))?;
3386    let signal = crate::NativeSignal::new(id.clone());
3387    render
3388        .signals
3389        .insert(id, crate::signal::SignalDescriptor::new(initial));
3390    Ok(signal)
3391}
3392
3393fn register_element_ref_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3394    let active = Rc::clone(active);
3395    FuncRegistration::new("element_ref")
3396        .in_global_namespace()
3397        .register_into_engine(
3398            engine,
3399            move |key: ImmutableString| -> Result<crate::ElementRef, Box<EvalAltResult>> {
3400                let key = key.to_string();
3401                let mut guard = active.try_borrow_mut().map_err(|_| {
3402                    Box::new(component_render_error(
3403                        "component render stack is already borrowed",
3404                    ))
3405                })?;
3406                let render = guard.as_mut().ok_or_else(|| {
3407                    Box::new(component_render_error(
3408                        "element_ref may run only during formal component render",
3409                    ))
3410                })?;
3411                if render.effect_keys.last().and_then(Option::as_ref).is_none() {
3412                    return Err(Box::new(component_render_error(
3413                        "element refs may be declared only by formal components",
3414                    )));
3415                }
3416                let component = render
3417                    .contexts
3418                    .last()
3419                    .ok_or_else(|| {
3420                        Box::new(component_render_error(
3421                            "component render context stack is empty",
3422                        ))
3423                    })?
3424                    .component_path()
3425                    .clone();
3426                let id = crate::ElementRefId::new(component.clone(), key.clone())
3427                    .map_err(|error| Box::new(component_render_error(error.to_string())))?;
3428                if !render.element_refs.insert(id.clone()) {
3429                    return Err(Box::new(component_render_error(format!(
3430                        "element ref `{key}` is declared more than once by component `{component}`"
3431                    ))));
3432                }
3433                Ok(crate::ElementRef::new(id))
3434            },
3435        );
3436}
3437
3438#[derive(Debug)]
3439struct DecodedVirtualCollection {
3440    key: String,
3441    label: String,
3442    data: crate::VirtualCollectionData,
3443    estimated_height: f64,
3444    height: Option<f64>,
3445    overdraw_pixels: f64,
3446    bottom_align: bool,
3447    follow_tail: bool,
3448    reveal_key: Option<String>,
3449    sticky_headers: Arc<BTreeSet<usize>>,
3450}
3451
3452impl DecodedVirtualCollection {
3453    fn into_spec(
3454        self,
3455        id: crate::VirtualCollectionId,
3456        realized: BTreeMap<usize, UiNode>,
3457    ) -> crate::VirtualCollectionNodeSpec {
3458        crate::VirtualCollectionNodeSpec {
3459            id,
3460            label: self.label,
3461            data: self.data,
3462            realized,
3463            estimated_height: self.estimated_height,
3464            height: self.height,
3465            overdraw_pixels: self.overdraw_pixels,
3466            bottom_align: self.bottom_align,
3467            follow_tail: self.follow_tail,
3468            reveal_key: self.reveal_key,
3469            sticky_headers: self.sticky_headers,
3470            inherited_motion_group: None,
3471        }
3472    }
3473}
3474
3475fn decode_virtual_collection(
3476    mut config: Map,
3477) -> Result<DecodedVirtualCollection, Box<EvalAltResult>> {
3478    let key = collection_string(&mut config, "key")?;
3479    let label = collection_optional_string(&mut config, "label")?.unwrap_or_default();
3480    let estimated_height = collection_positive(&mut config, "estimated_height")?;
3481    let height = collection_optional_number(&mut config, "height")?;
3482    let fill_height = collection_optional_bool(&mut config, "fill_height")?.unwrap_or(false);
3483    let height = match (height, fill_height) {
3484        (Some(_), true) => {
3485            return Err(Box::new(component_render_error(
3486                "virtual collection must specify either `height` or `fill_height`, not both",
3487            )));
3488        }
3489        (None, false) => {
3490            return Err(Box::new(component_render_error(
3491                "virtual collection requires `height` or `fill_height: true`",
3492            )));
3493        }
3494        (None, true) => None,
3495        (Some(height), false) if height.is_finite() && height > 0.0 => Some(height),
3496        (Some(_), false) => {
3497            return Err(Box::new(component_render_error(
3498                "virtual collection `height` must be finite and positive",
3499            )));
3500        }
3501    };
3502    let overdraw_pixels = collection_optional_number(&mut config, "overdraw_pixels")?
3503        .unwrap_or(estimated_height * 2.0);
3504    if !overdraw_pixels.is_finite() || !(0.0..=10_000.0).contains(&overdraw_pixels) {
3505        return Err(Box::new(component_render_error(
3506            "virtual collection overdraw_pixels must be between 0 and 10000",
3507        )));
3508    }
3509    let alignment =
3510        collection_optional_string(&mut config, "alignment")?.unwrap_or_else(|| "top".to_owned());
3511    let bottom_align = match alignment.as_str() {
3512        "top" => false,
3513        "bottom" => true,
3514        _ => {
3515            return Err(Box::new(component_render_error(
3516                "virtual collection alignment must be `top` or `bottom`",
3517            )));
3518        }
3519    };
3520    let follow_tail = collection_optional_bool(&mut config, "follow_tail")?.unwrap_or(false);
3521    let data = collection_data(&mut config)?;
3522    let reveal_key = collection_optional_string(&mut config, "reveal_key")?;
3523    if reveal_key.as_deref() == Some("") {
3524        return Err(Box::new(component_render_error(
3525            "virtual collection `reveal_key` must not be empty",
3526        )));
3527    }
3528    if let Some(reveal_key) = reveal_key.as_deref()
3529        && !(0..data.len()).any(|index| data.key(index) == Some(reveal_key))
3530    {
3531        return Err(Box::new(component_render_error(format!(
3532            "virtual collection reveal key `{reveal_key}` is not present in its data"
3533        ))));
3534    }
3535    if follow_tail && reveal_key.is_some() {
3536        return Err(Box::new(component_render_error(
3537            "virtual collection must not combine `follow_tail` with `reveal_key`",
3538        )));
3539    }
3540    let sticky_headers = collection_optional_indices(&mut config, "sticky_headers")?
3541        .map_or_else(|| data.sticky_headers(), Arc::new);
3542    if let Some(index) = sticky_headers.iter().find(|index| **index >= data.len()) {
3543        return Err(Box::new(component_render_error(format!(
3544            "virtual collection sticky header index {index} is outside {} items",
3545            data.len()
3546        ))));
3547    }
3548    if bottom_align && !sticky_headers.is_empty() {
3549        return Err(Box::new(component_render_error(
3550            "sticky headers require top-aligned virtual collections",
3551        )));
3552    }
3553    if let Some((unknown, _)) = config.into_iter().next() {
3554        return Err(Box::new(component_render_error(format!(
3555            "unknown virtual collection field `{unknown}`"
3556        ))));
3557    }
3558    Ok(DecodedVirtualCollection {
3559        key,
3560        label,
3561        data,
3562        estimated_height,
3563        height,
3564        overdraw_pixels,
3565        bottom_align,
3566        follow_tail,
3567        reveal_key,
3568        sticky_headers,
3569    })
3570}
3571
3572fn register_virtual_collection_api(engine: &mut Engine, active: &ActiveComponentRenderState) {
3573    let active = Rc::clone(active);
3574    FuncRegistration::new("virtual_collection")
3575        .in_global_namespace()
3576        .register_into_engine(
3577            engine,
3578            move |call: rhai::NativeCallContext<'_>,
3579                  config: Map,
3580                  renderer: FnPtr|
3581                  -> Result<UiNode, Box<EvalAltResult>> {
3582                validate_virtual_renderer(&renderer)?;
3583                let decoded = decode_virtual_collection(config)?;
3584
3585                let VirtualCollectionContext {
3586                    component,
3587                    context,
3588                    generation,
3589                } = virtual_collection_context(&active)?;
3590                let id = crate::VirtualCollectionId {
3591                    component: component.clone(),
3592                    key: decoded.key.clone(),
3593                };
3594                let collection_context = context
3595                    .for_structural_scope(
3596                        component.child("VirtualCollection", decoded.key.clone()),
3597                        BTreeMap::new(),
3598                    )
3599                    .with_generation(generation);
3600                let native_context =
3601                    crate::invocation::ScriptInvocationContext::capture_retained(&call);
3602                let realized = realize_seeded_virtual_collection(
3603                    &call,
3604                    &renderer,
3605                    &collection_context,
3606                    &active,
3607                    &id,
3608                    &decoded,
3609                )?;
3610                let recipe = VirtualCollectionRecipe {
3611                    id: id.clone(),
3612                    data: decoded.data.clone(),
3613                    renderer,
3614                    renderer_context: native_context,
3615                    context: collection_context,
3616                    generation,
3617                };
3618                let mut guard = active.try_borrow_mut().map_err(|_| {
3619                    Box::new(component_render_error(
3620                        "component render stack is already borrowed",
3621                    ))
3622                })?;
3623                let render = guard.as_mut().ok_or_else(|| {
3624                    Box::new(component_render_error(
3625                        "virtual_collection may run only during view render",
3626                    ))
3627                })?;
3628                if render
3629                    .virtual_collections
3630                    .insert(id.clone(), recipe)
3631                    .is_some()
3632                {
3633                    return Err(Box::new(component_render_error(format!(
3634                        "virtual collection `{}` is declared more than once in `{}`",
3635                        decoded.key, id.component
3636                    ))));
3637                }
3638                Ok(UiNode::virtual_collection(decoded.into_spec(id, realized)))
3639            },
3640        );
3641}
3642
3643fn validate_virtual_renderer(renderer: &FnPtr) -> Result<(), Box<EvalAltResult>> {
3644    if renderer.is_anonymous() {
3645        return Err(Box::new(component_render_error(
3646            "virtual collection item renderer must be a named function",
3647        )));
3648    }
3649    for (index, value) in renderer.iter_curry().enumerate() {
3650        validate_virtual_renderer_curry(value).map_err(|error| {
3651            Box::new(component_render_error(format!(
3652                "virtual collection renderer curry {index} is not durable presentation data: {error}"
3653            )))
3654        })?;
3655    }
3656    Ok(())
3657}
3658
3659fn validate_virtual_renderer_curry(value: &Dynamic) -> Result<(), crate::UiValueError> {
3660    if value.is::<UiNode>() || value.is::<crate::NativeSignal>() {
3661        return Ok(());
3662    }
3663    if value.is::<Array>() {
3664        for value in value.clone_cast::<Array>() {
3665            validate_virtual_renderer_curry(&value)?;
3666        }
3667        return Ok(());
3668    }
3669    if value.is::<Map>() {
3670        for value in value.clone_cast::<Map>().values() {
3671            validate_virtual_renderer_curry(value)?;
3672        }
3673        return Ok(());
3674    }
3675    UiValue::from_dynamic(value.clone()).map(|_| ())
3676}
3677
3678fn realize_initial_collection(
3679    call: &rhai::NativeCallContext<'_>,
3680    renderer: &FnPtr,
3681    context: &UiContext,
3682    id: &crate::VirtualCollectionId,
3683    data: &crate::VirtualCollectionData,
3684    indices: &BTreeSet<usize>,
3685) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3686    let mut realized = BTreeMap::new();
3687    for index in indices.iter().copied().filter(|index| *index < data.len()) {
3688        let item = data
3689            .item(index)
3690            .map_err(|error| Box::new(component_render_error(error.to_string())))?
3691            .ok_or_else(|| {
3692                Box::new(component_render_error(format!(
3693                    "virtual collection item {index} disappeared"
3694                )))
3695            })?;
3696        let (item_key, mut payload) = collection_payload(&item, index)?;
3697        add_collection_neighbors(data, index, &mut payload);
3698        let node = renderer
3699            .call_within_context::<UiNode>(
3700                call,
3701                (context.for_virtual_item(id, &item_key), payload),
3702            )?
3703            .with_key(item_key);
3704        realized.insert(index, node);
3705    }
3706    Ok(realized)
3707}
3708
3709fn realize_seeded_virtual_collection(
3710    call: &rhai::NativeCallContext<'_>,
3711    renderer: &FnPtr,
3712    context: &UiContext,
3713    active: &ActiveComponentRenderState,
3714    id: &crate::VirtualCollectionId,
3715    decoded: &DecodedVirtualCollection,
3716) -> Result<BTreeMap<usize, UiNode>, Box<EvalAltResult>> {
3717    let previous_metrics = context
3718        .runtime()
3719        .try_borrow()
3720        .map_err(|_| {
3721            Box::new(component_render_error(
3722                "UI state is already borrowed while seeding a virtual collection",
3723            ))
3724        })?
3725        .virtual_requests
3726        .metrics(id);
3727    let viewport = decoded.height.unwrap_or_else(|| {
3728        previous_metrics
3729            .as_ref()
3730            .map_or(decoded.estimated_height, |metrics| {
3731                if metrics.viewport_height > 0.0 {
3732                    metrics.viewport_height
3733                } else {
3734                    decoded.estimated_height
3735                }
3736            })
3737    });
3738    let count = nonnegative_usize(
3739        ((viewport + decoded.overdraw_pixels) / decoded.estimated_height).ceil() + 1.0,
3740    )
3741    .min(decoded.data.len());
3742    let indices =
3743        virtual_collection_seed_indices(active, id, decoded, count, previous_metrics.as_ref())?;
3744    enter_virtual_collection_scope(active, context.clone())?;
3745    let realized = realize_initial_collection(call, renderer, context, id, &decoded.data, &indices);
3746    leave_component_render(active)?;
3747    realized
3748}
3749
3750fn virtual_collection_seed_indices(
3751    active: &ActiveComponentRenderState,
3752    id: &crate::VirtualCollectionId,
3753    decoded: &DecodedVirtualCollection,
3754    count: usize,
3755    metrics: Option<&crate::VirtualCollectionMetrics>,
3756) -> Result<BTreeSet<usize>, Box<EvalAltResult>> {
3757    let mut indices = BTreeSet::new();
3758    let guard = active.try_borrow().map_err(|_| {
3759        Box::new(component_render_error(
3760            "component render stack is already borrowed",
3761        ))
3762    })?;
3763    let reuse = guard.as_ref().and_then(|render| render.reuse.as_ref());
3764    let previous = reuse.and_then(|reuse| reuse.previous_root.virtual_collection_spec(id));
3765    let preserves_scroll = previous.is_some_and(|previous| {
3766        previous.estimated_height.to_bits() == decoded.estimated_height.to_bits()
3767            && previous.overdraw_pixels.to_bits() == decoded.overdraw_pixels.to_bits()
3768            && previous.bottom_align == decoded.bottom_align
3769            && same_collection_key_order(&previous.data, &decoded.data)
3770    });
3771
3772    if preserves_scroll {
3773        if let Some(previous) = previous {
3774            indices.extend(previous.realized.keys().copied());
3775        }
3776        if let Some(metrics) = metrics {
3777            extend_virtual_window(
3778                &mut indices,
3779                metrics.scroll_item,
3780                count,
3781                decoded.data.len(),
3782                virtual_overdraw_items(decoded),
3783            );
3784        }
3785    } else if decoded.reveal_key.is_none() && !decoded.bottom_align && !decoded.follow_tail {
3786        extend_virtual_window(&mut indices, 0, count, decoded.data.len(), 0);
3787    }
3788    drop(guard);
3789
3790    if let Some(reveal) = decoded.reveal_key.as_deref()
3791        && let Some(index) =
3792            (0..decoded.data.len()).find(|index| decoded.data.key(*index) == Some(reveal))
3793    {
3794        extend_virtual_window(
3795            &mut indices,
3796            index,
3797            count,
3798            decoded.data.len(),
3799            virtual_overdraw_items(decoded),
3800        );
3801        if let Some(header) = decoded.sticky_headers.range(..=index).next_back() {
3802            indices.insert(*header);
3803        }
3804    }
3805    if (decoded.bottom_align || decoded.follow_tail) && !decoded.data.is_empty() {
3806        let start = decoded.data.len().saturating_sub(count);
3807        indices.extend(start..decoded.data.len());
3808    }
3809    Ok(indices)
3810}
3811
3812fn same_collection_key_order(
3813    current: &crate::VirtualCollectionData,
3814    next: &crate::VirtualCollectionData,
3815) -> bool {
3816    current.len() == next.len()
3817        && (0..current.len()).all(|index| current.key(index) == next.key(index))
3818}
3819
3820fn virtual_overdraw_items(decoded: &DecodedVirtualCollection) -> usize {
3821    nonnegative_usize((decoded.overdraw_pixels / decoded.estimated_height).ceil())
3822}
3823
3824fn extend_virtual_window(
3825    indices: &mut BTreeSet<usize>,
3826    anchor: usize,
3827    count: usize,
3828    len: usize,
3829    leading: usize,
3830) {
3831    let start = anchor.saturating_sub(leading).min(len);
3832    let end = anchor.saturating_add(count).min(len);
3833    indices.extend(start..end);
3834}
3835
3836struct VirtualCollectionContext {
3837    component: ComponentInstancePath,
3838    context: UiContext,
3839    generation: ScriptGeneration,
3840}
3841
3842fn enter_virtual_collection_scope(
3843    active: &ActiveComponentRenderState,
3844    context: UiContext,
3845) -> Result<(), Box<EvalAltResult>> {
3846    let mut guard = active.try_borrow_mut().map_err(|_| {
3847        Box::new(component_render_error(
3848            "component render stack is already borrowed",
3849        ))
3850    })?;
3851    let render = guard.as_mut().ok_or_else(|| {
3852        Box::new(component_render_error(
3853            "virtual_collection may run only during view render",
3854        ))
3855    })?;
3856    render.stack.push(context.component_path().clone());
3857    render.contexts.push(context);
3858    render.effect_keys.push(None);
3859    Ok(())
3860}
3861
3862fn virtual_collection_context(
3863    active: &ActiveComponentRenderState,
3864) -> Result<VirtualCollectionContext, Box<EvalAltResult>> {
3865    let guard = active.try_borrow().map_err(|_| {
3866        Box::new(component_render_error(
3867            "component render stack is already borrowed",
3868        ))
3869    })?;
3870    let render = guard.as_ref().ok_or_else(|| {
3871        Box::new(component_render_error(
3872            "virtual_collection may run only during view render",
3873        ))
3874    })?;
3875    let context = render.contexts.last().cloned().ok_or_else(|| {
3876        Box::new(component_render_error(
3877            "component render context stack is empty",
3878        ))
3879    })?;
3880    Ok(VirtualCollectionContext {
3881        component: context.component_path().clone(),
3882        context: context.clone(),
3883        generation: render.generation,
3884    })
3885}
3886
3887fn collection_string(config: &mut Map, name: &str) -> Result<String, Box<EvalAltResult>> {
3888    config
3889        .remove(name)
3890        .and_then(Dynamic::try_cast::<ImmutableString>)
3891        .map(|value| value.to_string())
3892        .ok_or_else(|| {
3893            Box::new(component_render_error(format!(
3894                "virtual collection `{name}` must be a string"
3895            )))
3896        })
3897}
3898
3899fn collection_optional_string(
3900    config: &mut Map,
3901    name: &str,
3902) -> Result<Option<String>, Box<EvalAltResult>> {
3903    config
3904        .remove(name)
3905        .map(|value| {
3906            value
3907                .try_cast::<ImmutableString>()
3908                .map(|value| value.to_string())
3909                .ok_or_else(|| {
3910                    Box::new(component_render_error(format!(
3911                        "virtual collection `{name}` must be a string"
3912                    )))
3913                })
3914        })
3915        .transpose()
3916}
3917
3918fn collection_optional_number(
3919    config: &mut Map,
3920    name: &str,
3921) -> Result<Option<f64>, Box<EvalAltResult>> {
3922    config
3923        .remove(name)
3924        .map(|value| {
3925            if value.is::<rhai::FLOAT>() {
3926                Ok(value.cast::<rhai::FLOAT>())
3927            } else if value.is::<rhai::INT>() {
3928                value
3929                    .cast::<rhai::INT>()
3930                    .to_string()
3931                    .parse::<f64>()
3932                    .map_err(|_| Box::new(component_render_error("invalid collection number")))
3933            } else {
3934                Err(Box::new(component_render_error(format!(
3935                    "virtual collection `{name}` must be a number"
3936                ))))
3937            }
3938        })
3939        .transpose()
3940}
3941
3942fn collection_positive(config: &mut Map, name: &str) -> Result<f64, Box<EvalAltResult>> {
3943    let value = collection_optional_number(config, name)?.ok_or_else(|| {
3944        Box::new(component_render_error(format!(
3945            "virtual collection `{name}` is required"
3946        )))
3947    })?;
3948    if value.is_finite() && value > 0.0 {
3949        Ok(value)
3950    } else {
3951        Err(Box::new(component_render_error(format!(
3952            "virtual collection `{name}` must be finite and positive"
3953        ))))
3954    }
3955}
3956
3957fn collection_optional_bool(
3958    config: &mut Map,
3959    name: &str,
3960) -> Result<Option<bool>, Box<EvalAltResult>> {
3961    config
3962        .remove(name)
3963        .map(|value| {
3964            value.try_cast::<bool>().ok_or_else(|| {
3965                Box::new(component_render_error(format!(
3966                    "virtual collection `{name}` must be bool"
3967                )))
3968            })
3969        })
3970        .transpose()
3971}
3972
3973fn collection_optional_indices(
3974    config: &mut Map,
3975    name: &str,
3976) -> Result<Option<BTreeSet<usize>>, Box<EvalAltResult>> {
3977    let Some(value) = config.remove(name) else {
3978        return Ok(None);
3979    };
3980    let values = value.try_cast::<Array>().ok_or_else(|| {
3981        Box::new(component_render_error(format!(
3982            "virtual collection `{name}` must be an array of non-negative integers"
3983        )))
3984    })?;
3985    let mut indices = BTreeSet::new();
3986    for (position, value) in values.into_iter().enumerate() {
3987        let index = value.try_cast::<rhai::INT>().ok_or_else(|| {
3988            Box::new(component_render_error(format!(
3989                "virtual collection `{name}` item {position} must be an integer"
3990            )))
3991        })?;
3992        let index = usize::try_from(index).map_err(|_| {
3993            Box::new(component_render_error(format!(
3994                "virtual collection `{name}` item {position} must be non-negative"
3995            )))
3996        })?;
3997        if !indices.insert(index) {
3998            return Err(Box::new(component_render_error(format!(
3999                "virtual collection `{name}` contains duplicate index {index}"
4000            ))));
4001        }
4002    }
4003    Ok(Some(indices))
4004}
4005
4006fn collection_data(config: &mut Map) -> Result<crate::VirtualCollectionData, Box<EvalAltResult>> {
4007    let data = config.remove("data").ok_or_else(|| {
4008        Box::new(component_render_error(
4009            "virtual collection `data` is required",
4010        ))
4011    })?;
4012    if data.is::<crate::NativeCollection>() {
4013        return Ok(crate::VirtualCollectionData::Native(
4014            data.cast::<crate::NativeCollection>(),
4015        ));
4016    }
4017    let data = data.try_cast::<Array>().ok_or_else(|| {
4018        Box::new(component_render_error(
4019            "virtual collection `data` must be an array or NativeCollection",
4020        ))
4021    })?;
4022    let data = data
4023        .into_iter()
4024        .map(|value| {
4025            UiValue::from_dynamic(value)
4026                .map_err(|error| Box::new(component_render_error(error.to_string())))
4027        })
4028        .collect::<Result<Vec<_>, _>>()?;
4029    let mut keys = BTreeSet::new();
4030    for (index, item) in data.iter().enumerate() {
4031        let (key, _) = collection_payload(item, index)?;
4032        if !keys.insert(key.clone()) {
4033            return Err(Box::new(component_render_error(format!(
4034                "virtual collection data key `{key}` is duplicated"
4035            ))));
4036        }
4037    }
4038    Ok(crate::VirtualCollectionData::Values(data))
4039}
4040
4041fn collection_payload(item: &UiValue, index: usize) -> Result<(String, Map), Box<EvalAltResult>> {
4042    let UiValue::Map(map) = item else {
4043        return Err(Box::new(component_render_error(
4044            "virtual collection data items must be maps with a string `key`",
4045        )));
4046    };
4047    let key = match map.get("key") {
4048        Some(UiValue::String(key)) if !key.is_empty() => key.clone(),
4049        _ => {
4050            return Err(Box::new(component_render_error(
4051                "virtual collection data item `key` must be a non-empty string",
4052            )));
4053        }
4054    };
4055    Ok((
4056        key.clone(),
4057        Map::from_iter([
4058            ("key".into(), Dynamic::from(key)),
4059            (
4060                "index".into(),
4061                Dynamic::from_int(rhai::INT::try_from(index).unwrap_or(rhai::INT::MAX)),
4062            ),
4063            ("item".into(), item.clone().into_dynamic()),
4064        ]),
4065    ))
4066}
4067
4068fn add_collection_neighbors(data: &crate::VirtualCollectionData, index: usize, payload: &mut Map) {
4069    let key = |index: Option<usize>| {
4070        index
4071            .and_then(|index| data.key(index))
4072            .map_or(Dynamic::UNIT, |key| Dynamic::from(key.to_owned()))
4073    };
4074    payload.insert("previous_key".into(), key(index.checked_sub(1)));
4075    payload.insert(
4076        "next_key".into(),
4077        key(index.checked_add(1).filter(|index| *index < data.len())),
4078    );
4079    payload.insert("first_key".into(), key((!data.is_empty()).then_some(0)));
4080    payload.insert("last_key".into(), key(data.len().checked_sub(1)));
4081}
4082
4083fn nonnegative_usize(value: f64) -> usize {
4084    value.to_string().parse().unwrap_or(usize::MAX)
4085}
4086
4087fn component_event_callbacks(
4088    component: &crate::ComponentDefinition,
4089    props: &Map,
4090) -> Vec<(String, FnPtr)> {
4091    component
4092        .schema
4093        .events
4094        .keys()
4095        .filter_map(|event| {
4096            props
4097                .get(format!("on_{event}").as_str())
4098                .filter(|value| value.is::<FnPtr>())
4099                .map(|value| (event.clone(), value.clone_cast::<FnPtr>()))
4100        })
4101        .collect()
4102}
4103
4104fn bind_component_callback_props(
4105    props: &mut Map,
4106    schema: &BTreeMap<String, crate::ObjectField>,
4107    binding: &ComponentCallbackBinding,
4108) {
4109    for (name, field) in schema {
4110        if schema_contains_callback(&field.schema)
4111            && let Some(value) = props.get_mut(name.as_str())
4112        {
4113            bind_component_callback_value(&field.schema, value, binding);
4114        }
4115    }
4116}
4117
4118fn schema_contains_callback(schema: &crate::ValueSchema) -> bool {
4119    match schema {
4120        crate::ValueSchema::Callback => true,
4121        crate::ValueSchema::Array { items, .. }
4122        | crate::ValueSchema::Map { values: items }
4123        | crate::ValueSchema::Optional { value: items } => schema_contains_callback(items),
4124        crate::ValueSchema::Object { fields, .. } => fields
4125            .values()
4126            .any(|field| schema_contains_callback(&field.schema)),
4127        crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_callback),
4128        _ => false,
4129    }
4130}
4131
4132fn bind_component_callback_value(
4133    schema: &crate::ValueSchema,
4134    value: &mut Dynamic,
4135    binding: &ComponentCallbackBinding,
4136) {
4137    match schema {
4138        crate::ValueSchema::Callback if value.is::<FnPtr>() => {
4139            let mut function = value.clone_cast::<FnPtr>();
4140            if !function
4141                .iter_curry()
4142                .any(Dynamic::is::<ComponentCallbackBinding>)
4143            {
4144                function.add_curry(Dynamic::from(binding.clone()));
4145                *value = Dynamic::from(function);
4146            }
4147        }
4148        crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4149            bind_component_callback_value(inner, value, binding);
4150        }
4151        crate::ValueSchema::OneOf { variants } => {
4152            if let Some(variant) = variants
4153                .iter()
4154                .find(|variant| variant.validate(value).is_ok())
4155            {
4156                bind_component_callback_value(variant, value, binding);
4157            }
4158        }
4159        crate::ValueSchema::Array { items, .. }
4160            if schema_contains_callback(items) && value.is::<Array>() =>
4161        {
4162            let mut values = value.clone_cast::<Array>();
4163            for value in &mut values {
4164                bind_component_callback_value(items, value, binding);
4165            }
4166            *value = Dynamic::from_array(values);
4167        }
4168        crate::ValueSchema::Map {
4169            values: item_schema,
4170        } if schema_contains_callback(item_schema) && value.is::<Map>() => {
4171            let mut values = value.clone_cast::<Map>();
4172            for value in values.values_mut() {
4173                bind_component_callback_value(item_schema, value, binding);
4174            }
4175            *value = Dynamic::from_map(values);
4176        }
4177        crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4178            let mut values = value.clone_cast::<Map>();
4179            for (name, field) in fields {
4180                if schema_contains_callback(&field.schema)
4181                    && let Some(value) = values.get_mut(name.as_str())
4182                {
4183                    bind_component_callback_value(&field.schema, value, binding);
4184                }
4185            }
4186            *value = Dynamic::from_map(values);
4187        }
4188        _ => {}
4189    }
4190}
4191
4192fn bind_component_node_props(
4193    props: &mut Map,
4194    schema: &BTreeMap<String, crate::ObjectField>,
4195    binding: &ComponentCallbackBinding,
4196) {
4197    for (name, field) in schema {
4198        if schema_contains_node(&field.schema)
4199            && let Some(value) = props.get_mut(name.as_str())
4200        {
4201            bind_component_node_value(&field.schema, value, binding);
4202        }
4203    }
4204}
4205
4206fn schema_contains_node(schema: &crate::ValueSchema) -> bool {
4207    match schema {
4208        crate::ValueSchema::Node => true,
4209        crate::ValueSchema::Array { items, .. }
4210        | crate::ValueSchema::Map { values: items }
4211        | crate::ValueSchema::Optional { value: items } => schema_contains_node(items),
4212        crate::ValueSchema::Object { fields, .. } => fields
4213            .values()
4214            .any(|field| schema_contains_node(&field.schema)),
4215        crate::ValueSchema::OneOf { variants } => variants.iter().any(schema_contains_node),
4216        _ => false,
4217    }
4218}
4219
4220fn bind_component_node_value(
4221    schema: &crate::ValueSchema,
4222    value: &mut Dynamic,
4223    binding: &ComponentCallbackBinding,
4224) {
4225    match schema {
4226        crate::ValueSchema::Node if value.is::<UiNode>() => {
4227            let mut node = value.clone_cast::<UiNode>();
4228            node.bind_component_scope(
4229                &binding.component,
4230                binding.incarnation,
4231                &binding.events,
4232                binding.context.as_ref(),
4233            );
4234            node.activate_component_snapshots();
4235            *value = Dynamic::from(node);
4236        }
4237        crate::ValueSchema::Optional { value: inner } if !value.is_unit() => {
4238            bind_component_node_value(inner, value, binding);
4239        }
4240        crate::ValueSchema::OneOf { variants } => {
4241            if let Some(variant) = variants
4242                .iter()
4243                .find(|variant| variant.validate(value).is_ok())
4244            {
4245                bind_component_node_value(variant, value, binding);
4246            }
4247        }
4248        crate::ValueSchema::Array { items, .. }
4249            if schema_contains_node(items) && value.is::<Array>() =>
4250        {
4251            let mut values = value.clone_cast::<Array>();
4252            for value in &mut values {
4253                bind_component_node_value(items, value, binding);
4254            }
4255            *value = Dynamic::from_array(values);
4256        }
4257        crate::ValueSchema::Map {
4258            values: item_schema,
4259        } if schema_contains_node(item_schema) && value.is::<Map>() => {
4260            let mut values = value.clone_cast::<Map>();
4261            for value in values.values_mut() {
4262                bind_component_node_value(item_schema, value, binding);
4263            }
4264            *value = Dynamic::from_map(values);
4265        }
4266        crate::ValueSchema::Object { fields, .. } if value.is::<Map>() => {
4267            let mut values = value.clone_cast::<Map>();
4268            for (name, field) in fields {
4269                if schema_contains_node(&field.schema)
4270                    && let Some(value) = values.get_mut(name.as_str())
4271                {
4272                    bind_component_node_value(&field.schema, value, binding);
4273                }
4274            }
4275            *value = Dynamic::from_map(values);
4276        }
4277        _ => {}
4278    }
4279}
4280
4281fn hydrate_replayed_node_value(value: &mut Dynamic) {
4282    if value.is::<UiNode>() {
4283        let mut node = value.clone_cast::<UiNode>();
4284        node.hydrate_component_subtrees();
4285        *value = Dynamic::from(node);
4286    } else if value.is::<Array>() {
4287        let mut values = value.clone_cast::<Array>();
4288        for value in &mut values {
4289            hydrate_replayed_node_value(value);
4290        }
4291        *value = Dynamic::from_array(values);
4292    } else if value.is::<Map>() {
4293        let mut values = value.clone_cast::<Map>();
4294        for value in values.values_mut() {
4295            hydrate_replayed_node_value(value);
4296        }
4297        *value = Dynamic::from_map(values);
4298    }
4299}
4300
4301fn component_part_styles(props: &Map) -> BTreeMap<String, crate::Style> {
4302    props
4303        .get("part_styles")
4304        .filter(|value| value.is::<Map>())
4305        .map(Dynamic::clone_cast::<Map>)
4306        .unwrap_or_default()
4307        .into_iter()
4308        .filter_map(|(name, value)| {
4309            value
4310                .is::<crate::Style>()
4311                .then(|| (name.to_string(), value.cast::<crate::Style>()))
4312        })
4313        .collect()
4314}
4315
4316fn component_root_style(props: &Map) -> Option<crate::Style> {
4317    props
4318        .get("style")
4319        .filter(|value| value.is::<crate::Style>())
4320        .map(Dynamic::clone_cast::<crate::Style>)
4321}
4322
4323fn register_component_event_callbacks(
4324    active: &ActiveComponentRenderState,
4325    component: &ComponentInstancePath,
4326    caller: &UiContext,
4327    callbacks: Vec<(String, FnPtr)>,
4328    native_context: &crate::invocation::ScriptInvocationContext,
4329) -> Result<(), Box<EvalAltResult>> {
4330    let mut guard = active.try_borrow_mut().map_err(|_| {
4331        Box::new(component_render_error(
4332            "component render stack is already borrowed",
4333        ))
4334    })?;
4335    let active = guard.as_mut().ok_or_else(|| {
4336        Box::new(component_render_error(
4337            "formal component rendering may run only inside view",
4338        ))
4339    })?;
4340    for (event, function) in callbacks {
4341        let mut callback = ScriptCallback::try_from_fn_ptr(function, active.generation)
4342            .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4343        callback.bind_component_scope_if_unset(
4344            caller.component_path(),
4345            caller.component_incarnation(),
4346            caller.event_schemas().clone(),
4347        );
4348        callback.bind_native_context_if_unset(native_context.clone());
4349        active
4350            .event_handlers
4351            .insert((component.clone(), event), callback);
4352    }
4353    Ok(())
4354}
4355
4356fn component_render_error(message: impl Into<String>) -> EvalAltResult {
4357    EvalAltResult::ErrorRuntime(Dynamic::from(message.into()), Position::NONE)
4358}
4359
4360#[cfg(feature = "charts")]
4361fn register_chart_validation_api(engine: &mut Engine) {
4362    engine.register_fn(
4363        "chart_validate",
4364        |spec: Map, data: Dynamic, key_dimension: Dynamic| -> Result<(), Box<EvalAltResult>> {
4365            let spec = UiValue::from_dynamic(Dynamic::from_map(spec))
4366                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4367            crate::ChartSpec::from_ui_value(&spec)
4368                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4369            let key_dimension = if key_dimension.is_unit() {
4370                None
4371            } else if key_dimension.is::<ImmutableString>() {
4372                Some(key_dimension.cast::<ImmutableString>().to_string())
4373            } else {
4374                return Err(Box::new(component_render_error(
4375                    "chart key_dimension must be a string or null",
4376                )));
4377            };
4378            if data.is::<crate::NativeChartData>() {
4379                return Ok(());
4380            }
4381            if !data.is::<Array>() {
4382                return Err(Box::new(component_render_error(
4383                    "chart data must be an array of objects or NativeChartData",
4384                )));
4385            }
4386            let data = UiValue::from_dynamic(Dynamic::from_array(data.cast::<Array>()))
4387                .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4388            let UiValue::Array(rows) = data else {
4389                unreachable!()
4390            };
4391            if rows.len() > 10_000 {
4392                return Err(Box::new(component_render_error(
4393                    "inline chart data is limited to 10000 rows; use NativeChartData",
4394                )));
4395            }
4396            let rows = rows
4397                .into_iter()
4398                .enumerate()
4399                .map(|(index, row)| match row {
4400                    UiValue::Map(row) => Ok(row),
4401                    _ => Err(Box::new(component_render_error(format!(
4402                        "chart inline row {index} must be an object"
4403                    )))),
4404                })
4405                .collect::<Result<Vec<_>, _>>()?;
4406            crate::ChartDataset::from_rows(
4407                "main",
4408                &rows,
4409                key_dimension,
4410                crate::ChartDataLimits::default(),
4411            )
4412            .map_err(|error| Box::new(component_render_error(error.to_string())))?;
4413            Ok(())
4414        },
4415    );
4416}
4417
4418#[cfg(test)]
4419mod tests {
4420    use super::*;
4421
4422    fn virtual_config(sticky: &[i64], alignment: &str) -> Map {
4423        let mut item = Map::new();
4424        item.insert("key".into(), Dynamic::from("header"));
4425        let mut config = Map::new();
4426        config.insert("key".into(), Dynamic::from("sections"));
4427        config.insert(
4428            "data".into(),
4429            Dynamic::from_array(vec![Dynamic::from_map(item)]),
4430        );
4431        config.insert("estimated_height".into(), Dynamic::from_float(30.0));
4432        config.insert("height".into(), Dynamic::from_float(120.0));
4433        config.insert("alignment".into(), Dynamic::from(alignment.to_owned()));
4434        config.insert(
4435            "sticky_headers".into(),
4436            Dynamic::from_array(sticky.iter().copied().map(Dynamic::from_int).collect()),
4437        );
4438        config
4439    }
4440
4441    #[test]
4442    fn sticky_virtual_collection_indices_are_bounded_unique_and_top_aligned() {
4443        let decoded = decode_virtual_collection(virtual_config(&[0], "top")).unwrap();
4444        assert_eq!(decoded.sticky_headers.as_ref(), &BTreeSet::from([0]));
4445
4446        for (sticky, alignment, expected) in [
4447            (&[1][..], "top", "outside 1 items"),
4448            (&[0, 0][..], "top", "duplicate index 0"),
4449            (&[0][..], "bottom", "require top-aligned"),
4450        ] {
4451            let error = decode_virtual_collection(virtual_config(sticky, alignment)).unwrap_err();
4452            assert!(error.to_string().contains(expected), "{error}");
4453        }
4454    }
4455
4456    #[test]
4457    fn virtual_collection_reveal_key_is_known_and_has_one_scroll_owner() {
4458        let mut config = virtual_config(&[], "top");
4459        config.insert("reveal_key".into(), Dynamic::from("header"));
4460        let decoded = decode_virtual_collection(config).unwrap();
4461        assert_eq!(decoded.reveal_key.as_deref(), Some("header"));
4462
4463        let mut unknown = virtual_config(&[], "top");
4464        unknown.insert("reveal_key".into(), Dynamic::from("missing"));
4465        let error = decode_virtual_collection(unknown).unwrap_err();
4466        assert!(error.to_string().contains("is not present"), "{error}");
4467
4468        let mut competing = virtual_config(&[], "top");
4469        competing.insert("reveal_key".into(), Dynamic::from("header"));
4470        competing.insert("follow_tail".into(), Dynamic::from(true));
4471        let error = decode_virtual_collection(competing).unwrap_err();
4472        assert!(error.to_string().contains("must not combine"), "{error}");
4473    }
4474
4475    #[test]
4476    fn script_builds_a_declarative_tree() {
4477        let mut runtime = RuntimeEngine::new();
4478        let compiled = runtime
4479            .compile(
4480                r#"
4481                    fn view() {
4482                        column([
4483                            text("Hello").with_key("greeting"),
4484                            text("from Rhai")
4485                        ])
4486                    }
4487                "#,
4488            )
4489            .expect("script should compile");
4490
4491        let root = runtime.render(&compiled).expect("view should render");
4492        assert_eq!(
4493            root.source().map(|source| source.module.as_str()),
4494            Some("ui/main.rhai")
4495        );
4496        let crate::UiNodeKind::Box { children } = root.kind() else {
4497            panic!("column must render a box");
4498        };
4499        assert_eq!(children.len(), 2);
4500        assert_eq!(
4501            children[0].key().map(crate::NodeKey::as_str),
4502            Some("greeting")
4503        );
4504        assert!(children.iter().all(|child| child.source().is_some()));
4505    }
4506
4507    #[test]
4508    fn runtime_emits_official_rhai_language_server_definitions() {
4509        let runtime = RuntimeEngine::new();
4510        let definitions = runtime.definition_source();
4511        assert!(definitions.starts_with("module static;"));
4512        assert!(definitions.contains("fn box"));
4513        assert!(definitions.contains("fn timeout"));
4514        assert!(definitions.contains("fn canvas_fill_path"));
4515        assert!(definitions.contains("UiNode"));
4516    }
4517
4518    #[test]
4519    fn retained_callbacks_reject_capturing_functions_and_non_data_curry() {
4520        let mut runtime = RuntimeEngine::new();
4521        let compiled = runtime
4522            .compile(r#"fn view() { text("x").on_click(|| ()) }"#)
4523            .unwrap();
4524        let error = runtime.render(&compiled).unwrap_err();
4525        assert!(
4526            error
4527                .to_string()
4528                .contains("anonymous or capturing functions")
4529        );
4530
4531        let mut callback = FnPtr::new("clicked").unwrap();
4532        callback.add_curry(Dynamic::from(UiNode::text("not durable data")));
4533        assert!(matches!(
4534            ScriptCallback::try_from_fn_ptr(callback, ScriptGeneration::initial()),
4535            Err(ScriptCallbackDefinitionError::InvalidCurry { index: 0, .. })
4536        ));
4537    }
4538
4539    #[test]
4540    fn replay_hydrates_formal_subtrees_inside_nested_dynamic_node_shapes() {
4541        let root = ComponentInstancePath::root("View", "main");
4542        let outer = root.child("Outer", "outer");
4543        let inner = root.child("Inner", "inner");
4544        let inner_snapshot = crate::node::ComponentOwnedSnapshot::default();
4545        let mut current_inner = UiNode::text("current")
4546            .with_component_root_snapshot(inner.clone(), inner_snapshot.reference());
4547        current_inner.activate_component_snapshots();
4548        let outer_snapshot = crate::node::ComponentOwnedSnapshot::default();
4549        let mut current_outer = UiNode::box_node(vec![
4550            UiNode::text("outer"),
4551            UiNode::text("stale-inner")
4552                .with_component_root_snapshot(inner, inner_snapshot.reference()),
4553        ])
4554        .with_component_root_snapshot(outer.clone(), outer_snapshot.reference());
4555        current_outer.activate_component_snapshots();
4556        let stale = UiNode::box_node(vec![
4557            UiNode::text("stale-outer")
4558                .with_component_root_snapshot(outer, outer_snapshot.reference())
4559                .with_attribute("presentation", UiValue::Bool(true)),
4560        ]);
4561        let mut nested = Map::new();
4562        nested.insert(
4563            "object".into(),
4564            Dynamic::from_map(Map::from_iter([(
4565                "nodes".into(),
4566                Dynamic::from_array(vec![Dynamic::from(stale)]),
4567            )])),
4568        );
4569        let mut value = Dynamic::from_map(nested);
4570
4571        hydrate_replayed_node_value(&mut value);
4572
4573        let nested = value.cast::<Map>();
4574        let object = nested["object"].clone_cast::<Map>();
4575        let nodes = object["nodes"].clone_cast::<Array>();
4576        let node = nodes[0].clone_cast::<UiNode>();
4577        let crate::UiNodeKind::Box { children } = node.kind() else {
4578            panic!("nested node shape must preserve its raw wrapper");
4579        };
4580        assert_eq!(
4581            children[0].attributes().get("presentation"),
4582            Some(&UiValue::Bool(true))
4583        );
4584        let crate::UiNodeKind::Box { children } = children[0].kind() else {
4585            panic!("outer component must hydrate its owned snapshot");
4586        };
4587        assert!(
4588            matches!(children[1].kind(), crate::UiNodeKind::Text { text } if text == "current")
4589        );
4590    }
4591
4592    #[test]
4593    fn final_component_api_registers_render_once_and_invokes_by_id() {
4594        let mut runtime = RuntimeEngine::new();
4595        let compiled = runtime
4596            .compile(
4597                r#"
4598                    define_component(#{
4599                        metadata: #{
4600                            id: "components/message", "export": "Message", version: "0.1.0",
4601                            runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
4602                            dependencies: [], capabilities: #{}
4603                        },
4604                        schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
4605                        render: Fn("render_Message")
4606                    });
4607                    fn Message(props) { render_component("components/message", props) }
4608                    fn render_Message(ctx, props) { text("final component api") }
4609                    fn view() { Message(#{ key: "message" }) }
4610                "#,
4611            )
4612            .unwrap();
4613        let root = runtime.render(&compiled).unwrap();
4614        assert!(matches!(
4615            root.kind(),
4616            crate::UiNodeKind::Text { text } if text == "final component api"
4617        ));
4618        let recipe = runtime.component_invocations().next().unwrap();
4619        assert_eq!(recipe.render_name(), "render_Message");
4620    }
4621
4622    #[test]
4623    fn self_contained_compile_rejects_dynamic_imports_before_evaluation() {
4624        let mut runtime = RuntimeEngine::new();
4625        assert!(matches!(
4626            runtime.compile_self_contained_named(
4627                "ui/dynamic_import.rhai",
4628                "let path = \"components/button\"; import path as button;",
4629            ),
4630            Err(RuntimeError::Import(_))
4631        ));
4632    }
4633
4634    #[test]
4635    fn script_builds_an_error_boundary() {
4636        let mut runtime = RuntimeEngine::new();
4637        let compiled = runtime
4638            .compile(
4639                r#"
4640                    fn view() {
4641                        error_boundary(text("content"), text("fallback"))
4642                    }
4643                "#,
4644            )
4645            .unwrap();
4646        let root = runtime.render(&compiled).unwrap();
4647        assert!(matches!(
4648            root.kind(),
4649            crate::UiNodeKind::ErrorBoundary { .. }
4650        ));
4651    }
4652
4653    #[test]
4654    fn lazy_error_boundary_catches_rhai_evaluation_failure() {
4655        let mut runtime = RuntimeEngine::new();
4656        let compiled = runtime
4657            .compile(
4658                r#"
4659                    fn broken() { throw "boom"; }
4660                    fn fallback() { text("fallback") }
4661                    fn view() { error_boundary_lazy(Fn("broken"), Fn("fallback")) }
4662                "#,
4663            )
4664            .unwrap();
4665        let root = runtime.render(&compiled).unwrap();
4666        assert!(matches!(root.kind(), crate::UiNodeKind::Text { text } if text == "fallback"));
4667        assert!(matches!(
4668            root.attributes().get("boundary_error"),
4669            Some(crate::UiValue::String(message)) if message.contains("boom")
4670        ));
4671    }
4672
4673    #[test]
4674    fn script_builds_a_typed_controlled_overlay() {
4675        let mut runtime = RuntimeEngine::new();
4676        let compiled = runtime
4677            .compile(
4678                r#"
4679                    fn changed(ctx, open) { () }
4680                    fn view() {
4681                        overlay(
4682                            text("trigger"),
4683                            text("content"),
4684                            #{
4685                                id: "account",
4686                                kind: "popover",
4687                                placement: "bottom",
4688                                open: true,
4689                                gap: 6
4690                            }
4691                        ).on_open_change(Fn("changed"))
4692                    }
4693                "#,
4694            )
4695            .unwrap();
4696        let root = runtime.render(&compiled).unwrap();
4697        assert!(matches!(
4698            root.kind(),
4699            crate::UiNodeKind::Overlay { spec, .. }
4700                if spec.id.as_str() == "account"
4701                    && spec.open
4702                    && (spec.gap - 6.0).abs() < f64::EPSILON
4703                    && spec.placement == crate::OverlayPlacement::Bottom
4704        ));
4705        assert_eq!(
4706            root.handler("open_change")
4707                .unwrap()
4708                .as_script()
4709                .unwrap()
4710                .generation(),
4711            compiled.generation()
4712        );
4713    }
4714
4715    #[test]
4716    fn click_handlers_can_carry_typed_semantic_payloads() {
4717        let mut runtime = RuntimeEngine::new();
4718        let compiled = runtime
4719            .compile(
4720                r#"
4721                    fn changed(ctx, value) { () }
4722                    fn view() {
4723                        text("toggle").on_click_value(
4724                            Fn("changed"), #{ checked: true, source: "pointer" }
4725                        )
4726                    }
4727                "#,
4728            )
4729            .unwrap();
4730        let root = runtime.render(&compiled).unwrap();
4731        assert_eq!(
4732            root.handler_payload("click"),
4733            Some(&crate::UiValue::Map(BTreeMap::from([
4734                ("checked".to_owned(), crate::UiValue::Bool(true)),
4735                (
4736                    "source".to_owned(),
4737                    crate::UiValue::String("pointer".to_owned()),
4738                ),
4739            ])))
4740        );
4741        assert!(root.handlers().contains_key("click"));
4742    }
4743
4744    #[test]
4745    fn rhai_nodes_retain_ordered_handlers_for_all_event_phases() {
4746        let mut runtime = RuntimeEngine::new();
4747        let compiled = runtime
4748            .compile(
4749                r#"
4750                    fn first(ctx, event) { event_response().prevent_default() }
4751                    fn second(ctx, event) { event_response().stop_immediate() }
4752                    fn capture(ctx, event) { propagate() }
4753                    fn view() {
4754                        text("drag")
4755                            .on_capture("pointer_down", Fn("capture"))
4756                            .on("pointer_down", Fn("first"))
4757                            .on("pointer_down", Fn("second"))
4758                    }
4759                "#,
4760            )
4761            .unwrap();
4762        let root = runtime.render(&compiled).unwrap();
4763        let bindings = root.event_handlers("pointer_down");
4764        assert_eq!(bindings.len(), 3);
4765        assert_eq!(bindings[0].phase(), crate::EventPhase::Capture);
4766        assert_eq!(bindings[1].phase(), crate::EventPhase::Target);
4767        assert_eq!(bindings[2].phase(), crate::EventPhase::Target);
4768        assert_eq!(bindings[2].handler().as_script().unwrap().name(), "second");
4769    }
4770
4771    #[test]
4772    fn rhai_nodes_attach_only_registered_schema_checked_native_handlers() {
4773        let mut runtime = RuntimeEngine::new();
4774        let id = crate::NativeHandlerId::parse("timeline.drag").unwrap();
4775        runtime
4776            .register_native_handler(
4777                crate::NativeHandlerDescriptor::new(
4778                    id,
4779                    BTreeMap::from([("pointer_down".to_owned(), crate::ValueSchema::UiValue)]),
4780                )
4781                .unwrap(),
4782                |_, _, _, _| Ok(crate::EventResponse::new().capture_pointer()),
4783            )
4784            .unwrap();
4785        let compiled = runtime
4786            .compile(
4787                r#"
4788                    fn view() {
4789                        text("drag").on(
4790                            "pointer_down", native_handler("timeline.drag")
4791                        )
4792                    }
4793                "#,
4794            )
4795            .unwrap();
4796        let root = runtime.render(&compiled).unwrap();
4797        assert!(matches!(
4798            root.handler("pointer_down"),
4799            Some(crate::UiEventHandler::Native(reference))
4800                if reference.descriptor().id.as_str() == "timeline.drag"
4801        ));
4802
4803        let input = runtime
4804            .compile(
4805                r#"
4806                    fn view() {
4807                        gpui_rhai::TextInputPrimitive(#{
4808                            key: "native-input", value: "", placeholder: "Edit",
4809                            disabled: false, read_only: false, typography: "body",
4810                            on_change: native_handler("timeline.drag")
4811                        })
4812                    }
4813                "#,
4814            )
4815            .unwrap();
4816        assert!(
4817            runtime.render(&input).is_ok(),
4818            "native handlers must cross formal component and primitive callback props"
4819        );
4820
4821        let missing = runtime
4822            .compile(
4823                "fn view() { text(\"x\").on(\"pointer_down\", native_handler(\"app.missing\")) }",
4824            )
4825            .unwrap();
4826        assert!(runtime.render(&missing).is_err());
4827    }
4828
4829    #[test]
4830    fn final_box_and_fragment_atoms_are_distinct_snapshots() {
4831        let mut runtime = RuntimeEngine::new();
4832        let compiled = runtime
4833            .compile(
4834                r#"
4835                    fn view() {
4836                        box([text("a"), fragment([text("b"), text("c")])])
4837                    }
4838                "#,
4839            )
4840            .unwrap();
4841        let root = runtime.render(&compiled).unwrap();
4842        let crate::UiNodeKind::Box { children } = root.kind() else {
4843            panic!("root must be the final Box atom");
4844        };
4845        assert!(matches!(
4846            children[1].kind(),
4847            crate::UiNodeKind::Fragment { children } if children.len() == 2
4848        ));
4849    }
4850
4851    #[test]
4852    fn text_accepts_typed_inline_span_runs() {
4853        let mut runtime = RuntimeEngine::new();
4854        let compiled = runtime
4855            .compile(
4856                r#"
4857                    fn view() {
4858                        text([
4859                            span("Hello ").bold(),
4860                            span("world").color(rgb(0x22cc88)).italic()
4861                        ])
4862                    }
4863                "#,
4864            )
4865            .unwrap();
4866        let root = runtime.render(&compiled).unwrap();
4867        let crate::UiNodeKind::RichText { text, spans } = root.kind() else {
4868            panic!("typed spans must produce rich text");
4869        };
4870        assert_eq!(text.as_str(), "Hello world");
4871        assert_eq!(spans.len(), 2);
4872        assert!(spans[0].is_bold());
4873        assert!(spans[1].is_italic());
4874    }
4875
4876    #[test]
4877    fn canvas_scene_is_keyed_validated_and_script_constructible() {
4878        let mut runtime = RuntimeEngine::new();
4879        let compiled = runtime
4880            .compile(
4881                r#"
4882                    fn view() {
4883                        canvas(canvas_scene([
4884                            canvas_rect("panel", 0.0, 0.0, 100.0, 40.0, rgb(0x112233)),
4885                            canvas_circle("dot", 20.0, 20.0, 8.0, rgb(0xffcc00)),
4886                            canvas_line("axis", 0.0, 39.0, 100.0, 39.0, 1.0, rgb(0xffffff))
4887                        ]))
4888                    }
4889                "#,
4890            )
4891            .unwrap();
4892        let root = runtime.render(&compiled).unwrap();
4893        assert!(matches!(
4894            root.kind(),
4895            crate::UiNodeKind::Canvas { scene } if scene.commands().len() == 3
4896        ));
4897    }
4898
4899    #[test]
4900    fn inline_svg_atom_is_bounded_and_self_contained() {
4901        let mut runtime = RuntimeEngine::new();
4902        let compiled = runtime
4903            .compile(
4904                r#"
4905                    fn view() {
4906                        svg("<svg viewBox='0 0 10 10'><path fill='currentColor' d='M0 0L10 10'/></svg>")
4907                    }
4908                "#,
4909            )
4910            .unwrap();
4911        let root = runtime.render(&compiled).unwrap();
4912        assert!(matches!(
4913            root.kind(),
4914            crate::UiNodeKind::Svg { source } if source.as_str().contains("currentColor")
4915        ));
4916
4917        let rejected = runtime
4918            .compile(r#"fn view() { svg("<svg><image href='https://example.com/a.png'/></svg>") }"#)
4919            .unwrap();
4920        assert!(runtime.render(&rejected).is_err());
4921    }
4922
4923    #[test]
4924    fn recompilation_invalidates_previous_generation() {
4925        let mut runtime = RuntimeEngine::new();
4926        let first = runtime.compile("fn view() { text(\"first\") }").unwrap();
4927        runtime.render(&first).unwrap();
4928        let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
4929
4930        assert!(runtime.is_current(first.generation()));
4931        assert!(!runtime.is_current(second.generation()));
4932        runtime.render(&second).unwrap();
4933        assert!(!runtime.is_current(first.generation()));
4934        assert!(runtime.is_current(second.generation()));
4935    }
4936
4937    #[test]
4938    fn callbacks_are_rejected_after_reload() {
4939        let mut runtime = RuntimeEngine::new();
4940        let first = runtime
4941            .compile(
4942                r#"
4943                    fn view() { text("first") }
4944                    fn add_suffix(value) { value + "!" }
4945                "#,
4946            )
4947            .unwrap();
4948        runtime.render(&first).unwrap();
4949        let callback = runtime.callback(&first, "add_suffix").unwrap();
4950
4951        let value = runtime
4952            .invoke_callback(&first, &callback, ("hello",))
4953            .unwrap();
4954        assert_eq!(value.into_immutable_string().unwrap(), "hello!");
4955
4956        let second = runtime.compile("fn view() { text(\"second\") }").unwrap();
4957        runtime.render(&second).unwrap();
4958        assert!(matches!(
4959            runtime.invoke_callback(&second, &callback, ("hello",)),
4960            Err(RuntimeError::StaleCallback { .. })
4961        ));
4962    }
4963
4964    #[test]
4965    fn failed_candidate_keeps_last_good_generation_active() {
4966        let mut runtime = RuntimeEngine::new();
4967        let active = runtime
4968            .compile(
4969                r#"
4970                    fn view() { text("active") }
4971                    fn action(value) { value }
4972                "#,
4973            )
4974            .unwrap();
4975        runtime.render(&active).unwrap();
4976        let callback = runtime.callback(&active, "action").unwrap();
4977
4978        let candidate = runtime
4979            .compile("fn view() { throw \"render failed\"; }")
4980            .unwrap();
4981        assert!(runtime.render(&candidate).is_err());
4982        assert!(runtime.is_current(active.generation()));
4983        let _ = runtime
4984            .invoke_callback(&active, &callback, ("still active",))
4985            .unwrap();
4986    }
4987
4988    #[test]
4989    fn failed_nested_component_records_the_deepest_component_path() {
4990        let mut runtime = RuntimeEngine::new();
4991        let compiled = runtime
4992            .compile(
4993                r#"
4994                    define_component(#{
4995                        metadata: #{ id: "components/broken", "export": "Broken",
4996                            version: "0.1.3",
4997                            runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
4998                            dependencies: [], capabilities: #{} },
4999                        schema: #{ props: #{}, state: #{ fields: #{} }, events: #{},
5000                            slots: #{}, parts: [] },
5001                        render: Fn("render_Broken")
5002                    });
5003                    fn Broken(props) { render_component("components/broken", props) }
5004                    fn render_Broken(ctx, props) { throw "nested failure"; }
5005                    fn view() { Broken(#{ key: "inner" }) }
5006                "#,
5007            )
5008            .unwrap();
5009
5010        assert!(runtime.render(&compiled).is_err());
5011        assert_eq!(
5012            runtime
5013                .last_failed_component()
5014                .as_ref()
5015                .map(ToString::to_string),
5016            Some("/App[root]/Broken[inner]".to_owned())
5017        );
5018        assert!(runtime.last_failed_timing().is_some());
5019
5020        let recovered = runtime
5021            .compile("fn view() { text(\"recovered\") }")
5022            .unwrap();
5023        runtime.render(&recovered).unwrap();
5024        assert_eq!(runtime.last_failed_component(), None);
5025        assert_eq!(runtime.last_failed_timing(), None);
5026    }
5027
5028    #[test]
5029    fn execution_timings_cover_compile_and_render() {
5030        let mut runtime = RuntimeEngine::new();
5031        runtime.set_slow_threshold(Duration::ZERO);
5032        let compiled = runtime
5033            .compile(
5034                r#"
5035                    fn view() { text("timed") }
5036                    fn counted() { let total = 0; for value in 0..100 { total += value; } total }
5037                "#,
5038            )
5039            .unwrap();
5040        runtime.render(&compiled).unwrap();
5041        let callback = runtime.callback(&compiled, "counted").unwrap();
5042        let _ = runtime.invoke_callback(&compiled, &callback, ()).unwrap();
5043        let timings = runtime.take_timings();
5044        assert_eq!(timings.len(), 3);
5045        assert_eq!(timings[0].operation, ExecutionOperation::Compile);
5046        assert_eq!(timings[1].operation, ExecutionOperation::Render);
5047        assert_eq!(
5048            timings[2].operation,
5049            ExecutionOperation::Callback("counted".to_owned())
5050        );
5051        assert_eq!(timings[0].operations, 0);
5052        assert!(timings[1].operations > 0);
5053        assert!(timings[2].operations > timings[1].operations);
5054        assert!(timings.iter().all(|timing| timing.slow && timing.succeeded));
5055    }
5056
5057    #[test]
5058    fn independent_candidates_never_share_callback_authority() {
5059        let mut runtime = RuntimeEngine::new();
5060        let first = runtime
5061            .compile("fn view() { text(\"first\") } fn action() { 1 }")
5062            .unwrap();
5063        let callback = runtime.callback(&first, "action").unwrap();
5064        let second = runtime
5065            .compile("fn view() { text(\"second\") } fn action() { 2 }")
5066            .unwrap();
5067        assert_ne!(first.generation(), second.generation());
5068        runtime.render(&second).unwrap();
5069        assert!(matches!(
5070            runtime.invoke_callback(&second, &callback, ()),
5071            Err(RuntimeError::StaleCallback { .. })
5072        ));
5073    }
5074
5075    #[test]
5076    fn callbacks_cannot_cross_runtime_engines() {
5077        let mut first_engine = RuntimeEngine::new();
5078        let first = first_engine
5079            .compile("fn view() { text(\"first\") } fn action() { 1 }")
5080            .unwrap();
5081        first_engine.render(&first).unwrap();
5082        let callback = first_engine.callback(&first, "action").unwrap();
5083
5084        let mut second_engine = RuntimeEngine::new();
5085        let second = second_engine
5086            .compile("fn view() { text(\"second\") } fn action() { 2 }")
5087            .unwrap();
5088        second_engine.render(&second).unwrap();
5089        assert!(matches!(
5090            second_engine.invoke_callback(&second, &callback, ()),
5091            Err(RuntimeError::StaleCallback { .. })
5092        ));
5093    }
5094
5095    #[test]
5096    fn one_program_preparation_shares_identity_across_module_and_entry_compilation() {
5097        let mut runtime = RuntimeEngine::new();
5098        let (module, entry) = runtime
5099            .with_program_preparation(|runtime| {
5100                Ok::<_, RuntimeError>((
5101                    runtime.compile_named("module", "fn helper() { 1 }")?,
5102                    runtime.compile_named("entry", "fn view() { text(\"entry\") }")?,
5103                ))
5104            })
5105            .unwrap();
5106        assert_eq!(module.generation(), entry.generation());
5107        let next = runtime.compile("fn view() { text(\"next\") }").unwrap();
5108        assert_ne!(entry.generation(), next.generation());
5109    }
5110
5111    #[test]
5112    fn unsafe_const_container_assignment_is_rejected_before_evaluation() {
5113        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5114            RuntimeEngine::new()
5115                .compile("fn view() { const m = #{ x: 1 }; m.x = 2; text(\"done\") }")
5116        }));
5117        assert!(matches!(
5118            result,
5119            Ok(Err(RuntimeError::InvalidAssignmentTarget(_)))
5120        ));
5121    }
5122
5123    #[test]
5124    fn default_runtime_rejects_filesystem_imports() {
5125        let directory = tempfile::tempdir().unwrap();
5126        let module = directory.path().join("external.rhai");
5127        std::fs::write(&module, "fn value() { \"external\" }").unwrap();
5128        let mut runtime = RuntimeEngine::new();
5129        let source = format!(
5130            "import {:?} as external; fn view() {{ text(external::value()) }}",
5131            module.with_extension("").to_string_lossy()
5132        );
5133        let compiled = runtime.compile(&source).unwrap();
5134        assert!(runtime.render(&compiled).is_err());
5135    }
5136
5137    #[test]
5138    fn nested_component_work_consumes_one_shared_operation_budget() {
5139        let module = r#"
5140            define_component(#{
5141                metadata: #{ id: "components/heavy", "export": "Heavy", version: "0.1.1",
5142                    runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5143                schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5144                render: Fn("render_Heavy")
5145            });
5146            fn Heavy(props) { render_component("components/heavy", props) }
5147            fn render_Heavy(ctx, props) {
5148                let n = 0;
5149                for i in 0..200000 { n += 1; }
5150                text(`${n}`)
5151            }
5152        "#;
5153        let mut resolver = crate::RestrictedModuleResolver::new();
5154        resolver.insert("components/heavy", module).unwrap();
5155        let mut runtime = RuntimeEngine::new();
5156        runtime.set_module_resolver(resolver);
5157        let compiled = runtime
5158            .compile_self_contained_named(
5159                "budget",
5160                r#"import "components/heavy" as h;
5161                    fn view() { column([h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{}), h::Heavy(#{} )]) }"#,
5162            )
5163            .unwrap();
5164        let result = runtime.render(&compiled);
5165        assert!(result.is_err(), "nested work must exceed the shared budget");
5166        assert!(
5167            runtime
5168                .take_timings()
5169                .iter()
5170                .any(|timing| timing.operations > DEFAULT_SCRIPT_OPERATION_LIMIT)
5171        );
5172    }
5173
5174    #[test]
5175    fn operation_limit_is_host_configurable() {
5176        let heavy = r"fn view() { let n = 0; for i in 0..400000 { n += 1; } text(`${n}`) }";
5177        let mut runtime = RuntimeEngine::new();
5178        assert_eq!(runtime.operation_limit(), DEFAULT_SCRIPT_OPERATION_LIMIT);
5179        let compiled = runtime
5180            .compile_self_contained_named("limit", heavy)
5181            .unwrap();
5182        assert!(
5183            runtime.render(&compiled).is_err(),
5184            "this finite workload actually exceeds the default budget"
5185        );
5186        assert!(runtime.last_failed_timing().unwrap().operations > DEFAULT_SCRIPT_OPERATION_LIMIT);
5187
5188        runtime.set_operation_limit(10_000);
5189        let compiled = runtime
5190            .compile_self_contained_named("limit-low", heavy)
5191            .unwrap();
5192        assert!(
5193            runtime.render(&compiled).is_err(),
5194            "a lowered budget rejects the same script"
5195        );
5196
5197        runtime.set_operation_limit(50_000_000);
5198        let compiled = runtime
5199            .compile_self_contained_named("limit-high", heavy)
5200            .unwrap();
5201        assert!(
5202            runtime.render(&compiled).is_ok(),
5203            "a raised budget admits heavier one-shot work"
5204        );
5205    }
5206
5207    #[test]
5208    fn host_policies_are_independent_and_reload_candidates_do_not_share_mutable_quota() {
5209        let mut engine = RuntimeEngine::new();
5210        assert_eq!(engine.expression_depth_limits(), (64, 32));
5211        engine.set_operation_limit(2_000_000);
5212        engine.set_expression_depth_limits(80, 64);
5213        assert_eq!(engine.engine().max_call_levels(), 64);
5214        assert_eq!(engine.engine().max_array_size(), RHAI_MAX_ARRAY_SIZE);
5215        #[cfg(feature = "dev-reload")]
5216        {
5217            let mut candidate = engine.candidate_engine();
5218            assert_eq!(candidate.operation_limit(), 2_000_000);
5219            assert_eq!(candidate.expression_depth_limits(), (80, 64));
5220            candidate.set_operation_limit(10);
5221            candidate.set_expression_depth_limits(20, 10);
5222            assert_eq!(engine.operation_limit(), 2_000_000);
5223            assert_eq!(engine.expression_depth_limits(), (80, 64));
5224        }
5225        engine.set_operation_limit(0);
5226        engine.set_expression_depth_limits(0, 0);
5227        assert_eq!(engine.operation_limit(), 1);
5228        assert_eq!(engine.expression_depth_limits(), (1, 1));
5229    }
5230
5231    #[test]
5232    fn retained_component_budget_ignores_parent_history_without_resetting_session_total() {
5233        let module = r#"
5234            define_component(#{
5235                metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5236                    runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5237                schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5238                    state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5239                    events: #{}, slots: #{}, parts: ["root"] },
5240                render: Fn("render_Probe")
5241            });
5242            fn Probe(props) { render_component("components/probe", props) }
5243            fn render_Probe(ctx, props) {
5244                let n = 0;
5245                if ctx.get_state("heavy") { for i in 0..100000 { n += 1; } }
5246                text(`${n}`)
5247            }
5248        "#;
5249        let mut measured = Vec::new();
5250        for parent_work in [0, 250_000] {
5251            let mut resolver = crate::RestrictedModuleResolver::new();
5252            resolver.insert("components/probe", module).unwrap();
5253            let mut engine = RuntimeEngine::new();
5254            engine.set_module_resolver(resolver);
5255            let source = format!(
5256                r#"import "components/probe" as probe;
5257                    fn view(ctx) {{
5258                        let n = 0;
5259                        for i in 0..{parent_work} {{ n += 1; }}
5260                        probe::Probe(#{{ key: "probe" }})
5261                    }}"#
5262            );
5263            let compiled = engine
5264                .compile_self_contained_named("incremental-budget", &source)
5265                .unwrap();
5266            let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5267            let mut lifecycle = crate::ScriptLifecycle::new(
5268                compiled,
5269                Rc::clone(&runtime),
5270                ComponentInstancePath::root("App", "budget"),
5271                None,
5272                BTreeMap::new(),
5273                &ComponentStateSchema::default(),
5274            )
5275            .unwrap();
5276            lifecycle.start(&mut engine).unwrap();
5277            let owner = engine
5278                .component_invocations()
5279                .next()
5280                .unwrap()
5281                .path()
5282                .clone();
5283            let _ = engine.take_timings();
5284            runtime
5285                .borrow_mut()
5286                .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5287                .unwrap();
5288            assert!(lifecycle.render_dirty(&mut engine).unwrap());
5289            let operations = engine
5290                .take_timings()
5291                .into_iter()
5292                .filter(|timing| timing.operation == ExecutionOperation::Render)
5293                .map(|timing| timing.operations)
5294                .sum::<u64>();
5295            assert!(
5296                operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5297                "{operations}"
5298            );
5299            measured.push(operations);
5300        }
5301        assert_eq!(measured[0], measured[1]);
5302    }
5303
5304    #[test]
5305    fn multiple_dirty_components_share_one_incremental_budget() {
5306        let module = r#"
5307            define_component(#{
5308                metadata: #{ id: "components/probe", "export": "Probe", version: "0.1.1",
5309                    runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5310                schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5311                    state: #{ fields: #{ heavy: #{ schema: #{ type: "bool" }, "default": #{ type: "bool", value: false } } } },
5312                    events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Probe") }
5313            );
5314            fn Probe(props) { render_component("components/probe", props) }
5315            fn render_Probe(ctx, props) {
5316                let n = 0;
5317                if ctx.get_state("heavy") { for i in 0..200000 { n += 1; } }
5318                text(`${n}`)
5319            }
5320        "#;
5321        let mut resolver = crate::RestrictedModuleResolver::new();
5322        resolver.insert("components/probe", module).unwrap();
5323        let mut engine = RuntimeEngine::new();
5324        engine.set_module_resolver(resolver);
5325        let compiled = engine
5326            .compile_self_contained_named(
5327                "incremental-aggregate-budget",
5328                r#"import "components/probe" as probe;
5329                    fn view(ctx) {
5330                        column([
5331                            probe::Probe(#{ key: "first" }),
5332                            probe::Probe(#{ key: "second" }),
5333                        ])
5334                    }"#,
5335            )
5336            .unwrap();
5337        let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5338        let mut lifecycle = crate::ScriptLifecycle::new(
5339            compiled,
5340            Rc::clone(&runtime),
5341            ComponentInstancePath::root("App", "budget"),
5342            None,
5343            BTreeMap::new(),
5344            &ComponentStateSchema::default(),
5345        )
5346        .unwrap();
5347        lifecycle.start(&mut engine).unwrap();
5348        let owners = engine
5349            .component_invocations()
5350            .map(|recipe| recipe.path().clone())
5351            .collect::<Vec<_>>();
5352        assert_eq!(owners.len(), 2);
5353        for owner in owners {
5354            runtime
5355                .borrow_mut()
5356                .set_component_state_from_host(&owner, "heavy", UiValue::Bool(true))
5357                .unwrap();
5358        }
5359        assert!(lifecycle.render_dirty(&mut engine).is_err());
5360    }
5361
5362    #[test]
5363    fn retained_virtual_renderer_budget_ignores_parent_history() {
5364        let mut measured = Vec::new();
5365        for parent_work in [0, 250_000] {
5366            let mut engine = RuntimeEngine::new();
5367            let source = format!(
5368                r#"
5369                    fn render_item(ctx, payload) {{
5370                        let n = 0;
5371                        if payload.item.heavy {{ for i in 0..100000 {{ n += 1; }} }}
5372                        text(`${{n}}`).with_key(payload.key)
5373                    }}
5374                    fn view(ctx) {{
5375                        let n = 0;
5376                        for i in 0..{parent_work} {{ n += 1; }}
5377                        let data = [];
5378                        for index in 0..32 {{
5379                            data.push(#{{ key: `row-${{index}}`, heavy: index == 31 }});
5380                        }}
5381                        virtual_collection(#{{
5382                            key: "rows", label: "Rows", data: data,
5383                            estimated_height: 24, height: 24, overdraw_pixels: 0,
5384                            alignment: "top", follow_tail: false,
5385                        }}, Fn("render_item"))
5386                    }}
5387                "#
5388            );
5389            let compiled = engine.compile_named("virtual-budget", &source).unwrap();
5390            let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5391            let root_path = ComponentInstancePath::root("App", "budget");
5392            let mut lifecycle = crate::ScriptLifecycle::new(
5393                compiled,
5394                Rc::clone(&runtime),
5395                root_path.clone(),
5396                None,
5397                BTreeMap::new(),
5398                &ComponentStateSchema::default(),
5399            )
5400            .unwrap();
5401            lifecycle.start(&mut engine).unwrap();
5402            let id = engine
5403                .virtual_collection_ids_in_scope(&root_path)
5404                .into_iter()
5405                .next()
5406                .unwrap();
5407            let _ = engine.take_timings();
5408            runtime.borrow().virtual_requests.request(id, [31]);
5409            assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5410            let operations = engine
5411                .take_timings()
5412                .into_iter()
5413                .filter(|timing| {
5414                    matches!(timing.operation, ExecutionOperation::VirtualCollection(_))
5415                })
5416                .map(|timing| timing.operations)
5417                .sum::<u64>();
5418            assert!(
5419                operations < DEFAULT_SCRIPT_OPERATION_LIMIT / 2,
5420                "{operations}"
5421            );
5422            measured.push(operations);
5423        }
5424        assert_eq!(measured[0], measured[1]);
5425    }
5426
5427    #[test]
5428    fn retained_virtual_renderer_replays_curried_node_presentation() {
5429        let mut engine = RuntimeEngine::new();
5430        let compiled = engine
5431            .compile_named(
5432                "virtual-rich-presentation",
5433                r#"
5434                    fn render_item(content, ctx, payload) {
5435                        content[payload.key]
5436                    }
5437                    fn view(ctx) {
5438                        let data = [];
5439                        let content = #{};
5440                        for index in 0..32 {
5441                            let key = `row-${index}`;
5442                            data.push(#{ key: key });
5443                            content[key] = text(`Rich ${index}`);
5444                        }
5445                        virtual_collection(#{
5446                            key: "rows", label: "Rows", data: data,
5447                            estimated_height: 24, height: 24, overdraw_pixels: 0,
5448                            alignment: "top", follow_tail: false,
5449                        }, Fn("render_item").curry(content))
5450                    }
5451                "#,
5452            )
5453            .unwrap();
5454        let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5455        let root_path = ComponentInstancePath::root("App", "rich");
5456        let mut lifecycle = crate::ScriptLifecycle::new(
5457            compiled,
5458            Rc::clone(&runtime),
5459            root_path.clone(),
5460            None,
5461            BTreeMap::new(),
5462            &ComponentStateSchema::default(),
5463        )
5464        .unwrap();
5465        lifecycle.start(&mut engine).unwrap();
5466        let id = engine
5467            .virtual_collection_ids_in_scope(&root_path)
5468            .into_iter()
5469            .next()
5470            .unwrap();
5471        runtime.borrow().virtual_requests.request(id, [31]);
5472        assert!(lifecycle.realize_virtual_requests(&mut engine).unwrap());
5473        let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5474            unreachable!()
5475        };
5476        assert!(matches!(
5477            spec.realized[&31].kind(),
5478            crate::UiNodeKind::Text { text } if text == "Rich 31"
5479        ));
5480    }
5481
5482    #[test]
5483    #[allow(clippy::too_many_lines)]
5484    fn virtual_row_formal_component_preserves_the_real_callback_owner() {
5485        let mut engine = RuntimeEngine::new();
5486        let compiled = engine
5487            .compile_named(
5488                "virtual-callback-owner",
5489                r#"
5490                    define_component(#{
5491                        metadata: #{ id: "test/action", "export": "Action", version: "0.1.8",
5492                            runtime_api: #{ min_inclusive: 2, max_exclusive: 3 },
5493                            dependencies: [], capabilities: #{} },
5494                        schema: #{ props: #{
5495                                key: #{ schema: #{ type: "string" }, required: true, sensitive: false },
5496                                on_action: #{ schema: #{ type: "callback" }, required: true, sensitive: false },
5497                            }, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: [] },
5498                        render: Fn("render_Action")
5499                    });
5500                    fn Action(props) { render_component("test/action", props) }
5501                    fn render_Action(ctx, props) { text("Action").on_click(props.on_action) }
5502                    fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5503                    fn render_item(ctx, payload) {
5504                        Action(#{ key: payload.key, on_action: Fn("increment") })
5505                    }
5506                    fn view(ctx) {
5507                        let data=[];for index in 0..32 {data.push(#{key:`row-${index}`});}
5508                        virtual_collection(#{
5509                            key: "rows", label: "Rows", data: data,
5510                            estimated_height: 24, height: 24, overdraw_pixels: 0,
5511                            alignment: "top", follow_tail: false,
5512                        }, Fn("render_item"))
5513                    }
5514                "#,
5515            )
5516            .unwrap();
5517        let runtime = Rc::new(RefCell::new(crate::UiRuntimeState::new()));
5518        let root_path = ComponentInstancePath::root("App", "virtual-callback");
5519        let schema = ComponentStateSchema::new(BTreeMap::from([(
5520            "count".to_owned(),
5521            crate::StateField::new(
5522                crate::ValueSchema::Integer {
5523                    min: None,
5524                    max: None,
5525                },
5526                UiValue::Integer(0),
5527            ),
5528        )]))
5529        .unwrap();
5530        let mut lifecycle = crate::ScriptLifecycle::new(
5531            compiled,
5532            Rc::clone(&runtime),
5533            root_path.clone(),
5534            None,
5535            BTreeMap::new(),
5536            &schema,
5537        )
5538        .unwrap();
5539        lifecycle.start(&mut engine).unwrap();
5540        let crate::UiNodeKind::VirtualCollection { spec } = lifecycle.root().unwrap().kind() else {
5541            unreachable!()
5542        };
5543        let crate::UiEventHandler::Script(callback) =
5544            spec.realized[&0].handlers()["click"][0].handler()
5545        else {
5546            panic!("formal row component must retain its callback");
5547        };
5548        let _ = lifecycle
5549            .invoke_callback_transactional(&engine, callback, UiValue::Null)
5550            .unwrap();
5551        assert_eq!(
5552            runtime.borrow().component_state.get(&root_path, "count"),
5553            Some(&UiValue::Integer(1))
5554        );
5555        let id = engine
5556            .virtual_collection_ids_in_scope(&root_path)
5557            .into_iter()
5558            .next()
5559            .unwrap();
5560        assert!(
5561            !lifecycle
5562                .root()
5563                .unwrap()
5564                .virtual_collection_items(&id)
5565                .unwrap()
5566                .contains_key(&31)
5567        );
5568        runtime.borrow().virtual_requests.request(id.clone(), [31]);
5569        lifecycle.realize_virtual_requests(&mut engine).unwrap();
5570        let callback = lifecycle
5571            .root()
5572            .unwrap()
5573            .virtual_collection_items(&id)
5574            .unwrap()[&31]
5575            .handler("click")
5576            .unwrap()
5577            .as_script()
5578            .unwrap();
5579        let _ = lifecycle
5580            .invoke_callback_transactional(&engine, callback, UiValue::Null)
5581            .unwrap();
5582        assert_eq!(
5583            runtime.borrow().component_state.get(&root_path, "count"),
5584            Some(&UiValue::Integer(2))
5585        );
5586    }
5587
5588    #[test]
5589    fn invalid_state_default_reports_the_field_and_tagged_encoding() {
5590        let mut engine = RuntimeEngine::new();
5591        let compiled = engine.compile(r#"
5592            fn state_schema() {#{fields:#{current_theme:#{schema:#{type:"map",values:#{type:"ui_value"}},
5593                "default":#{type:"map",value:#{family:"Default"}}}}}}
5594            fn view(ctx) {text("view")}
5595        "#).unwrap();
5596        let error = engine.root_state_schema(&compiled).unwrap_err().to_string();
5597        assert!(
5598            error.contains("state_schema.fields.current_theme.default"),
5599            "{error}"
5600        );
5601        assert!(error.contains("recursively tagged UiValue"), "{error}");
5602    }
5603
5604    #[test]
5605    fn delayed_component_callback_starts_with_a_fresh_operation_budget() {
5606        let module = r#"
5607            define_component(#{
5608                metadata: #{ id: "components/counter", "export": "Counter", version: "0.1.1",
5609                    runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5610                schema: #{ props: #{}, state: #{ fields: #{} }, events: #{}, slots: #{}, parts: ["root"] },
5611                render: Fn("render_Counter")
5612            });
5613            fn Counter(props) { render_component("components/counter", props) }
5614            fn render_Counter(ctx, props) { text("counter").on_click(Fn("clicked")) }
5615            fn clicked(ctx, payload) {
5616                let n = 0;
5617                for i in 0..100000 { n += 1; }
5618                n
5619            }
5620        "#;
5621        let mut resolver = crate::RestrictedModuleResolver::new();
5622        resolver.insert("components/counter", module).unwrap();
5623        let mut engine = RuntimeEngine::new();
5624        engine.set_module_resolver(resolver);
5625        let compiled = engine
5626            .compile_self_contained_named(
5627                "callback-budget",
5628                r#"import "components/counter" as c;
5629                    fn view(ctx) {
5630                        let n = 0;
5631                        for i in 0..250000 { n += 1; }
5632                        c::Counter(#{} )
5633                    }"#,
5634            )
5635            .unwrap();
5636        let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5637        let mut lifecycle = crate::ScriptLifecycle::new(
5638            compiled,
5639            runtime,
5640            ComponentInstancePath::root("App", "budget"),
5641            None,
5642            BTreeMap::new(),
5643            &ComponentStateSchema::default(),
5644        )
5645        .unwrap();
5646        let root = lifecycle.start(&mut engine).unwrap();
5647        let crate::UiEventHandler::Script(callback) = root.handlers()["click"][0].handler() else {
5648            panic!("component must expose a script click callback");
5649        };
5650        let callback = callback.clone();
5651        assert!(
5652            lifecycle
5653                .invoke_callback_transactional(&engine, &callback, UiValue::Null)
5654                .is_ok()
5655        );
5656    }
5657
5658    #[test]
5659    fn old_component_handles_cannot_target_a_same_key_remount() {
5660        let module = r#"
5661            define_component(#{
5662                metadata: #{ id: "components/incarnation", "export": "Counter", version: "0.1.1",
5663                    runtime_api: #{ min_inclusive: 2, max_exclusive: 3 }, dependencies: [], capabilities: #{} },
5664                schema: #{ props: #{ key: #{ schema: #{ type: "string" }, required: true, sensitive: false } },
5665                    state: #{ fields: #{ count: #{ schema: #{ type: "integer" }, "default": #{ type: "integer", value: 0 } } } },
5666                    events: #{}, slots: #{}, parts: ["root"] }, render: Fn("render_Counter")
5667            });
5668            fn Counter(props) { render_component("components/incarnation", props) }
5669            fn increment(ctx, payload) { ctx.set_state("count", ctx.get_state("count") + 1); }
5670            fn render_Counter(ctx, props) {
5671                let progress = signal("progress", 0.0);
5672                text(`${ctx.get_state("count")}`).bind_signal("opacity", progress).on_click(Fn("increment"))
5673            }
5674        "#;
5675        let mut resolver = crate::RestrictedModuleResolver::new();
5676        resolver.insert("components/incarnation", module).unwrap();
5677        let mut engine = RuntimeEngine::new();
5678        engine.set_module_resolver(resolver);
5679        let compiled = engine
5680            .compile_self_contained_named(
5681                "incarnation",
5682                r#"import "components/incarnation" as c;
5683                    fn view(ctx) {
5684                        if ctx.get_state("visible") { c::Counter(#{ key: "same" }) }
5685                        else { text("hidden") }
5686                    }"#,
5687            )
5688            .unwrap();
5689        let runtime = Rc::new(RefCell::new(UiRuntimeState::new()));
5690        let root_path = ComponentInstancePath::root("App", "incarnation");
5691        let schema = ComponentStateSchema::new(BTreeMap::from([(
5692            "visible".to_owned(),
5693            crate::StateField::new(crate::ValueSchema::Bool, UiValue::Bool(true)),
5694        )]))
5695        .unwrap();
5696        let mut lifecycle = crate::ScriptLifecycle::new(
5697            compiled,
5698            Rc::clone(&runtime),
5699            root_path.clone(),
5700            None,
5701            BTreeMap::new(),
5702            &schema,
5703        )
5704        .unwrap();
5705        lifecycle.start(&mut engine).unwrap();
5706        let component = engine
5707            .component_invocations()
5708            .next()
5709            .unwrap()
5710            .path()
5711            .clone();
5712        let crate::UiEventHandler::Script(old_callback) =
5713            lifecycle.root().unwrap().handlers()["click"][0].handler()
5714        else {
5715            panic!("component must expose its callback");
5716        };
5717        let old_callback = old_callback.clone();
5718        let old_signal = runtime
5719            .borrow()
5720            .signals
5721            .resolve(&component, "progress")
5722            .unwrap();
5723        runtime
5724            .borrow_mut()
5725            .set_component_state_from_host(&root_path, "visible", UiValue::Bool(false))
5726            .unwrap();
5727        lifecycle.render_dirty(&mut engine).unwrap();
5728        runtime
5729            .borrow_mut()
5730            .set_component_state_from_host(&root_path, "visible", UiValue::Bool(true))
5731            .unwrap();
5732        lifecycle.render_dirty(&mut engine).unwrap();
5733        assert!(matches!(
5734            lifecycle.invoke_callback_transactional(&engine, &old_callback, UiValue::Null),
5735            Err(crate::LifecycleError::Runtime(
5736                RuntimeError::StaleComponentCallback { .. }
5737            ))
5738        ));
5739        assert!(matches!(
5740            runtime
5741                .borrow_mut()
5742                .signals
5743                .write(&old_signal, crate::SignalValue::Float(0.5)),
5744            Err(crate::SignalError::Stale(_))
5745        ));
5746    }
5747}