Skip to main content

presolve_compiler/
runtime_effect.rs

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