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