Skip to main content

gpui_rhai/
engine.rs

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