Skip to main content

presolve_compiler/
context_update.rs

1use std::collections::BTreeSet;
2
3use crate::{
4    ApplicationSemanticModel, ContextSourcePlanEntry, ContextSourcePlanStatus,
5    ContextValueSourceId, OptimizedContextIrReport, SemanticId,
6};
7
8/// Compiler-owned re-evaluation work for one completed authored action batch.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ContextActionUpdatePlan {
11    pub action_batch: SemanticId,
12    pub invalidated_sources: Vec<ContextValueSourceId>,
13    pub prerequisite_computed_batches: Vec<u32>,
14    pub source_evaluation_batches: Vec<crate::ContextEvaluationBatchId>,
15    pub affected_consumers: Vec<crate::ConsumerId>,
16}
17
18/// Immutable G15 update plan. Empty action records are retained so the runtime
19/// never infers that an absent Context update is an accidental omission.
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct ContextUpdatePlan {
22    pub actions: Vec<ContextActionUpdatePlan>,
23}
24
25/// Build Context updates solely from G7/G9 dependencies, existing computed
26/// transitive facts, authored action-batch identities, and G10 bindings.
27#[must_use]
28pub fn build_context_update_plan(
29    model: &ApplicationSemanticModel,
30    optimized: &OptimizedContextIrReport,
31) -> ContextUpdatePlan {
32    let planned = optimized
33        .source_evaluations
34        .iter()
35        .filter_map(|evaluation| {
36            model
37                .context_evaluation
38                .context_source_plan(&evaluation.source)
39                .filter(|entry| entry.status == ContextSourcePlanStatus::Planned)
40                .map(|entry| (evaluation, entry))
41        })
42        .collect::<Vec<_>>();
43    let actions = model
44        .effect_trigger_plan
45        .action_batches
46        .values()
47        .map(|action| {
48            let written = action.written_states.iter().collect::<BTreeSet<_>>();
49            let invalidated_sources = planned
50                .iter()
51                .filter(|(_, entry)| matches!(entry.source, ContextValueSourceId::Provider(_)))
52                .filter(|(_, entry)| source_depends_on_written_state(model, entry, &written))
53                .map(|(evaluation, _)| evaluation.source.clone())
54                .collect::<Vec<_>>();
55            let invalidated = invalidated_sources.iter().collect::<BTreeSet<_>>();
56            let prerequisite_computed_batches = planned
57                .iter()
58                .filter(|(evaluation, _)| invalidated.contains(&evaluation.source))
59                .flat_map(|(evaluation, _)| {
60                    evaluation.prerequisite_computed_batches.iter().copied()
61                })
62                .collect::<BTreeSet<_>>()
63                .into_iter()
64                .collect();
65            let source_evaluation_batches = model
66                .context_evaluation
67                .evaluation_batches
68                .iter()
69                .filter(|batch| {
70                    batch
71                        .sources
72                        .iter()
73                        .any(|source| invalidated.contains(source))
74                })
75                .map(|batch| batch.id.clone())
76                .collect();
77            let affected_consumers = optimized
78                .optimized_module
79                .context_ir
80                .consumer_bindings
81                .iter()
82                .filter(|binding| invalidated.contains(&binding.source))
83                .map(|binding| binding.consumer.clone())
84                .collect();
85            ContextActionUpdatePlan {
86                action_batch: action.id.clone(),
87                invalidated_sources,
88                prerequisite_computed_batches,
89                source_evaluation_batches,
90                affected_consumers,
91            }
92        })
93        .collect();
94    ContextUpdatePlan { actions }
95}
96
97fn source_depends_on_written_state(
98    model: &ApplicationSemanticModel,
99    entry: &ContextSourcePlanEntry,
100    written: &BTreeSet<&SemanticId>,
101) -> bool {
102    entry
103        .required_state
104        .iter()
105        .any(|state| written.contains(state))
106        || entry.required_computed.iter().any(|computed| {
107            model
108                .reactive_transitive_analysis
109                .dependencies_of(computed.as_str())
110                .iter()
111                .any(|dependency| written.iter().any(|state| state.as_str() == dependency))
112        })
113}
114
115#[cfg(test)]
116mod tests {
117    use crate::{
118        build_application_semantic_model, build_context_update_plan, lower_components_to_ir,
119        optimize_context_ir, ContextValueSourceId, ProviderId,
120    };
121
122    #[test]
123    fn plans_each_reactive_provider_once_per_matching_action_batch() {
124        let model = build_application_semantic_model(&presolve_parser::parse_file(
125            "src/App.tsx",
126            r#"
127@component("x-app")
128class App extends Component {
129  count = state(1);
130  other = state(0);
131  @computed() get doubled(): number { return this.count * 2; }
132  @context() total!: number;
133  @provide(App.total) providedTotal: number = this.doubled;
134  @consume(App.total) total!: number;
135  @action() increment() { this.count += 1; this.count += 1; }
136  @action() ignore() { this.other += 1; }
137  render() { return <main />; }
138}
139"#,
140        ));
141        let component = &model.components[0].id;
142        let source =
143            ContextValueSourceId::Provider(ProviderId::for_component(component, "providedTotal"));
144        let plan = build_context_update_plan(
145            &model,
146            &optimize_context_ir(&lower_components_to_ir(&model)),
147        );
148        let increment = plan
149            .actions
150            .iter()
151            .find(|action| action.action_batch == component.action_batch("increment"))
152            .expect("increment plan");
153        let ignore = plan
154            .actions
155            .iter()
156            .find(|action| action.action_batch == component.action_batch("ignore"))
157            .expect("ignore plan");
158        assert_eq!(increment.invalidated_sources, vec![source]);
159        assert!(increment.affected_consumers.len() == 1);
160        assert!(ignore.invalidated_sources.is_empty());
161    }
162}