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