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