Skip to main content

presolve_compiler/
runtime_effect.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    ApplicationSemanticModel, CapabilityOperationId, EffectComputedPrerequisiteBatch,
5    EffectDeclaration, EffectExecutionBatch, EffectExecutionPolicy, EffectRenderBoundary,
6    ExecutionBoundary, IntermediateRepresentation, SemanticId, SourceProvenance,
7};
8
9/// Compiler-owned runtime registry for F10-lowered effects.
10///
11/// The registry is metadata only: it projects existing F8 trigger, F9
12/// prerequisite, and F10/F11 IR facts without executing or scheduling effects.
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct RuntimeEffectRegistry {
15    pub records: BTreeMap<SemanticId, RuntimeEffectRecord>,
16}
17
18/// Runtime metadata for one compiler-lowered effect.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct RuntimeEffectRecord {
21    pub effect: SemanticId,
22    pub execution_function: SemanticId,
23    pub initial_trigger_policy: EffectExecutionPolicy,
24    pub initial_trigger: Option<RuntimeInitialEffectTrigger>,
25    pub action_batch_triggers: Vec<RuntimeActionBatchEffectTrigger>,
26    pub capability_operations: Vec<CapabilityOperationId>,
27    pub execution_boundary: ExecutionBoundary,
28    /// V2 source field order, propagated to runtime batch ordering.
29    pub declaration_order: Option<u32>,
30    /// V2 field effects are scheduled once after a successful resume. Legacy
31    /// decorator effects retain the frozen resume behavior.
32    pub run_on_resume: bool,
33    pub provenance: SourceProvenance,
34}
35
36/// Compiler-owned initial-render trigger metadata for one effect.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct RuntimeInitialEffectTrigger {
39    pub render_boundary: EffectRenderBoundary,
40    pub required_computed: Vec<SemanticId>,
41    pub prerequisite_batches: Vec<EffectComputedPrerequisiteBatch>,
42    pub effect_batch_index: u32,
43}
44
45/// Compiler-owned completed-action-batch trigger metadata for one effect.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct RuntimeActionBatchEffectTrigger {
48    pub action_batch: SemanticId,
49    pub matched_states: Vec<SemanticId>,
50    pub required_computed: Vec<SemanticId>,
51    pub prerequisite_batches: Vec<EffectComputedPrerequisiteBatch>,
52    pub effect_batch_index: u32,
53}
54
55impl RuntimeEffectRegistry {
56    #[must_use]
57    pub fn record(&self, effect: &SemanticId) -> Option<&RuntimeEffectRecord> {
58        self.records.get(effect)
59    }
60}
61
62/// Build deterministic runtime effect records from canonical F8, F9, and F10/F11 products.
63///
64/// Registry membership is restricted to `IrEffectExecution` records. This
65/// function never walks effect source/body expressions, re-infers dependencies,
66/// creates scheduler positions, or invokes runtime capabilities.
67#[must_use]
68pub fn build_runtime_effect_registry(
69    model: &ApplicationSemanticModel,
70    ir: &IntermediateRepresentation,
71) -> RuntimeEffectRegistry {
72    let executions = ir
73        .modules
74        .iter()
75        .flat_map(|module| &module.effect_executions)
76        .map(|execution| (execution.effect.clone(), execution))
77        .collect::<BTreeMap<_, _>>();
78    let records = executions
79        .into_iter()
80        .filter_map(|(effect_id, execution)| {
81            let effect = model.effects.get(&effect_id)?;
82            let computed_dependencies = effect_computed_dependencies(model, &effect_id);
83            Some((
84                effect_id.clone(),
85                RuntimeEffectRecord {
86                    effect: effect_id.clone(),
87                    execution_function: execution.function.clone(),
88                    initial_trigger_policy: effect.execution_policy,
89                    initial_trigger: initial_trigger(model, &effect_id, &computed_dependencies),
90                    action_batch_triggers: action_batch_triggers(
91                        model,
92                        &effect_id,
93                        &computed_dependencies,
94                    ),
95                    capability_operations: execution.capability_operations.clone(),
96                    execution_boundary: effect.execution_boundary,
97                    declaration_order: effect.declaration_order,
98                    run_on_resume: matches!(effect.declaration, EffectDeclaration::V2Field),
99                    provenance: effect.provenance.clone(),
100                },
101            ))
102        })
103        .collect();
104
105    RuntimeEffectRegistry { records }
106}
107
108fn effect_computed_dependencies(
109    model: &ApplicationSemanticModel,
110    effect: &SemanticId,
111) -> BTreeSet<SemanticId> {
112    model
113        .effect_reactive_analysis
114        .get(effect)
115        .into_iter()
116        .flat_map(|analysis| &analysis.dependencies)
117        .filter(|dependency| model.computed_values.contains_key(*dependency))
118        .cloned()
119        .collect()
120}
121
122fn initial_trigger(
123    model: &ApplicationSemanticModel,
124    effect: &SemanticId,
125    computed_dependencies: &BTreeSet<SemanticId>,
126) -> Option<RuntimeInitialEffectTrigger> {
127    let initial = &model.effect_execution_plan.initial;
128    let effect_batch_index = effect_batch_index(&initial.effect_batches, effect)?;
129    let render_boundary = initial.render_boundary?;
130    let required_computed =
131        select_required_computed(&initial.required_computed, computed_dependencies);
132    Some(RuntimeInitialEffectTrigger {
133        render_boundary,
134        prerequisite_batches: select_prerequisite_batches(
135            &initial.prerequisite_batches,
136            &required_computed,
137        ),
138        required_computed,
139        effect_batch_index,
140    })
141}
142
143fn action_batch_triggers(
144    model: &ApplicationSemanticModel,
145    effect: &SemanticId,
146    computed_dependencies: &BTreeSet<SemanticId>,
147) -> Vec<RuntimeActionBatchEffectTrigger> {
148    let trigger_evidence = model
149        .effect_trigger_plan
150        .action_batch_triggers
151        .iter()
152        .filter_map(|trigger| {
153            trigger
154                .matched_states
155                .get(effect)
156                .map(|states| (trigger.action_batch.clone(), states.clone()))
157        })
158        .collect::<BTreeMap<_, _>>();
159    let mut triggers = model
160        .effect_execution_plan
161        .actions
162        .iter()
163        .filter_map(|action| {
164            let effect_batch_index = effect_batch_index(&action.effect_batches, effect)?;
165            let matched_states = trigger_evidence.get(&action.action_batch)?.clone();
166            let required_computed =
167                select_required_computed(&action.required_computed, computed_dependencies);
168            Some(RuntimeActionBatchEffectTrigger {
169                action_batch: action.action_batch.clone(),
170                matched_states,
171                prerequisite_batches: select_prerequisite_batches(
172                    &action.prerequisite_batches,
173                    &required_computed,
174                ),
175                required_computed,
176                effect_batch_index,
177            })
178        })
179        .collect::<Vec<_>>();
180    triggers.sort_by(|left, right| left.action_batch.cmp(&right.action_batch));
181    triggers
182}
183
184fn effect_batch_index(batches: &[EffectExecutionBatch], effect: &SemanticId) -> Option<u32> {
185    batches
186        .iter()
187        .find(|batch| batch.effects.contains(effect))
188        .map(|batch| batch.index)
189}
190
191fn select_required_computed(
192    scheduled: &[SemanticId],
193    dependencies: &BTreeSet<SemanticId>,
194) -> Vec<SemanticId> {
195    scheduled
196        .iter()
197        .filter(|computed| dependencies.contains(*computed))
198        .cloned()
199        .collect()
200}
201
202fn select_prerequisite_batches(
203    batches: &[EffectComputedPrerequisiteBatch],
204    required_computed: &[SemanticId],
205) -> Vec<EffectComputedPrerequisiteBatch> {
206    let required = required_computed.iter().collect::<BTreeSet<_>>();
207    batches
208        .iter()
209        .filter_map(|batch| {
210            let computed = batch
211                .computed
212                .iter()
213                .filter(|computed| required.contains(computed))
214                .cloned()
215                .collect::<Vec<_>>();
216            (!computed.is_empty()).then_some(EffectComputedPrerequisiteBatch {
217                source_batch_index: batch.source_batch_index,
218                computed,
219            })
220        })
221        .collect()
222}
223
224#[cfg(test)]
225mod tests {
226    use crate::{
227        build_application_semantic_model, build_runtime_effect_registry, lower_components_to_ir,
228        optimize_effect_ir, CapabilityOperationId, EffectExecutionPolicy, EffectRenderBoundary,
229        ExecutionBoundary,
230    };
231
232    #[test]
233    #[allow(clippy::too_many_lines)]
234    fn builds_deterministic_runtime_records_from_effect_plans_and_ir() {
235        let parsed = presolve_parser::parse_file(
236            "src/RuntimeEffect.tsx",
237            r#"
238@component("x-runtime-effect")
239class RuntimeEffect extends Component {
240  price = state(1);
241  locale = state("en-US");
242  theme = state("light");
243
244  @computed()
245  get subtotal() { return this.price * 2; }
246
247  @computed()
248  get total() { return this.subtotal + 1; }
249
250  @computed()
251  get currentLocale() { return this.locale; }
252
253  @action()
254  increment() { this.price += 1; }
255
256  @effect()
257  report() { console.log(this.total, this.currentLocale); }
258
259  @effect()
260  audit() { console.log(this.price); }
261
262  @effect()
263  bootLog() { console.log("ready"); }
264
265  @action()
266  invalidAction() { this.price += 1; }
267
268  @effect()
269  invalid() { this.invalidAction(); }
270
271  render() { return <p />; }
272}
273"#,
274        );
275        let model = build_application_semantic_model(&parsed);
276        let component = &model.components[0];
277        let price = component.id.state_field("price");
278        let subtotal = component.id.computed("subtotal");
279        let total = component.id.computed("total");
280        let current_locale = component.id.computed("currentLocale");
281        let report = component.id.effect("report");
282        let audit = component.id.effect("audit");
283        let boot_log = component.id.effect("bootLog");
284        let invalid = component.id.effect("invalid");
285        let increment = component.id.action_batch("increment");
286        let registry = build_runtime_effect_registry(
287            &model,
288            &optimize_effect_ir(&lower_components_to_ir(&model)).output,
289        );
290        let report_record = registry.record(&report).expect("report record");
291        let audit_record = registry.record(&audit).expect("audit record");
292        let boot_log_record = registry.record(&boot_log).expect("boot log record");
293        let initial = report_record
294            .initial_trigger
295            .as_ref()
296            .expect("initial report trigger");
297        let action = report_record
298            .action_batch_triggers
299            .iter()
300            .find(|trigger| trigger.action_batch == increment)
301            .expect("increment report trigger");
302
303        assert_eq!(registry.records.len(), 3);
304        assert_eq!(
305            registry.records.keys().cloned().collect::<Vec<_>>(),
306            vec![audit.clone(), boot_log, report.clone()]
307        );
308        assert!(registry.record(&invalid).is_none());
309        assert_eq!(report_record.effect, report);
310        assert_eq!(report_record.execution_function, report_record.effect);
311        assert_eq!(
312            report_record.initial_trigger_policy,
313            EffectExecutionPolicy::AfterInitialRenderAndCompletedActionBatch
314        );
315        assert_eq!(report_record.execution_boundary, ExecutionBoundary::Client);
316        assert!(!report_record.run_on_resume);
317        assert_eq!(
318            initial.render_boundary,
319            EffectRenderBoundary::AfterInitialRender
320        );
321        assert_eq!(
322            initial.required_computed,
323            vec![current_locale, subtotal.clone(), total.clone()]
324        );
325        assert_eq!(
326            initial
327                .prerequisite_batches
328                .iter()
329                .map(|batch| batch.computed.clone())
330                .collect::<Vec<_>>(),
331            vec![
332                vec![component.id.computed("currentLocale"), subtotal.clone()],
333                vec![total.clone()]
334            ]
335        );
336        assert_eq!(initial.effect_batch_index, 0);
337        assert_eq!(action.matched_states, vec![price.clone()]);
338        assert_eq!(action.required_computed, vec![subtotal, total]);
339        assert_eq!(
340            action
341                .prerequisite_batches
342                .iter()
343                .map(|batch| batch.computed.clone())
344                .collect::<Vec<_>>(),
345            vec![
346                vec![component.id.computed("subtotal")],
347                vec![component.id.computed("total")]
348            ]
349        );
350        assert_eq!(action.effect_batch_index, 0);
351        assert_eq!(
352            report_record.capability_operations,
353            vec![CapabilityOperationId("builtin.browser.console.log")]
354        );
355        assert_eq!(
356            audit_record
357                .initial_trigger
358                .as_ref()
359                .expect("initial audit")
360                .required_computed,
361            Vec::new()
362        );
363        assert_eq!(
364            audit_record.action_batch_triggers[0].matched_states,
365            vec![price]
366        );
367        assert!(boot_log_record.action_batch_triggers.is_empty());
368        assert_eq!(
369            boot_log_record
370                .initial_trigger
371                .as_ref()
372                .expect("initial boot log")
373                .required_computed,
374            Vec::new()
375        );
376        assert_eq!(
377            report_record.provenance,
378            model
379                .effect(&report_record.effect)
380                .expect("effect")
381                .provenance
382        );
383    }
384}