1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3
4use crate::component_graph::{
5 ArithmeticOperator, ComparisonOperator, LogicalOperator, UnaryOperator,
6};
7use crate::{
8 ApplicationSemanticModel, CapabilityOperationId, CapabilityOperationKind, ComponentNode,
9 ComputedPurity, ComputedValue, ConsumerId, ContextConsumerAvailabilityStatus,
10 ContextEvaluationBatchId, ContextSourcePlanStatus, ContextValueSourceId, Effect,
11 EffectCompatibility, EffectStatementKind, EffectValidation, ExpressionNode, ExpressionNodeKind,
12 ResourceId, SemanticId, SemanticReference, SemanticReferenceKind, SemanticType, SemanticTypeId,
13 SerializableValue, SourceProvenance, EFFECT_CAPABILITY_REGISTRY,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub struct IntermediateRepresentation {
19 pub modules: Vec<IrModule>,
20 pub context_ir: ContextIrReport,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct IrModule {
26 pub path: PathBuf,
27 pub components: Vec<SemanticId>,
28 pub storages: Vec<IrStorage>,
29 pub storage_initializers: Vec<IrInstruction>,
30 pub template_entrypoints: Vec<IrTemplateEntrypoint>,
31 pub functions: Vec<IrFunction>,
32 pub computed_evaluations: Vec<IrComputedEvaluation>,
33 pub effect_executions: Vec<IrEffectExecution>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct IrTemplateEntrypoint {
38 pub template: SemanticId,
39 pub render_method: SemanticId,
40 pub provenance: SourceProvenance,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct IrComputedEvaluation {
46 pub computed: SemanticId,
47 pub function: SemanticId,
48 pub result: IrValueId,
49 pub provenance: SourceProvenance,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct IrEffectExecution {
55 pub effect: SemanticId,
56 pub function: SemanticId,
57 pub entry_block: IrBlockId,
58 pub completion: IrEffectCompletion,
59 pub capability_operations: Vec<CapabilityOperationId>,
60 pub cleanup_function: Option<SemanticId>,
61 pub cleanup_entry_block: Option<IrBlockId>,
62 pub cleanup_capability_operations: Vec<CapabilityOperationId>,
63 pub provenance: SourceProvenance,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub struct ContextValueSlotId(String);
69
70impl ContextValueSlotId {
71 #[must_use]
72 pub fn for_source(source: &ContextValueSourceId) -> Self {
73 match source {
74 ContextValueSourceId::Provider(provider) => Self(format!("{provider}/context-slot")),
75 ContextValueSourceId::ContextDefault(context) => {
76 Self(format!("{context}/default-context-slot"))
77 }
78 }
79 }
80
81 #[must_use]
82 pub fn as_str(&self) -> &str {
83 &self.0
84 }
85}
86
87impl std::fmt::Display for ContextValueSlotId {
88 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 formatter.write_str(&self.0)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
95pub struct ContextSourceFunctionId(SemanticId);
96
97impl ContextSourceFunctionId {
98 #[must_use]
99 pub fn for_source(source: &ContextValueSourceId) -> Self {
100 match source {
101 ContextValueSourceId::Provider(provider) => {
102 Self(provider.as_semantic_id().context_provider_function())
103 }
104 ContextValueSourceId::ContextDefault(context) => {
105 Self(context.as_semantic_id().context_default_function())
106 }
107 }
108 }
109
110 #[must_use]
111 pub const fn as_semantic_id(&self) -> &SemanticId {
112 &self.0
113 }
114}
115
116impl std::fmt::Display for ContextSourceFunctionId {
117 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 self.0.fmt(formatter)
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
124pub struct ContextConsumerLoadId(SemanticId);
125
126impl ContextConsumerLoadId {
127 #[must_use]
128 pub fn for_consumer(consumer: &ConsumerId) -> Self {
129 Self(consumer.as_semantic_id().context_consumer_load())
130 }
131
132 #[must_use]
133 pub const fn as_semantic_id(&self) -> &SemanticId {
134 &self.0
135 }
136}
137
138impl std::fmt::Display for ContextConsumerLoadId {
139 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 self.0.fmt(formatter)
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct IrContextSourceEvaluation {
147 pub source: ContextValueSourceId,
148 pub context: crate::ContextId,
149 pub function: ContextSourceFunctionId,
150 pub entry_block: IrBlockId,
151 pub result: IrValueId,
152 pub slot: ContextValueSlotId,
153 pub evaluation_batch: ContextEvaluationBatchId,
154 pub prerequisite_computed_batches: Vec<u32>,
155 pub provenance: SourceProvenance,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct IrContextLoad {
161 pub id: ContextConsumerLoadId,
162 pub slot: ContextValueSlotId,
163 pub result: IrValueId,
164}
165
166impl IrContextLoad {
167 #[must_use]
170 pub fn kind(&self) -> IrInstructionKind {
171 IrInstructionKind::LoadContextSlot {
172 slot: self.slot.clone(),
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct IrContextConsumerBinding {
180 pub consumer: ConsumerId,
181 pub context: crate::ContextId,
182 pub source: ContextValueSourceId,
183 pub slot: ContextValueSlotId,
184 pub load: IrContextLoad,
185 pub semantic_type: SemanticTypeId,
186 pub provenance: SourceProvenance,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Default)]
191pub struct ContextIrReport {
192 pub source_evaluations: Vec<IrContextSourceEvaluation>,
193 pub consumer_bindings: Vec<IrContextConsumerBinding>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct OptimizedIrContextSourceEvaluation {
199 pub source: ContextValueSourceId,
200 pub context: crate::ContextId,
201 pub function: ContextSourceFunctionId,
202 pub entry_block: IrBlockId,
203 pub result: IrValueId,
204 pub slot: ContextValueSlotId,
205 pub evaluation_batch: ContextEvaluationBatchId,
206 pub prerequisite_computed_batches: Vec<u32>,
207 pub provenance: SourceProvenance,
208}
209
210impl From<&IrContextSourceEvaluation> for OptimizedIrContextSourceEvaluation {
211 fn from(evaluation: &IrContextSourceEvaluation) -> Self {
212 Self {
213 source: evaluation.source.clone(),
214 context: evaluation.context.clone(),
215 function: evaluation.function.clone(),
216 entry_block: evaluation.entry_block.clone(),
217 result: evaluation.result.clone(),
218 slot: evaluation.slot.clone(),
219 evaluation_batch: evaluation.evaluation_batch.clone(),
220 prerequisite_computed_batches: evaluation.prerequisite_computed_batches.clone(),
221 provenance: evaluation.provenance.clone(),
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct OptimizedContextIrReport {
229 pub source_report: ContextIrReport,
230 pub optimized_module: IntermediateRepresentation,
231 pub source_evaluations: Vec<OptimizedIrContextSourceEvaluation>,
232 pub pass_metrics: Vec<IrOptimizationPassReport>,
233}
234
235impl ContextIrReport {
236 #[must_use]
237 pub fn context_source_evaluation(
238 &self,
239 source: &ContextValueSourceId,
240 ) -> Option<&IrContextSourceEvaluation> {
241 self.source_evaluations
242 .iter()
243 .find(|evaluation| evaluation.source == *source)
244 }
245
246 #[must_use]
247 pub fn context_consumer_binding(
248 &self,
249 consumer: &ConsumerId,
250 ) -> Option<&IrContextConsumerBinding> {
251 self.consumer_bindings
252 .iter()
253 .find(|binding| binding.consumer == *consumer)
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum IrEffectCompletion {
260 Normal,
261}
262
263impl IntermediateRepresentation {
264 #[must_use]
266 pub fn effect_ir_function(&self, effect: &SemanticId) -> Option<&SemanticId> {
267 self.modules
268 .iter()
269 .flat_map(|module| &module.effect_executions)
270 .find(|execution| execution.effect == *effect)
271 .map(|execution| &execution.function)
272 }
273
274 #[must_use]
275 pub fn context_source_evaluation(
276 &self,
277 source: &ContextValueSourceId,
278 ) -> Option<&IrContextSourceEvaluation> {
279 self.context_ir.context_source_evaluation(source)
280 }
281
282 #[must_use]
283 pub fn context_consumer_binding(
284 &self,
285 consumer: &ConsumerId,
286 ) -> Option<&IrContextConsumerBinding> {
287 self.context_ir.context_consumer_binding(consumer)
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
293pub struct IrDomNodeId(String);
294
295impl IrDomNodeId {
296 #[must_use]
297 pub fn for_template(template: &SemanticId, path: &str) -> Self {
298 Self(format!("{template}/dom:{path}"))
299 }
300
301 #[must_use]
302 pub fn as_str(&self) -> &str {
303 &self.0
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct IrDomNode {
310 pub id: IrDomNodeId,
311 pub kind: IrDomNodeKind,
312 pub provenance: SourceProvenance,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq)]
317pub enum IrDomNodeKind {
318 Element {
319 tag: String,
320 children: Vec<IrDomNodeId>,
321 },
322 Fragment {
323 children: Vec<IrDomNodeId>,
324 },
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
329pub struct IrDomText {
330 pub node: IrDomNodeId,
331 pub value: String,
332 pub provenance: SourceProvenance,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct IrDomBinding {
338 pub node: IrDomNodeId,
339 pub value: IrValueId,
340 pub provenance: SourceProvenance,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct IrDomAttribute {
346 pub node: IrDomNodeId,
347 pub name: String,
348 pub value: IrDomAttributeValue,
349 pub provenance: SourceProvenance,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq)]
353pub enum IrDomAttributeValue {
354 Static(String),
355 Binding(IrValueId),
356}
357
358#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct IrDomEvent {
361 pub node: IrDomNodeId,
362 pub event: String,
363 pub handler: SemanticId,
364 pub provenance: SourceProvenance,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct IrDomConditional {
370 pub condition: IrValueId,
371 pub when_true: IrDomNodeId,
372 pub when_false: Option<IrDomNodeId>,
373 pub provenance: SourceProvenance,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct IrDomList {
379 pub iterable: IrValueId,
380 pub item: IrValueId,
381 pub index: Option<IrValueId>,
382 pub body: IrDomNodeId,
383 pub provenance: SourceProvenance,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
388pub struct IrDomInspection {
389 pub nodes: BTreeMap<IrDomNodeId, IrDomNode>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Default)]
394pub struct IrReactiveGraph {
395 pub nodes: BTreeMap<String, IrReactiveNode>,
396 pub edges: Vec<IrReactiveEdge>,
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Default)]
402pub struct IrReactiveTransitiveAnalysis {
403 pub dependencies: BTreeMap<String, Vec<String>>,
404 pub dependents: BTreeMap<String, Vec<String>>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct IrReactiveCycle {
410 pub nodes: Vec<String>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Default)]
416pub struct IrReactiveCycleAnalysis {
417 pub cycles: Vec<IrReactiveCycle>,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Default)]
422pub struct IrComputedEvaluationPlan {
423 pub evaluation_order: Vec<String>,
424 pub update_batches: Vec<Vec<String>>,
425 pub unplanned: Vec<String>,
426}
427
428#[must_use]
433pub fn analyze_reactive_transitive_graph(graph: &IrReactiveGraph) -> IrReactiveTransitiveAnalysis {
434 let dependencies = graph
435 .nodes
436 .keys()
437 .map(|id| {
438 (
439 id.clone(),
440 graph.transitive_targets(id, IrReactiveEdgeKind::Reads),
441 )
442 })
443 .collect();
444 let dependents = graph
445 .nodes
446 .keys()
447 .map(|id| {
448 (
449 id.clone(),
450 graph.transitive_targets(id, IrReactiveEdgeKind::Invalidates),
451 )
452 })
453 .collect();
454
455 IrReactiveTransitiveAnalysis {
456 dependencies,
457 dependents,
458 }
459}
460
461#[must_use]
463pub fn analyze_reactive_cycles(graph: &IrReactiveGraph) -> IrReactiveCycleAnalysis {
464 let computed = graph
465 .nodes
466 .iter()
467 .filter(|(_, node)| node.kind == IrReactiveNodeKind::Computed)
468 .map(|(id, _)| id.clone())
469 .collect::<BTreeSet<_>>();
470 let adjacency = computed
471 .iter()
472 .map(|id| {
473 let targets = graph
474 .edges
475 .iter()
476 .filter(|edge| {
477 edge.source == *id
478 && edge.kind == IrReactiveEdgeKind::Reads
479 && computed.contains(&edge.target)
480 })
481 .map(|edge| edge.target.clone())
482 .collect();
483 (id.clone(), targets)
484 })
485 .collect::<BTreeMap<String, BTreeSet<String>>>();
486 let reverse_adjacency = reverse_reactive_adjacency(&adjacency);
487 let mut visited = BTreeSet::new();
488 let mut finish_order = Vec::new();
489 for node in &computed {
490 visit_reactive_node(node, &adjacency, &mut visited, &mut finish_order);
491 }
492
493 let mut cycles = Vec::new();
494 visited.clear();
495 for node in finish_order.into_iter().rev() {
496 if !visited.insert(node.clone()) {
497 continue;
498 }
499 let mut members = BTreeSet::new();
500 collect_reactive_component(&node, &reverse_adjacency, &mut visited, &mut members);
501 let is_self_cycle = members.len() == 1
502 && adjacency
503 .get(&node)
504 .is_some_and(|targets| targets.contains(&node));
505 if members.len() > 1 || is_self_cycle {
506 cycles.push(IrReactiveCycle {
507 nodes: members.into_iter().collect(),
508 });
509 }
510 }
511 cycles.sort_by(|left, right| left.nodes.cmp(&right.nodes));
512
513 IrReactiveCycleAnalysis { cycles }
514}
515
516#[must_use]
519pub fn plan_computed_evaluation(graph: &IrReactiveGraph) -> IrComputedEvaluationPlan {
520 let nodes = graph
521 .nodes
522 .iter()
523 .filter(|(_, node)| node.kind == IrReactiveNodeKind::Computed)
524 .map(|(id, node)| (id.clone(), node.clone()))
525 .collect::<BTreeMap<_, _>>();
526 let edges = graph
527 .edges
528 .iter()
529 .filter(|edge| {
530 edge.kind == IrReactiveEdgeKind::Invalidates
531 && nodes.contains_key(&edge.source)
532 && nodes.contains_key(&edge.target)
533 })
534 .cloned()
535 .collect();
536 let inspection = IrUpdateScheduler::new(IrReactiveGraph { nodes, edges }).inspect();
537
538 IrComputedEvaluationPlan {
539 evaluation_order: inspection.order,
540 update_batches: inspection.batches,
541 unplanned: inspection.cycles,
542 }
543}
544
545fn reverse_reactive_adjacency(
546 adjacency: &BTreeMap<String, BTreeSet<String>>,
547) -> BTreeMap<String, BTreeSet<String>> {
548 let mut reversed = adjacency
549 .keys()
550 .cloned()
551 .map(|node| (node, BTreeSet::new()))
552 .collect::<BTreeMap<_, _>>();
553 for (source, targets) in adjacency {
554 for target in targets {
555 reversed
556 .get_mut(target)
557 .expect("computed dependency target should be a reactive node")
558 .insert(source.clone());
559 }
560 }
561 reversed
562}
563
564fn visit_reactive_node(
565 node: &str,
566 adjacency: &BTreeMap<String, BTreeSet<String>>,
567 visited: &mut BTreeSet<String>,
568 finish_order: &mut Vec<String>,
569) {
570 if !visited.insert(node.to_string()) {
571 return;
572 }
573 for target in adjacency
574 .get(node)
575 .expect("computed reactive node should have adjacency")
576 {
577 visit_reactive_node(target, adjacency, visited, finish_order);
578 }
579 finish_order.push(node.to_string());
580}
581
582fn collect_reactive_component(
583 node: &str,
584 adjacency: &BTreeMap<String, BTreeSet<String>>,
585 visited: &mut BTreeSet<String>,
586 members: &mut BTreeSet<String>,
587) {
588 members.insert(node.to_string());
589 for target in adjacency
590 .get(node)
591 .expect("computed reactive node should have reverse adjacency")
592 {
593 if visited.insert(target.clone()) {
594 collect_reactive_component(target, adjacency, visited, members);
595 }
596 }
597}
598
599#[must_use]
606pub fn build_reactive_graph(
607 components: &[ComponentNode],
608 computed_values: &BTreeMap<SemanticId, ComputedValue>,
609 effects: &BTreeMap<SemanticId, Effect>,
610 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
611 references: &[SemanticReference],
612 provenance: &BTreeMap<SemanticId, SourceProvenance>,
613) -> IrReactiveGraph {
614 let mut nodes = BTreeMap::new();
615 for component in components {
616 for field in &component.state_fields {
617 let id = field.id.as_str().to_string();
618 nodes.insert(
619 id.clone(),
620 IrReactiveNode {
621 id,
622 kind: IrReactiveNodeKind::State,
623 provenance: provenance
624 .get(&field.id)
625 .expect("state field should have canonical provenance")
626 .clone(),
627 },
628 );
629 }
630 }
631 for resource in resource_declarations.values() {
632 let id = resource.id.as_str().to_string();
633 nodes.insert(
634 id.clone(),
635 IrReactiveNode {
636 id,
637 kind: IrReactiveNodeKind::Resource,
638 provenance: resource.provenance.clone(),
639 },
640 );
641 }
642 for computed in computed_values.values() {
643 let id = computed.id.as_str().to_string();
644 nodes.insert(
645 id.clone(),
646 IrReactiveNode {
647 id,
648 kind: IrReactiveNodeKind::Computed,
649 provenance: computed.provenance.clone(),
650 },
651 );
652 }
653 for effect in effects
654 .values()
655 .filter(|effect| effect.validation == EffectValidation::Valid)
656 {
657 let id = effect.id.as_str().to_string();
658 nodes.insert(
659 id.clone(),
660 IrReactiveNode {
661 id,
662 kind: IrReactiveNodeKind::Effect,
663 provenance: effect.provenance.clone(),
664 },
665 );
666 }
667
668 let mut edges = Vec::new();
669 for reference in references.iter().filter(|reference| {
670 matches!(
671 reference.kind,
672 SemanticReferenceKind::ComputedState
673 | SemanticReferenceKind::ComputedComputed
674 | SemanticReferenceKind::ComputedResource
675 | SemanticReferenceKind::EffectState
676 | SemanticReferenceKind::EffectComputed
677 )
678 }) {
679 let source = reference.source.as_str().to_string();
680 let target = reference.target.as_str().to_string();
681 if !nodes.contains_key(&source) || !nodes.contains_key(&target) {
682 continue;
683 }
684 edges.push(IrReactiveEdge {
685 source: source.clone(),
686 target: target.clone(),
687 kind: IrReactiveEdgeKind::Reads,
688 provenance: reference.provenance.clone(),
689 });
690 edges.push(IrReactiveEdge {
691 source: target,
692 target: source,
693 kind: IrReactiveEdgeKind::Invalidates,
694 provenance: reference.provenance.clone(),
695 });
696 }
697 edges.sort_by(|left, right| {
698 (left.kind, left.source.as_str(), left.target.as_str()).cmp(&(
699 right.kind,
700 right.source.as_str(),
701 right.target.as_str(),
702 ))
703 });
704 edges.dedup_by(|left, right| {
705 left.kind == right.kind && left.source == right.source && left.target == right.target
706 });
707
708 IrReactiveGraph { nodes, edges }
709}
710
711#[derive(Debug, Clone, PartialEq, Eq)]
712pub struct IrReactiveNode {
713 pub id: String,
714 pub kind: IrReactiveNodeKind,
715 pub provenance: SourceProvenance,
716}
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq)]
719pub enum IrReactiveNodeKind {
720 State,
721 Resource,
722 Computed,
723 Effect,
724 Action,
725 Template,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq)]
729pub struct IrReactiveEdge {
730 pub source: String,
731 pub target: String,
732 pub kind: IrReactiveEdgeKind,
733 pub provenance: SourceProvenance,
734}
735
736#[derive(Debug, Clone, PartialEq, Eq)]
738pub struct IrUpdateScheduler {
739 pub graph: IrReactiveGraph,
740}
741
742#[derive(Debug, Clone, PartialEq, Eq)]
744pub struct IrSchedulerInspection {
745 pub order: Vec<String>,
746 pub batches: Vec<Vec<String>>,
747 pub cycles: Vec<String>,
748}
749
750impl IrUpdateScheduler {
751 #[must_use]
752 pub fn new(graph: IrReactiveGraph) -> Self {
753 Self { graph }
754 }
755
756 #[must_use]
757 pub fn dependency_order(&self) -> Vec<String> {
758 let mut incoming = self
759 .graph
760 .nodes
761 .keys()
762 .cloned()
763 .map(|id| (id, 0_usize))
764 .collect::<BTreeMap<_, _>>();
765 for edge in &self.graph.edges {
766 if let Some(count) = incoming.get_mut(&edge.target) {
767 *count += 1;
768 }
769 }
770 let mut ready = incoming
771 .iter()
772 .filter(|(_, count)| **count == 0)
773 .map(|(id, _)| id.clone())
774 .collect::<BTreeSet<_>>();
775 let mut order = Vec::new();
776 while let Some(id) = ready.pop_first() {
777 order.push(id.clone());
778 for edge in self.graph.dependents_of(&id) {
779 if let Some(count) = incoming.get_mut(&edge.target) {
780 *count -= 1;
781 if *count == 0 {
782 ready.insert(edge.target.clone());
783 }
784 }
785 }
786 }
787 order
788 }
789
790 #[must_use]
791 pub fn update_batches(&self) -> Vec<Vec<String>> {
792 let mut incoming = self
793 .graph
794 .nodes
795 .keys()
796 .cloned()
797 .map(|id| (id, 0_usize))
798 .collect::<BTreeMap<_, _>>();
799 for edge in &self.graph.edges {
800 if let Some(count) = incoming.get_mut(&edge.target) {
801 *count += 1;
802 }
803 }
804 let mut ready = incoming
805 .iter()
806 .filter(|(_, count)| **count == 0)
807 .map(|(id, _)| id.clone())
808 .collect::<BTreeSet<_>>();
809 let mut batches = Vec::new();
810 while !ready.is_empty() {
811 let batch = std::mem::take(&mut ready).into_iter().collect::<Vec<_>>();
812 for id in &batch {
813 for edge in self.graph.dependents_of(id) {
814 if let Some(count) = incoming.get_mut(&edge.target) {
815 *count -= 1;
816 if *count == 0 {
817 ready.insert(edge.target.clone());
818 }
819 }
820 }
821 }
822 batches.push(batch);
823 }
824 batches
825 }
826
827 #[must_use]
828 pub fn cyclic_nodes(&self) -> Vec<String> {
829 let ordered = self.dependency_order().into_iter().collect::<BTreeSet<_>>();
830 self.graph
831 .nodes
832 .keys()
833 .filter(|id| !ordered.contains(*id))
834 .cloned()
835 .collect()
836 }
837
838 #[must_use]
839 pub fn inspect(&self) -> IrSchedulerInspection {
840 IrSchedulerInspection {
841 order: self.dependency_order(),
842 batches: self.update_batches(),
843 cycles: self.cyclic_nodes(),
844 }
845 }
846}
847
848#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
849pub enum IrReactiveEdgeKind {
850 Reads,
851 Invalidates,
852}
853
854impl IrReactiveGraph {
855 fn transitive_targets(&self, source: &str, kind: IrReactiveEdgeKind) -> Vec<String> {
856 let mut discovered = BTreeSet::new();
857 let mut pending = BTreeSet::from([source.to_string()]);
858 while let Some(current) = pending.pop_first() {
859 for edge in self
860 .edges
861 .iter()
862 .filter(|edge| edge.source == current && edge.kind == kind)
863 {
864 if discovered.insert(edge.target.clone()) {
865 pending.insert(edge.target.clone());
866 }
867 }
868 }
869 discovered.into_iter().collect()
870 }
871
872 #[must_use]
873 pub fn computed_dependencies(&self, computed: &str) -> Vec<&IrReactiveEdge> {
874 matches!(
875 self.nodes.get(computed).map(|node| node.kind),
876 Some(IrReactiveNodeKind::Computed)
877 )
878 .then(|| {
879 self.edges
880 .iter()
881 .filter(|edge| edge.source == computed && edge.kind == IrReactiveEdgeKind::Reads)
882 .collect()
883 })
884 .unwrap_or_default()
885 }
886
887 #[must_use]
888 pub fn action_dependencies(&self, action: &str) -> Vec<&IrReactiveEdge> {
889 matches!(
890 self.nodes.get(action).map(|node| node.kind),
891 Some(IrReactiveNodeKind::Action)
892 )
893 .then(|| {
894 self.edges
895 .iter()
896 .filter(|edge| edge.source == action && edge.kind == IrReactiveEdgeKind::Reads)
897 .collect()
898 })
899 .unwrap_or_default()
900 }
901
902 #[must_use]
903 pub fn invalidations_from(&self, source: &str) -> Vec<&IrReactiveEdge> {
904 self.edges
905 .iter()
906 .filter(|edge| edge.source == source && edge.kind == IrReactiveEdgeKind::Invalidates)
907 .collect()
908 }
909
910 #[must_use]
911 pub fn dependencies_of(&self, target: &str) -> Vec<&IrReactiveEdge> {
912 self.edges
913 .iter()
914 .filter(|edge| edge.target == target)
915 .collect()
916 }
917
918 #[must_use]
919 pub fn dependents_of(&self, source: &str) -> Vec<&IrReactiveEdge> {
920 self.edges
921 .iter()
922 .filter(|edge| edge.source == source)
923 .collect()
924 }
925}
926
927impl IrReactiveTransitiveAnalysis {
928 #[must_use]
929 pub fn dependencies_of(&self, node: &str) -> &[String] {
930 self.dependencies
931 .get(node)
932 .map(Vec::as_slice)
933 .unwrap_or_default()
934 }
935
936 #[must_use]
937 pub fn dependents_of(&self, node: &str) -> &[String] {
938 self.dependents
939 .get(node)
940 .map(Vec::as_slice)
941 .unwrap_or_default()
942 }
943}
944
945#[must_use]
946pub fn inspect_dom_nodes(nodes: Vec<IrDomNode>) -> IrDomInspection {
947 IrDomInspection {
948 nodes: nodes
949 .into_iter()
950 .map(|node| (node.id.clone(), node))
951 .collect(),
952 }
953}
954
955#[must_use]
957pub fn lower_components_to_ir(model: &ApplicationSemanticModel) -> IntermediateRepresentation {
958 let mut modules = std::collections::BTreeMap::<PathBuf, IrModule>::new();
959 for component in &model.components {
960 let Some(provenance) = model.provenance(&component.id) else {
961 continue;
962 };
963 let module = modules
964 .entry(provenance.path.clone())
965 .or_insert_with(|| IrModule {
966 path: provenance.path.clone(),
967 components: Vec::new(),
968 storages: Vec::new(),
969 storage_initializers: Vec::new(),
970 template_entrypoints: Vec::new(),
971 functions: Vec::new(),
972 computed_evaluations: Vec::new(),
973 effect_executions: Vec::new(),
974 });
975 lower_component_to_ir(model, component, module);
976 }
977 let context_ir = lower_context_ir(model, &mut modules);
978 IntermediateRepresentation {
979 modules: modules.into_values().collect(),
980 context_ir,
981 }
982}
983
984#[allow(clippy::too_many_lines)]
985fn lower_component_to_ir(
986 model: &ApplicationSemanticModel,
987 component: &ComponentNode,
988 module: &mut IrModule,
989) {
990 module.components.push(component.id.clone());
991 module
992 .storages
993 .extend(component.state_fields.iter().filter_map(|field| {
994 model.provenance(&field.id).map(|provenance| IrStorage {
995 id: IrStorageId::for_semantic_origin(&field.id),
996 semantic_origin: field.id.clone(),
997 value_type: model
998 .semantic_types
999 .assignments
1000 .get(&field.id)
1001 .map_or(SemanticType::Unknown, |assignment| {
1002 assignment.semantic_type.clone()
1003 }),
1004 initial_value: field.initial_value.clone(),
1005 provenance: provenance.clone(),
1006 })
1007 }));
1008 let storage_offset = module.storage_initializers.len();
1009 module
1010 .storage_initializers
1011 .extend(
1012 component
1013 .state_fields
1014 .iter()
1015 .enumerate()
1016 .filter_map(|(index, field)| {
1017 model.provenance(&field.id).map(|provenance| IrInstruction {
1018 id: IrInstructionId::for_module(&module.path, storage_offset + index),
1019 provenance: provenance.clone(),
1020 result: None,
1021 semantic_origin: Some(field.id.clone()),
1022 kind: IrInstructionKind::InitializeStorage {
1023 storage: IrStorageId::for_semantic_origin(&field.id),
1024 },
1025 })
1026 }),
1027 );
1028 if let (Some(template), Some(render)) = (
1029 model
1030 .templates
1031 .iter()
1032 .find(|template| template.component_name == component.class_name),
1033 component
1034 .methods
1035 .iter()
1036 .find(|method| method.name == "render"),
1037 ) {
1038 module.template_entrypoints.push(IrTemplateEntrypoint {
1039 template: template.id.clone(),
1040 render_method: render.id.clone(),
1041 provenance: template.provenance.clone(),
1042 });
1043 }
1044 module
1045 .functions
1046 .extend(component.methods.iter().filter_map(|method| {
1047 model.provenance(&method.id).map(|provenance| IrFunction {
1048 id: method.id.clone(),
1049 name: method.name.clone(),
1050 provenance: provenance.clone(),
1051 entry_block: IrBlockId::entry_for(&method.id),
1052 blocks: vec![IrBlock {
1053 id: IrBlockId::entry_for(&method.id),
1054 provenance: provenance.clone(),
1055 instructions: Vec::new(),
1056 }],
1057 branch_edges: Vec::new(),
1058 values: BTreeMap::new(),
1059 loops: Vec::new(),
1060 })
1061 }));
1062 for computed in model
1063 .computed_evaluation_plan
1064 .evaluation_order
1065 .iter()
1066 .filter_map(|id| {
1067 model
1068 .computed_values
1069 .values()
1070 .find(|computed| computed.id.as_str() == id)
1071 })
1072 {
1073 if computed.owner.entity_id() != Some(&component.id)
1074 || computed.purity != ComputedPurity::Pure
1075 {
1076 continue;
1077 }
1078 if let Some((function, evaluation)) = lower_computed_evaluation(model, component, computed)
1079 {
1080 module.functions.push(function);
1081 module.computed_evaluations.push(evaluation);
1082 }
1083 }
1084 let executable_computed = module
1085 .computed_evaluations
1086 .iter()
1087 .map(|evaluation| evaluation.computed.clone())
1088 .collect::<BTreeSet<_>>();
1089 for effect in model.effects.values().filter(|effect| {
1090 effect.owner.entity_id() == Some(&component.id)
1091 && effect.validation == EffectValidation::Valid
1092 && effect_is_scheduled(model, &effect.id)
1093 }) {
1094 if let Some((functions, execution)) =
1095 lower_effect_execution(model, component, effect, &executable_computed)
1096 {
1097 module.functions.extend(functions);
1098 module.effect_executions.push(execution);
1099 }
1100 }
1101}
1102
1103fn lower_context_ir(
1104 model: &ApplicationSemanticModel,
1105 modules: &mut BTreeMap<PathBuf, IrModule>,
1106) -> ContextIrReport {
1107 let executable_computed = modules
1108 .values()
1109 .flat_map(|module| &module.computed_evaluations)
1110 .map(|evaluation| evaluation.computed.clone())
1111 .collect::<BTreeSet<_>>();
1112 let mut report = ContextIrReport::default();
1113 let mut slots = BTreeMap::new();
1114
1115 for batch in &model.context_evaluation.evaluation_batches {
1116 for source in &batch.sources {
1117 let Some(entry) = model.context_evaluation.context_source_plan(source) else {
1118 continue;
1119 };
1120 if entry.status != ContextSourcePlanStatus::Planned {
1121 continue;
1122 }
1123 let Some(component) = model
1124 .components
1125 .iter()
1126 .find(|component| component.id == entry.owner_component)
1127 else {
1128 continue;
1129 };
1130 let Some((function, evaluation)) = lower_context_source(
1131 model,
1132 component,
1133 entry,
1134 batch.id.clone(),
1135 &executable_computed,
1136 ) else {
1137 continue;
1138 };
1139 let Some(module) = modules.get_mut(&entry.provenance.path) else {
1140 continue;
1141 };
1142 slots.insert(source.clone(), evaluation.slot.clone());
1143 module.functions.push(function);
1144 report.source_evaluations.push(evaluation);
1145 }
1146 }
1147
1148 for (consumer, entry) in &model.context_evaluation.consumer_entries {
1149 if entry.status != ContextConsumerAvailabilityStatus::Available {
1150 continue;
1151 }
1152 let (Some(source), Some(slot), Some(entity), Some(context)) = (
1153 entry.selected_source.as_ref(),
1154 entry
1155 .selected_source
1156 .as_ref()
1157 .and_then(|source| slots.get(source)),
1158 model.consumers.get(consumer),
1159 model
1160 .consumers
1161 .get(consumer)
1162 .and_then(crate::ConsumerEntity::context),
1163 ) else {
1164 continue;
1165 };
1166 let load = IrContextLoad {
1167 id: ContextConsumerLoadId::for_consumer(consumer),
1168 slot: slot.clone(),
1169 result: IrValueId::for_function(
1170 ContextConsumerLoadId::for_consumer(consumer).as_semantic_id(),
1171 0,
1172 ),
1173 };
1174 report.consumer_bindings.push(IrContextConsumerBinding {
1175 consumer: consumer.clone(),
1176 context: context.clone(),
1177 source: source.clone(),
1178 slot: slot.clone(),
1179 load,
1180 semantic_type: entity.requested_type_id.clone(),
1181 provenance: entry.provenance.clone(),
1182 });
1183 }
1184 report
1185}
1186
1187fn lower_context_source(
1188 model: &ApplicationSemanticModel,
1189 component: &ComponentNode,
1190 entry: &crate::ContextSourcePlanEntry,
1191 evaluation_batch: ContextEvaluationBatchId,
1192 executable_computed: &BTreeSet<SemanticId>,
1193) -> Option<(IrFunction, IrContextSourceEvaluation)> {
1194 let function_id = ContextSourceFunctionId::for_source(&entry.source);
1195 let entry_block = IrBlockId::entry_for(function_id.as_semantic_id());
1196 let dependencies = entry
1197 .required_state
1198 .iter()
1199 .chain(&entry.required_computed)
1200 .cloned()
1201 .collect::<BTreeSet<_>>();
1202 let mut lowering = ExpressionIrLowering {
1203 model,
1204 component,
1205 function: function_id.as_semantic_id(),
1206 reference_owner: match &entry.source {
1207 ContextValueSourceId::Provider(provider) => provider.as_semantic_id(),
1208 ContextValueSourceId::ContextDefault(context) => context.as_semantic_id(),
1209 },
1210 entry_block: entry_block.clone(),
1211 instructions: Vec::new(),
1212 values: BTreeMap::new(),
1213 executable_computed: Some(executable_computed),
1214 allowed_dependencies: Some(&dependencies),
1215 };
1216 let result = lowering.lower_node(&entry.expression_root)?;
1217 let slot = ContextValueSlotId::for_source(&entry.source);
1218 lowering.instructions.push(IrInstruction {
1219 id: IrInstructionId::for_block(&entry_block, lowering.instructions.len()),
1220 provenance: entry.provenance.clone(),
1221 result: None,
1222 semantic_origin: Some(entry.expression_root.clone()),
1223 kind: IrInstructionKind::InitializeContextSlot {
1224 slot: slot.clone(),
1225 value: result.clone(),
1226 },
1227 });
1228 let function = IrFunction {
1229 id: function_id.as_semantic_id().clone(),
1230 name: format!("context-evaluate:{:?}", entry.source),
1231 provenance: entry.provenance.clone(),
1232 entry_block: entry_block.clone(),
1233 blocks: vec![IrBlock {
1234 id: entry_block.clone(),
1235 provenance: entry.provenance.clone(),
1236 instructions: lowering.instructions,
1237 }],
1238 branch_edges: Vec::new(),
1239 values: lowering.values,
1240 loops: Vec::new(),
1241 };
1242 let evaluation = IrContextSourceEvaluation {
1243 source: entry.source.clone(),
1244 context: entry.context.clone(),
1245 function: function_id,
1246 entry_block,
1247 result,
1248 slot,
1249 evaluation_batch,
1250 prerequisite_computed_batches: entry.prerequisite_computed_batches.clone(),
1251 provenance: entry.provenance.clone(),
1252 };
1253 Some((function, evaluation))
1254}
1255
1256fn lower_computed_evaluation(
1257 model: &ApplicationSemanticModel,
1258 component: &ComponentNode,
1259 computed: &ComputedValue,
1260) -> Option<(IrFunction, IrComputedEvaluation)> {
1261 let root = model.expression_graph.root_for(&computed.id)?.clone();
1262 let entry_block = IrBlockId::entry_for(&computed.id);
1263 let mut lowering = ExpressionIrLowering {
1264 model,
1265 component,
1266 function: &computed.id,
1267 reference_owner: &computed.id,
1268 entry_block: entry_block.clone(),
1269 instructions: Vec::new(),
1270 values: BTreeMap::new(),
1271 executable_computed: None,
1272 allowed_dependencies: None,
1273 };
1274 let result = lowering.lower_node(&root)?;
1275 let function = IrFunction {
1276 id: computed.id.clone(),
1277 name: computed.name.clone(),
1278 provenance: computed.provenance.clone(),
1279 entry_block: entry_block.clone(),
1280 blocks: vec![IrBlock {
1281 id: entry_block,
1282 provenance: computed.provenance.clone(),
1283 instructions: lowering.instructions,
1284 }],
1285 branch_edges: Vec::new(),
1286 values: lowering.values,
1287 loops: Vec::new(),
1288 };
1289 let evaluation = IrComputedEvaluation {
1290 computed: computed.id.clone(),
1291 function: function.id.clone(),
1292 result,
1293 provenance: computed.provenance.clone(),
1294 };
1295 Some((function, evaluation))
1296}
1297
1298fn lower_effect_execution(
1299 model: &ApplicationSemanticModel,
1300 component: &ComponentNode,
1301 effect: &Effect,
1302 executable_computed: &BTreeSet<SemanticId>,
1303) -> Option<(Vec<IrFunction>, IrEffectExecution)> {
1304 let body = model.effect_body(&effect.id)?;
1305 let (function, capability_operations) = lower_effect_program(
1306 model,
1307 component,
1308 effect,
1309 &effect.id,
1310 &effect.name,
1311 &body.statements,
1312 &effect.provenance,
1313 executable_computed,
1314 )?;
1315 let cleanup = if let Some(cleanup) = &body.cleanup {
1316 let id = effect.id.effect_cleanup_program();
1317 let (function, operations) = lower_effect_program(
1318 model,
1319 component,
1320 effect,
1321 &id,
1322 &format!("{} cleanup", effect.name),
1323 &cleanup.statements,
1324 &cleanup.provenance,
1325 executable_computed,
1326 )?;
1327 Some((id, function, operations))
1328 } else {
1329 None
1330 };
1331 let entry_block = function.entry_block.clone();
1332 let (cleanup_function, cleanup_entry_block, cleanup_capability_operations, functions) =
1333 match cleanup {
1334 Some((id, cleanup_function, operations)) => {
1335 let entry = cleanup_function.entry_block.clone();
1336 (
1337 Some(id),
1338 Some(entry),
1339 operations,
1340 vec![function, cleanup_function],
1341 )
1342 }
1343 None => (None, None, Vec::new(), vec![function]),
1344 };
1345 Some((
1346 functions,
1347 IrEffectExecution {
1348 effect: effect.id.clone(),
1349 function: effect.id.clone(),
1350 entry_block,
1351 completion: IrEffectCompletion::Normal,
1352 capability_operations,
1353 cleanup_function,
1354 cleanup_entry_block,
1355 cleanup_capability_operations,
1356 provenance: effect.provenance.clone(),
1357 },
1358 ))
1359}
1360
1361#[allow(clippy::too_many_arguments)]
1362fn lower_effect_program(
1363 model: &ApplicationSemanticModel,
1364 component: &ComponentNode,
1365 effect: &Effect,
1366 function_id: &SemanticId,
1367 name: &str,
1368 statements: &[SemanticId],
1369 provenance: &SourceProvenance,
1370 executable_computed: &BTreeSet<SemanticId>,
1371) -> Option<(IrFunction, Vec<CapabilityOperationId>)> {
1372 let entry_block = IrBlockId::entry_for(function_id);
1373 let mut lowering = ExpressionIrLowering {
1374 model,
1375 component,
1376 function: function_id,
1377 reference_owner: &effect.id,
1378 entry_block: entry_block.clone(),
1379 instructions: Vec::new(),
1380 values: BTreeMap::new(),
1381 executable_computed: Some(executable_computed),
1382 allowed_dependencies: None,
1383 };
1384 let mut capability_operations = Vec::new();
1385 for statement_id in statements {
1386 let statement = model.effect_statement(statement_id)?;
1387 let record = model.effect_statement_type(statement_id)?;
1388 match &statement.kind {
1389 EffectStatementKind::ExternalMemberAssignment { value, .. } => {
1390 let operation =
1391 effect_capability_operation(record, CapabilityOperationKind::MemberAssignment)?;
1392 let value = lowering.lower_node(value)?;
1393 lowering.emit_effect(
1394 statement,
1395 IrInstructionKind::CapabilityAssign {
1396 operation: operation.id,
1397 value,
1398 },
1399 );
1400 capability_operations.push(operation.id);
1401 }
1402 EffectStatementKind::CapabilityCall { arguments, .. } => {
1403 let operation =
1404 effect_capability_operation(record, CapabilityOperationKind::MethodCall)?;
1405 let arguments = arguments
1406 .iter()
1407 .map(|argument| lowering.lower_node(argument))
1408 .collect::<Option<Vec<_>>>()?;
1409 lowering.emit_effect(
1410 statement,
1411 IrInstructionKind::CapabilityCall {
1412 operation: operation.id,
1413 arguments,
1414 },
1415 );
1416 capability_operations.push(operation.id);
1417 }
1418 EffectStatementKind::EffectReturn { value: None } | EffectStatementKind::Empty => {}
1419 EffectStatementKind::EffectReturn { value: Some(_) }
1420 | EffectStatementKind::Unsupported(_) => return None,
1421 }
1422 }
1423 let function = IrFunction {
1424 id: function_id.clone(),
1425 name: name.to_owned(),
1426 provenance: provenance.clone(),
1427 entry_block: entry_block.clone(),
1428 blocks: vec![IrBlock {
1429 id: entry_block.clone(),
1430 provenance: provenance.clone(),
1431 instructions: lowering.instructions,
1432 }],
1433 branch_edges: Vec::new(),
1434 values: lowering.values,
1435 loops: Vec::new(),
1436 };
1437 Some((function, capability_operations))
1438}
1439
1440fn effect_is_scheduled(model: &ApplicationSemanticModel, effect: &SemanticId) -> bool {
1441 model
1442 .effect_execution_plan
1443 .initial
1444 .effect_batches
1445 .iter()
1446 .chain(
1447 model
1448 .effect_execution_plan
1449 .actions
1450 .iter()
1451 .flat_map(|action| action.effect_batches.iter()),
1452 )
1453 .any(|batch| batch.effects.contains(effect))
1454}
1455
1456fn effect_capability_operation(
1457 record: &crate::EffectStatementTypeRecord,
1458 kind: CapabilityOperationKind,
1459) -> Option<&'static crate::CapabilityOperation> {
1460 let operation = EFFECT_CAPABILITY_REGISTRY.operation(record.capability_operation?)?;
1461 (operation.kind == kind).then_some(operation)
1462}
1463
1464struct ExpressionIrLowering<'a> {
1465 model: &'a ApplicationSemanticModel,
1466 component: &'a ComponentNode,
1467 function: &'a SemanticId,
1468 reference_owner: &'a SemanticId,
1469 entry_block: IrBlockId,
1470 instructions: Vec<IrInstruction>,
1471 values: BTreeMap<IrValueId, IrValue>,
1472 executable_computed: Option<&'a BTreeSet<SemanticId>>,
1473 allowed_dependencies: Option<&'a BTreeSet<SemanticId>>,
1474}
1475
1476impl ExpressionIrLowering<'_> {
1477 fn lower_node(&mut self, id: &SemanticId) -> Option<IrValueId> {
1478 let node = self.model.expression_graph.node(id)?.clone();
1479 let kind = match node.kind.clone() {
1480 ExpressionNodeKind::Literal(value) => IrInstructionKind::Constant {
1481 value: ir_constant(value),
1482 },
1483 ExpressionNodeKind::Boolean(value) => IrInstructionKind::Constant {
1484 value: IrConstant::Boolean(value),
1485 },
1486 ExpressionNodeKind::Identifier(_) => {
1487 return None;
1488 }
1489 ExpressionNodeKind::Call { .. } => {
1490 return None;
1491 }
1492 ExpressionNodeKind::BuiltinPureCall {
1493 operation,
1494 arguments,
1495 } => match operation {
1496 crate::component_graph::BuiltinPureOperation::MathAbs
1497 | crate::component_graph::BuiltinPureOperation::MathFloor
1498 | crate::component_graph::BuiltinPureOperation::MathCeil
1499 | crate::component_graph::BuiltinPureOperation::MathRound => {
1500 let [argument] = arguments.as_slice() else {
1501 return None;
1502 };
1503 IrInstructionKind::Unary {
1504 operation: match operation {
1505 crate::component_graph::BuiltinPureOperation::MathAbs => {
1506 IrUnaryOperation::Abs
1507 }
1508 crate::component_graph::BuiltinPureOperation::MathFloor => {
1509 IrUnaryOperation::Floor
1510 }
1511 crate::component_graph::BuiltinPureOperation::MathCeil => {
1512 IrUnaryOperation::Ceil
1513 }
1514 crate::component_graph::BuiltinPureOperation::MathRound => {
1515 IrUnaryOperation::Round
1516 }
1517 crate::component_graph::BuiltinPureOperation::MathMin
1518 | crate::component_graph::BuiltinPureOperation::MathMax => {
1519 unreachable!()
1520 }
1521 },
1522 operand: IrOperand::Value(self.lower_node(argument)?),
1523 }
1524 }
1525 crate::component_graph::BuiltinPureOperation::MathMin
1526 | crate::component_graph::BuiltinPureOperation::MathMax => {
1527 let [left, right] = arguments.as_slice() else {
1528 return None;
1529 };
1530 IrInstructionKind::Binary {
1531 operation: match operation {
1532 crate::component_graph::BuiltinPureOperation::MathMin => {
1533 IrBinaryOperation::Min
1534 }
1535 crate::component_graph::BuiltinPureOperation::MathMax => {
1536 IrBinaryOperation::Max
1537 }
1538 crate::component_graph::BuiltinPureOperation::MathAbs
1539 | crate::component_graph::BuiltinPureOperation::MathFloor
1540 | crate::component_graph::BuiltinPureOperation::MathCeil
1541 | crate::component_graph::BuiltinPureOperation::MathRound => {
1542 unreachable!()
1543 }
1544 },
1545 left: IrOperand::Value(self.lower_node(left)?),
1546 right: IrOperand::Value(self.lower_node(right)?),
1547 }
1548 }
1549 },
1550 ExpressionNodeKind::Template {
1551 quasis,
1552 expressions,
1553 } => IrInstructionKind::Template {
1554 quasis,
1555 expressions: expressions
1556 .iter()
1557 .map(|expression| self.lower_node(expression))
1558 .collect::<Option<Vec<_>>>()?,
1559 },
1560 ExpressionNodeKind::SemanticPackagePureCall {
1561 package,
1562 version,
1563 integrity,
1564 export,
1565 runtime_module,
1566 resume_policy,
1567 operation,
1568 arguments,
1569 } => IrInstructionKind::PurePackageCall {
1570 package,
1571 version,
1572 integrity,
1573 export,
1574 runtime_module,
1575 resume_policy,
1576 operation,
1577 arguments: arguments
1578 .iter()
1579 .map(|argument| self.lower_node(argument))
1580 .collect::<Option<Vec<_>>>()?,
1581 },
1582 ExpressionNodeKind::ThisMember { name } => self.lower_this_member(&name)?,
1583 ExpressionNodeKind::MemberAccess {
1584 object,
1585 property,
1586 optional,
1587 } => IrInstructionKind::GetMember {
1588 object: IrOperand::Value(self.lower_node(&object)?),
1589 property,
1590 optional,
1591 },
1592 ExpressionNodeKind::IndexAccess { object, index } => IrInstructionKind::GetIndex {
1593 object: IrOperand::Value(self.lower_node(&object)?),
1594 index: IrOperand::Value(self.lower_node(&index)?),
1595 },
1596 ExpressionNodeKind::Conditional {
1597 condition,
1598 when_true,
1599 when_false,
1600 } => IrInstructionKind::Select {
1601 condition: IrOperand::Value(self.lower_node(&condition)?),
1602 when_true: IrOperand::Value(self.lower_node(&when_true)?),
1603 when_false: IrOperand::Value(self.lower_node(&when_false)?),
1604 },
1605 ExpressionNodeKind::Arithmetic {
1606 left,
1607 right,
1608 operator,
1609 } => IrInstructionKind::Binary {
1610 operation: ir_arithmetic_operation(operator),
1611 left: IrOperand::Value(self.lower_node(&left)?),
1612 right: IrOperand::Value(self.lower_node(&right)?),
1613 },
1614 ExpressionNodeKind::Comparison {
1615 left,
1616 right,
1617 operator,
1618 } => IrInstructionKind::Binary {
1619 operation: ir_comparison_operation(operator),
1620 left: IrOperand::Value(self.lower_node(&left)?),
1621 right: IrOperand::Value(self.lower_node(&right)?),
1622 },
1623 ExpressionNodeKind::Logical {
1624 left,
1625 right,
1626 operator,
1627 } => IrInstructionKind::Binary {
1628 operation: ir_logical_operation(operator),
1629 left: IrOperand::Value(self.lower_node(&left)?),
1630 right: IrOperand::Value(self.lower_node(&right)?),
1631 },
1632 ExpressionNodeKind::NullishCoalescing { left, right } => IrInstructionKind::Binary {
1633 operation: IrBinaryOperation::NullishCoalesce,
1634 left: IrOperand::Value(self.lower_node(&left)?),
1635 right: IrOperand::Value(self.lower_node(&right)?),
1636 },
1637 ExpressionNodeKind::Unary { operand, operator } => IrInstructionKind::Unary {
1638 operation: ir_unary_operation(operator),
1639 operand: IrOperand::Value(self.lower_node(&operand)?),
1640 },
1641 };
1642 Some(self.emit(node, kind))
1643 }
1644
1645 fn lower_this_member(&self, name: &str) -> Option<IrInstructionKind> {
1646 let target = self
1647 .component
1648 .state_fields
1649 .iter()
1650 .find(|field| field.name == name)
1651 .map(|field| field.id.clone())
1652 .or_else(|| {
1653 self.model
1654 .computed_values
1655 .get(&self.component.id.computed(name))
1656 .map(|computed| computed.id.clone())
1657 })
1658 .or_else(|| {
1659 let resource = ResourceId::for_owner(&self.component.id, name);
1660 self.model
1661 .resource_declarations
1662 .contains_key(&resource)
1663 .then_some(resource.as_semantic_id().clone())
1664 })?;
1665 let has_reference = self.model.references.iter().any(|reference| {
1666 reference.source == *self.reference_owner && reference.target == target
1667 });
1668 if !has_reference
1669 && !self
1670 .allowed_dependencies
1671 .is_some_and(|dependencies| dependencies.contains(&target))
1672 {
1673 return None;
1674 }
1675 if self
1676 .component
1677 .state_fields
1678 .iter()
1679 .any(|field| field.id == target)
1680 {
1681 Some(IrInstructionKind::LoadStorage {
1682 storage: IrStorageId::for_semantic_origin(&target),
1683 })
1684 } else {
1685 if self
1686 .model
1687 .resource_declarations
1688 .contains_key(&ResourceId::for_owner(&self.component.id, name))
1689 {
1690 return Some(IrInstructionKind::LoadResource {
1691 declaration: target,
1692 });
1693 }
1694 if self
1695 .executable_computed
1696 .is_some_and(|computed| !computed.contains(&target))
1697 {
1698 return None;
1699 }
1700 Some(IrInstructionKind::LoadComputed { computed: target })
1701 }
1702 }
1703
1704 fn emit(&mut self, node: ExpressionNode, kind: IrInstructionKind) -> IrValueId {
1705 let value = IrValueId::for_function(self.function, self.values.len());
1706 let instruction = IrInstructionId::for_block(&self.entry_block, self.instructions.len());
1707 self.values.insert(
1708 value.clone(),
1709 IrValue {
1710 id: value.clone(),
1711 definition: IrValueDefinition::Instruction(instruction.clone()),
1712 semantic_type: self
1713 .model
1714 .semantic_type_of(&node.id)
1715 .cloned()
1716 .unwrap_or(SemanticType::Unknown),
1717 provenance: node.provenance.clone(),
1718 semantic_origin: Some(node.id.clone()),
1719 },
1720 );
1721 self.instructions.push(IrInstruction {
1722 id: instruction,
1723 provenance: node.provenance,
1724 result: Some(value.clone()),
1725 semantic_origin: Some(node.id),
1726 kind,
1727 });
1728 value
1729 }
1730
1731 fn emit_effect(&mut self, statement: &crate::EffectStatement, kind: IrInstructionKind) {
1732 let instruction = IrInstructionId::for_block(&self.entry_block, self.instructions.len());
1733 self.instructions.push(IrInstruction {
1734 id: instruction,
1735 provenance: statement.provenance.clone(),
1736 result: None,
1737 semantic_origin: Some(statement.id.clone()),
1738 kind,
1739 });
1740 }
1741}
1742
1743fn ir_constant(value: SerializableValue) -> IrConstant {
1744 match value {
1745 SerializableValue::Null => IrConstant::Null,
1746 SerializableValue::Boolean(value) => IrConstant::Boolean(value),
1747 SerializableValue::Number(value) => IrConstant::Number(value),
1748 SerializableValue::String(value) => IrConstant::String(value),
1749 SerializableValue::Array(value) => IrConstant::Array(value),
1750 SerializableValue::Object(value) => IrConstant::Object(value),
1751 }
1752}
1753
1754const fn ir_arithmetic_operation(operator: ArithmeticOperator) -> IrBinaryOperation {
1755 match operator {
1756 ArithmeticOperator::Add => IrBinaryOperation::Add,
1757 ArithmeticOperator::Subtract => IrBinaryOperation::Subtract,
1758 ArithmeticOperator::Multiply => IrBinaryOperation::Multiply,
1759 ArithmeticOperator::Divide => IrBinaryOperation::Divide,
1760 ArithmeticOperator::Remainder => IrBinaryOperation::Remainder,
1761 }
1762}
1763
1764const fn ir_comparison_operation(operator: ComparisonOperator) -> IrBinaryOperation {
1765 match operator {
1766 ComparisonOperator::Equal => IrBinaryOperation::Equal,
1767 ComparisonOperator::NotEqual => IrBinaryOperation::NotEqual,
1768 ComparisonOperator::LessThan => IrBinaryOperation::LessThan,
1769 ComparisonOperator::LessThanOrEqual => IrBinaryOperation::LessThanOrEqual,
1770 ComparisonOperator::GreaterThan => IrBinaryOperation::GreaterThan,
1771 ComparisonOperator::GreaterThanOrEqual => IrBinaryOperation::GreaterThanOrEqual,
1772 }
1773}
1774
1775const fn ir_logical_operation(operator: LogicalOperator) -> IrBinaryOperation {
1776 match operator {
1777 LogicalOperator::And => IrBinaryOperation::And,
1778 LogicalOperator::Or => IrBinaryOperation::Or,
1779 }
1780}
1781
1782const fn ir_unary_operation(operator: UnaryOperator) -> IrUnaryOperation {
1783 match operator {
1784 UnaryOperator::Not => IrUnaryOperation::Not,
1785 UnaryOperator::Plus => IrUnaryOperation::Identity,
1786 UnaryOperator::Minus => IrUnaryOperation::Negate,
1787 }
1788}
1789
1790#[derive(Debug, Clone, PartialEq, Eq)]
1792pub struct IrFunction {
1793 pub id: SemanticId,
1794 pub name: String,
1795 pub provenance: SourceProvenance,
1796 pub entry_block: IrBlockId,
1797 pub blocks: Vec<IrBlock>,
1798 pub branch_edges: Vec<IrBranchEdge>,
1799 pub values: BTreeMap<IrValueId, IrValue>,
1800 pub loops: Vec<IrLoop>,
1801}
1802
1803impl IrFunction {
1804 #[must_use]
1805 pub fn block(&self, id: &IrBlockId) -> Option<&IrBlock> {
1806 self.blocks.iter().find(|block| block.id == *id)
1807 }
1808
1809 #[must_use]
1810 pub fn successor_blocks(&self, id: &IrBlockId) -> Vec<IrBlockId> {
1811 self.branch_edges
1812 .iter()
1813 .filter(|edge| edge.from == *id)
1814 .map(|edge| edge.to.clone())
1815 .collect::<BTreeSet<_>>()
1816 .into_iter()
1817 .collect()
1818 }
1819
1820 #[must_use]
1821 pub fn predecessor_blocks(&self, id: &IrBlockId) -> Vec<IrBlockId> {
1822 self.branch_edges
1823 .iter()
1824 .filter(|edge| edge.to == *id)
1825 .map(|edge| edge.from.clone())
1826 .collect::<BTreeSet<_>>()
1827 .into_iter()
1828 .collect()
1829 }
1830
1831 #[must_use]
1832 pub fn is_exit_block(&self, id: &IrBlockId) -> bool {
1833 self.block(id).is_some() && self.successor_blocks(id).is_empty()
1834 }
1835
1836 #[must_use]
1837 pub fn value(&self, id: &IrValueId) -> Option<&IrValue> {
1838 self.values.get(id)
1839 }
1840}
1841
1842#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1844pub struct IrBlockId(String);
1845
1846impl IrBlockId {
1847 #[must_use]
1848 pub fn entry_for(function: &SemanticId) -> Self {
1849 Self::for_function(function, "entry")
1850 }
1851
1852 #[must_use]
1853 pub fn for_function(function: &SemanticId, name: &str) -> Self {
1854 Self(format!("{function}/block:{name}"))
1855 }
1856
1857 #[must_use]
1858 pub fn as_str(&self) -> &str {
1859 &self.0
1860 }
1861}
1862
1863impl std::fmt::Display for IrBlockId {
1864 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1865 formatter.write_str(&self.0)
1866 }
1867}
1868
1869#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1871pub struct IrInstructionId(String);
1872
1873impl IrInstructionId {
1874 #[must_use]
1875 pub fn for_block(block: &IrBlockId, index: usize) -> Self {
1876 Self(format!("{block}/instruction:{index}"))
1877 }
1878
1879 #[must_use]
1880 pub fn for_module(path: &Path, index: usize) -> Self {
1881 Self(format!("module:{}/instruction:{index}", path.display()))
1882 }
1883
1884 #[must_use]
1885 pub fn as_str(&self) -> &str {
1886 &self.0
1887 }
1888}
1889
1890impl std::fmt::Display for IrInstructionId {
1891 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1892 formatter.write_str(&self.0)
1893 }
1894}
1895
1896#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1898pub struct IrValueId(String);
1899
1900impl IrValueId {
1901 #[must_use]
1902 pub fn for_function(function: &SemanticId, index: usize) -> Self {
1903 Self(format!("{function}/value:{index}"))
1904 }
1905
1906 #[must_use]
1907 pub fn as_str(&self) -> &str {
1908 &self.0
1909 }
1910}
1911
1912impl std::fmt::Display for IrValueId {
1913 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1914 formatter.write_str(&self.0)
1915 }
1916}
1917
1918#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1920pub struct IrStorageId(String);
1921
1922impl IrStorageId {
1923 #[must_use]
1924 pub fn for_semantic_origin(origin: &SemanticId) -> Self {
1925 Self(format!("storage:{origin}"))
1926 }
1927
1928 #[must_use]
1929 pub fn as_str(&self) -> &str {
1930 &self.0
1931 }
1932}
1933
1934#[derive(Debug, Clone, PartialEq, Eq)]
1936pub struct IrStorage {
1937 pub id: IrStorageId,
1938 pub semantic_origin: SemanticId,
1939 pub value_type: SemanticType,
1940 pub initial_value: Option<crate::SerializableValue>,
1941 pub provenance: SourceProvenance,
1942}
1943
1944impl std::fmt::Display for IrStorageId {
1945 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1946 formatter.write_str(&self.0)
1947 }
1948}
1949
1950#[derive(Debug, Clone, PartialEq, Eq)]
1952pub enum IrConstant {
1953 Null,
1954 Boolean(bool),
1955 Number(String),
1956 String(String),
1957 Array(Vec<SerializableValue>),
1958 Object(BTreeMap<String, SerializableValue>),
1959}
1960
1961#[derive(Debug, Clone, PartialEq, Eq)]
1963pub enum IrOperand {
1964 Value(IrValueId),
1965 Constant(IrConstant),
1966 Storage(IrStorageId),
1967}
1968
1969#[derive(Debug, Clone, PartialEq, Eq)]
1971pub enum IrValueDefinition {
1972 Instruction(IrInstructionId),
1973 Parameter { function: SemanticId, index: usize },
1974 BlockParameter { block: IrBlockId, index: usize },
1975}
1976
1977#[derive(Debug, Clone, PartialEq, Eq)]
1979pub struct IrValue {
1980 pub id: IrValueId,
1981 pub definition: IrValueDefinition,
1982 pub semantic_type: SemanticType,
1983 pub provenance: SourceProvenance,
1984 pub semantic_origin: Option<SemanticId>,
1985}
1986
1987#[derive(Debug, Clone, PartialEq, Eq)]
1989pub struct IrValidationDiagnostic {
1990 pub code: &'static str,
1991 pub message: String,
1992}
1993
1994#[derive(Debug, Clone, PartialEq, Eq)]
1996pub struct IrUse {
1997 pub instruction: IrInstructionId,
1998 pub operand_index: usize,
1999}
2000
2001#[derive(Debug, Clone, PartialEq, Eq)]
2003pub struct IrDefinitionUseAnalysis {
2004 pub definitions: BTreeMap<IrValueId, IrValueDefinition>,
2005 pub uses: BTreeMap<IrValueId, Vec<IrUse>>,
2006}
2007
2008#[derive(Debug, Clone, PartialEq, Eq)]
2010pub struct IrUseDefinition {
2011 pub value: IrValueId,
2012 pub instruction: IrInstructionId,
2013 pub operand_index: usize,
2014 pub definition: IrValueDefinition,
2015}
2016
2017#[derive(Debug, Clone, PartialEq, Eq)]
2019pub struct IrLivenessAnalysis {
2020 pub live_in: BTreeMap<IrBlockId, Vec<IrValueId>>,
2021 pub live_out: BTreeMap<IrBlockId, Vec<IrValueId>>,
2022}
2023
2024#[derive(Debug, Clone, PartialEq, Eq)]
2026pub struct IrReachabilityAnalysis {
2027 pub reachable: Vec<IrBlockId>,
2028 pub unreachable: Vec<IrBlockId>,
2029}
2030
2031#[derive(Debug, Clone, PartialEq, Eq)]
2033pub struct IrConstantPropagationAnalysis {
2034 pub constants: BTreeMap<IrValueId, IrConstant>,
2035}
2036
2037#[derive(Debug, Clone, PartialEq, Eq)]
2039pub struct IrDeadAssignmentAnalysis {
2040 pub instructions: Vec<IrInstructionId>,
2041}
2042
2043pub trait IrOptimizationPass {
2045 fn name(&self) -> &'static str;
2046 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation;
2047}
2048
2049#[derive(Default)]
2051pub struct IrPassManager {
2052 passes: Vec<Box<dyn IrOptimizationPass>>,
2053}
2054
2055impl IrPassManager {
2056 #[must_use]
2057 pub fn new() -> Self {
2058 Self::default()
2059 }
2060
2061 pub fn register(&mut self, pass: Box<dyn IrOptimizationPass>) {
2062 self.passes.push(pass);
2063 }
2064
2065 #[must_use]
2066 pub fn pass_names(&self) -> Vec<&'static str> {
2067 self.passes.iter().map(|pass| pass.name()).collect()
2068 }
2069
2070 #[must_use]
2071 pub fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
2072 self.passes
2073 .iter()
2074 .fold(input.clone(), |current, pass| pass.run(¤t))
2075 }
2076}
2077
2078pub struct IrOptimizationPipeline {
2080 passes: Vec<Box<dyn IrOptimizationPass>>,
2081}
2082
2083impl IrOptimizationPipeline {
2084 #[must_use]
2085 pub fn new(passes: Vec<Box<dyn IrOptimizationPass>>) -> Self {
2086 Self { passes }
2087 }
2088
2089 #[must_use]
2090 pub fn pass_names(&self) -> Vec<&'static str> {
2091 self.passes.iter().map(|pass| pass.name()).collect()
2092 }
2093
2094 #[must_use]
2095 pub fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
2096 self.passes
2097 .iter()
2098 .fold(input.clone(), |current, pass| pass.run(¤t))
2099 }
2100
2101 #[must_use]
2102 pub fn run_with_report(&self, input: &IntermediateRepresentation) -> IrOptimizationReport {
2103 let mut current = input.clone();
2104 let mut passes = Vec::new();
2105 for pass in &self.passes {
2106 let before = optimization_metrics(¤t);
2107 current = pass.run(¤t);
2108 passes.push(IrOptimizationPassReport {
2109 name: pass.name(),
2110 before,
2111 after: optimization_metrics(¤t),
2112 });
2113 }
2114 IrOptimizationReport {
2115 output: current,
2116 passes,
2117 }
2118 }
2119}
2120
2121#[must_use]
2123pub fn computed_optimization_pipeline() -> IrOptimizationPipeline {
2124 IrOptimizationPipeline::new(vec![
2125 Box::new(IrCommonSubexpressionEliminationPass),
2126 Box::new(IrCopyPropagationPass),
2127 Box::new(IrConstantFoldingPass),
2128 Box::new(IrInstructionSimplificationPass),
2129 Box::new(IrDeadCodeEliminationPass),
2130 Box::new(IrCfgCleanupPass),
2131 ])
2132}
2133
2134#[must_use]
2136pub fn optimize_computed_ir(input: &IntermediateRepresentation) -> IrOptimizationReport {
2137 computed_optimization_pipeline().run_with_report(input)
2138}
2139
2140#[must_use]
2143pub fn optimize_effect_ir(input: &IntermediateRepresentation) -> IrOptimizationReport {
2144 let mut effect_input = input.clone();
2145 for module in &mut effect_input.modules {
2146 let effect_functions = module
2147 .effect_executions
2148 .iter()
2149 .flat_map(|execution| {
2150 std::iter::once(execution.function.clone())
2151 .chain(execution.cleanup_function.clone())
2152 })
2153 .collect::<BTreeSet<_>>();
2154 module
2155 .functions
2156 .retain(|function| effect_functions.contains(&function.id));
2157 module.computed_evaluations.clear();
2158 }
2159 let mut report = computed_optimization_pipeline().run_with_report(&effect_input);
2160 let mut output = input.clone();
2161 for (module, optimized) in output.modules.iter_mut().zip(&report.output.modules) {
2162 let optimized_functions = optimized
2163 .functions
2164 .iter()
2165 .map(|function| (function.id.clone(), function.clone()))
2166 .collect::<BTreeMap<_, _>>();
2167 for function in &mut module.functions {
2168 if let Some(optimized) = optimized_functions.get(&function.id) {
2169 *function = optimized.clone();
2170 }
2171 }
2172 }
2173 report.output = output;
2174 report
2175}
2176
2177#[must_use]
2182pub fn optimize_context_ir(input: &IntermediateRepresentation) -> OptimizedContextIrReport {
2183 let context_functions = input
2184 .context_ir
2185 .source_evaluations
2186 .iter()
2187 .map(|evaluation| evaluation.function.as_semantic_id().clone())
2188 .collect::<BTreeSet<_>>();
2189 let mut context_input = input.clone();
2190 for module in &mut context_input.modules {
2191 module
2192 .functions
2193 .retain(|function| context_functions.contains(&function.id));
2194 module.computed_evaluations.clear();
2195 module.effect_executions.clear();
2196 }
2197
2198 let projection = computed_optimization_pipeline().run_with_report(&context_input);
2199 let mut optimized_module = input.clone();
2200 for (module, optimized) in optimized_module
2201 .modules
2202 .iter_mut()
2203 .zip(&projection.output.modules)
2204 {
2205 let optimized_functions = optimized
2206 .functions
2207 .iter()
2208 .map(|function| (function.id.clone(), function.clone()))
2209 .collect::<BTreeMap<_, _>>();
2210 for function in &mut module.functions {
2211 if context_functions.contains(&function.id) {
2212 if let Some(optimized) = optimized_functions.get(&function.id) {
2213 *function = optimized.clone();
2214 }
2215 }
2216 }
2217 }
2218
2219 OptimizedContextIrReport {
2220 source_report: input.context_ir.clone(),
2221 optimized_module,
2222 source_evaluations: input
2223 .context_ir
2224 .source_evaluations
2225 .iter()
2226 .map(OptimizedIrContextSourceEvaluation::from)
2227 .collect(),
2228 pass_metrics: projection.passes,
2229 }
2230}
2231
2232#[derive(Debug, Clone, PartialEq, Eq)]
2234pub struct IrOptimizationMetrics {
2235 pub blocks: usize,
2236 pub instructions: usize,
2237 pub values: usize,
2238}
2239
2240#[derive(Debug, Clone, PartialEq, Eq)]
2242pub struct IrOptimizationPassReport {
2243 pub name: &'static str,
2244 pub before: IrOptimizationMetrics,
2245 pub after: IrOptimizationMetrics,
2246}
2247
2248#[derive(Debug, Clone, PartialEq, Eq)]
2250pub struct IrOptimizationReport {
2251 pub output: IntermediateRepresentation,
2252 pub passes: Vec<IrOptimizationPassReport>,
2253}
2254
2255fn optimization_metrics(representation: &IntermediateRepresentation) -> IrOptimizationMetrics {
2256 IrOptimizationMetrics {
2257 blocks: representation
2258 .modules
2259 .iter()
2260 .flat_map(|module| &module.functions)
2261 .map(|function| function.blocks.len())
2262 .sum(),
2263 instructions: representation
2264 .modules
2265 .iter()
2266 .flat_map(|module| &module.functions)
2267 .flat_map(|function| &function.blocks)
2268 .map(|block| block.instructions.len())
2269 .sum(),
2270 values: representation
2271 .modules
2272 .iter()
2273 .flat_map(|module| &module.functions)
2274 .map(|function| function.values.len())
2275 .sum(),
2276 }
2277}
2278
2279#[must_use]
2281pub fn analyze_dead_assignments(function: &IrFunction) -> IrDeadAssignmentAnalysis {
2282 analyze_dead_assignments_preserving(function, &BTreeSet::new())
2283}
2284
2285fn analyze_dead_assignments_preserving(
2286 function: &IrFunction,
2287 preserved_values: &BTreeSet<IrValueId>,
2288) -> IrDeadAssignmentAnalysis {
2289 let uses = analyze_definition_uses(function).uses;
2290 let instructions = function
2291 .blocks
2292 .iter()
2293 .flat_map(|block| block.instructions.iter())
2294 .filter(|instruction| {
2295 instruction.result.as_ref().is_some_and(|result| {
2296 !preserved_values.contains(result)
2297 && uses.get(result).is_some_and(Vec::is_empty)
2298 && matches!(
2299 instruction.kind,
2300 IrInstructionKind::Constant { .. }
2301 | IrInstructionKind::LoadStorage { .. }
2302 | IrInstructionKind::LoadComputed { .. }
2303 | IrInstructionKind::GetMember { .. }
2304 | IrInstructionKind::Copy { .. }
2305 | IrInstructionKind::Binary { .. }
2306 | IrInstructionKind::Unary { .. }
2307 )
2308 })
2309 })
2310 .map(|instruction| instruction.id.clone())
2311 .collect();
2312 IrDeadAssignmentAnalysis { instructions }
2313}
2314
2315#[must_use]
2317pub fn analyze_constant_propagation(function: &IrFunction) -> IrConstantPropagationAnalysis {
2318 let mut constants = BTreeMap::new();
2319 for block in &function.blocks {
2320 for instruction in &block.instructions {
2321 let Some(result) = &instruction.result else {
2322 continue;
2323 };
2324 let constant = match &instruction.kind {
2325 IrInstructionKind::Constant { value } => Some(value.clone()),
2326 IrInstructionKind::Unary { operation, operand } => {
2327 resolve_constant(operand, &constants).and_then(|operand| {
2328 match (operation, operand) {
2329 (IrUnaryOperation::Not, IrConstant::Boolean(value)) => {
2330 Some(IrConstant::Boolean(!value))
2331 }
2332 (IrUnaryOperation::Identity, value) => Some(value),
2333 (IrUnaryOperation::Negate, IrConstant::Number(value)) => {
2334 negate_number(&value).map(IrConstant::Number)
2335 }
2336 _ => None,
2337 }
2338 })
2339 }
2340 IrInstructionKind::Binary {
2341 operation,
2342 left,
2343 right,
2344 } => {
2345 let (Some(IrConstant::Number(left)), Some(IrConstant::Number(right))) = (
2346 resolve_constant(left, &constants),
2347 resolve_constant(right, &constants),
2348 ) else {
2349 continue;
2350 };
2351 evaluate_numeric_binary(*operation, &left, &right).map(IrConstant::Number)
2352 }
2353 _ => None,
2354 };
2355 if let Some(constant) = constant {
2356 constants.insert(result.clone(), constant);
2357 }
2358 }
2359 }
2360 IrConstantPropagationAnalysis { constants }
2361}
2362
2363fn resolve_constant(
2364 operand: &IrOperand,
2365 constants: &BTreeMap<IrValueId, IrConstant>,
2366) -> Option<IrConstant> {
2367 match operand {
2368 IrOperand::Constant(constant) => Some(constant.clone()),
2369 IrOperand::Value(value) => constants.get(value).cloned(),
2370 IrOperand::Storage(_) => None,
2371 }
2372}
2373
2374fn negate_number(value: &str) -> Option<String> {
2375 value.parse::<f64>().ok().map(|value| format_number(-value))
2376}
2377fn evaluate_numeric_binary(
2378 operation: IrBinaryOperation,
2379 left: &str,
2380 right: &str,
2381) -> Option<String> {
2382 let left = left.parse::<f64>().ok()?;
2383 let right = right.parse::<f64>().ok()?;
2384 let value = match operation {
2385 IrBinaryOperation::Add => left + right,
2386 IrBinaryOperation::Subtract => left - right,
2387 IrBinaryOperation::Multiply => left * right,
2388 IrBinaryOperation::Divide if right != 0.0 => left / right,
2389 IrBinaryOperation::Remainder if right != 0.0 => left % right,
2390 IrBinaryOperation::Min => left.min(right),
2391 IrBinaryOperation::Max => left.max(right),
2392 _ => return None,
2393 };
2394 Some(format_number(value))
2395}
2396fn format_number(value: f64) -> String {
2397 if value.fract() == 0.0 {
2398 format!("{value:.0}")
2399 } else {
2400 value.to_string()
2401 }
2402}
2403
2404#[must_use]
2406pub fn analyze_reachability(function: &IrFunction) -> IrReachabilityAnalysis {
2407 let mut reachable = BTreeSet::from([function.entry_block.clone()]);
2408 let mut pending = vec![function.entry_block.clone()];
2409 while let Some(block) = pending.pop() {
2410 for successor in function.successor_blocks(&block) {
2411 if reachable.insert(successor.clone()) {
2412 pending.push(successor);
2413 }
2414 }
2415 }
2416 let all_blocks = function
2417 .blocks
2418 .iter()
2419 .map(|block| block.id.clone())
2420 .collect::<BTreeSet<_>>();
2421 IrReachabilityAnalysis {
2422 reachable: reachable
2423 .iter()
2424 .filter(|block| all_blocks.contains(*block))
2425 .cloned()
2426 .collect(),
2427 unreachable: all_blocks.difference(&reachable).cloned().collect(),
2428 }
2429}
2430
2431#[must_use]
2433pub fn analyze_liveness(function: &IrFunction) -> IrLivenessAnalysis {
2434 let block_ids = function
2435 .blocks
2436 .iter()
2437 .map(|block| block.id.clone())
2438 .collect::<BTreeSet<_>>();
2439 let mut block_uses = BTreeMap::new();
2440 let mut block_definitions = BTreeMap::new();
2441 for block in &function.blocks {
2442 let mut uses = BTreeSet::new();
2443 let mut definitions = BTreeSet::new();
2444 for instruction in &block.instructions {
2445 for operand in ir_instruction_operands(&instruction.kind) {
2446 if let IrOperand::Value(value) = operand {
2447 if !definitions.contains(&value) {
2448 uses.insert(value);
2449 }
2450 }
2451 }
2452 if let Some(result) = &instruction.result {
2453 definitions.insert(result.clone());
2454 }
2455 }
2456 block_uses.insert(block.id.clone(), uses);
2457 block_definitions.insert(block.id.clone(), definitions);
2458 }
2459 let mut live_in = block_ids
2460 .iter()
2461 .cloned()
2462 .map(|id| (id, BTreeSet::new()))
2463 .collect::<BTreeMap<_, _>>();
2464 let mut live_out = live_in.clone();
2465 let mut changed = true;
2466 while changed {
2467 changed = false;
2468 for block in block_ids.iter().rev() {
2469 let next_out = function
2470 .successor_blocks(block)
2471 .into_iter()
2472 .flat_map(|successor| live_in[&successor].clone())
2473 .collect::<BTreeSet<_>>();
2474 let mut next_in = block_uses[block].clone();
2475 next_in.extend(next_out.difference(&block_definitions[block]).cloned());
2476 if live_out.get(block) != Some(&next_out) || live_in.get(block) != Some(&next_in) {
2477 live_out.insert(block.clone(), next_out);
2478 live_in.insert(block.clone(), next_in);
2479 changed = true;
2480 }
2481 }
2482 }
2483 IrLivenessAnalysis {
2484 live_in: live_in
2485 .into_iter()
2486 .map(|(block, values)| (block, values.into_iter().collect()))
2487 .collect(),
2488 live_out: live_out
2489 .into_iter()
2490 .map(|(block, values)| (block, values.into_iter().collect()))
2491 .collect(),
2492 }
2493}
2494
2495#[must_use]
2497pub fn analyze_use_definitions(function: &IrFunction) -> Vec<IrUseDefinition> {
2498 let mut relations = Vec::new();
2499 for block in &function.blocks {
2500 for instruction in &block.instructions {
2501 for (operand_index, operand) in ir_instruction_operands(&instruction.kind)
2502 .into_iter()
2503 .enumerate()
2504 {
2505 let IrOperand::Value(value) = operand else {
2506 continue;
2507 };
2508 let Some(definition) = function
2509 .values
2510 .get(&value)
2511 .map(|value| value.definition.clone())
2512 else {
2513 continue;
2514 };
2515 relations.push(IrUseDefinition {
2516 value,
2517 instruction: instruction.id.clone(),
2518 operand_index,
2519 definition,
2520 });
2521 }
2522 }
2523 }
2524 relations
2525}
2526
2527#[must_use]
2529pub fn analyze_definition_uses(function: &IrFunction) -> IrDefinitionUseAnalysis {
2530 let definitions = function
2531 .values
2532 .iter()
2533 .map(|(id, value)| (id.clone(), value.definition.clone()))
2534 .collect::<BTreeMap<_, _>>();
2535 let mut uses = function
2536 .values
2537 .keys()
2538 .cloned()
2539 .map(|id| (id, Vec::new()))
2540 .collect::<BTreeMap<_, _>>();
2541 for block in &function.blocks {
2542 for instruction in &block.instructions {
2543 for (operand_index, operand) in ir_instruction_operands(&instruction.kind)
2544 .into_iter()
2545 .enumerate()
2546 {
2547 if let IrOperand::Value(value) = operand {
2548 uses.entry(value).or_default().push(IrUse {
2549 instruction: instruction.id.clone(),
2550 operand_index,
2551 });
2552 }
2553 }
2554 }
2555 }
2556 IrDefinitionUseAnalysis { definitions, uses }
2557}
2558
2559#[must_use]
2561pub fn validate_intermediate_representation(
2562 representation: &IntermediateRepresentation,
2563) -> Vec<IrValidationDiagnostic> {
2564 let storage_ids = representation
2565 .modules
2566 .iter()
2567 .flat_map(|module| module.storages.iter().map(|storage| storage.id.clone()))
2568 .collect::<BTreeSet<_>>();
2569 let computed_evaluation_ids = representation
2570 .modules
2571 .iter()
2572 .flat_map(|module| {
2573 module
2574 .computed_evaluations
2575 .iter()
2576 .map(|evaluation| evaluation.computed.clone())
2577 })
2578 .collect::<BTreeSet<_>>();
2579 let mut diagnostics = Vec::new();
2580 let mut instruction_ids = BTreeSet::new();
2581 let mut effect_ids = BTreeSet::new();
2582
2583 for module in &representation.modules {
2584 for instruction in &module.storage_initializers {
2585 validate_instruction(
2586 instruction,
2587 None,
2588 &BTreeSet::new(),
2589 &storage_ids,
2590 &computed_evaluation_ids,
2591 &mut instruction_ids,
2592 &mut diagnostics,
2593 );
2594 }
2595 for function in &module.functions {
2596 validate_function(
2597 function,
2598 &storage_ids,
2599 &computed_evaluation_ids,
2600 &mut instruction_ids,
2601 &mut diagnostics,
2602 );
2603 }
2604 validate_computed_evaluations(module, &mut diagnostics);
2605 validate_effect_executions(module, &mut effect_ids, &mut diagnostics);
2606 }
2607 diagnostics
2608}
2609
2610#[allow(clippy::too_many_lines)]
2613#[must_use]
2614pub fn validate_effect_ir(
2615 model: &ApplicationSemanticModel,
2616 representation: &IntermediateRepresentation,
2617) -> Vec<IrValidationDiagnostic> {
2618 let mut diagnostics = validate_intermediate_representation(representation);
2619 let lowered = representation
2620 .modules
2621 .iter()
2622 .flat_map(|module| &module.effect_executions)
2623 .map(|execution| execution.effect.clone())
2624 .collect::<BTreeSet<_>>();
2625 for execution in representation.modules.iter().flat_map(|module| {
2626 module
2627 .effect_executions
2628 .iter()
2629 .map(move |execution| (module, execution))
2630 }) {
2631 let (module, execution) = execution;
2632 let Some(effect) = model.effects.get(&execution.effect) else {
2633 diagnostics.push(IrValidationDiagnostic {
2634 code: "PSIR1020",
2635 message: format!(
2636 "effect execution references missing effect {}",
2637 execution.effect
2638 ),
2639 });
2640 continue;
2641 };
2642 if effect.validation != EffectValidation::Valid || !effect_is_scheduled(model, &effect.id) {
2643 diagnostics.push(IrValidationDiagnostic {
2644 code: "PSIR1021",
2645 message: format!(
2646 "effect execution {} is invalid or F9-unplanned",
2647 execution.effect
2648 ),
2649 });
2650 }
2651 let Some(body) = model.effect_body(&effect.id) else {
2652 diagnostics.push(IrValidationDiagnostic {
2653 code: "PSIR1022",
2654 message: format!(
2655 "effect execution {} has no canonical body",
2656 execution.effect
2657 ),
2658 });
2659 continue;
2660 };
2661 let Some(function) = module
2662 .functions
2663 .iter()
2664 .find(|function| function.id == execution.function)
2665 else {
2666 continue;
2667 };
2668 let statement_positions = body
2669 .statements
2670 .iter()
2671 .enumerate()
2672 .map(|(index, statement)| (statement, index))
2673 .collect::<BTreeMap<_, _>>();
2674 let mut previous_statement = None;
2675 for instruction in function.blocks.iter().flat_map(|block| &block.instructions) {
2676 let (operation, expected_kind) = match &instruction.kind {
2677 IrInstructionKind::CapabilityCall { operation, .. } => {
2678 (*operation, CapabilityOperationKind::MethodCall)
2679 }
2680 IrInstructionKind::CapabilityAssign { operation, .. } => {
2681 (*operation, CapabilityOperationKind::MemberAssignment)
2682 }
2683 _ => continue,
2684 };
2685 let Some(statement) = instruction.semantic_origin.as_ref() else {
2686 diagnostics.push(IrValidationDiagnostic {
2687 code: "PSIR1023",
2688 message: format!(
2689 "capability instruction {} lacks an effect statement origin",
2690 instruction.id
2691 ),
2692 });
2693 continue;
2694 };
2695 let Some(record) = model.effect_statement_type(statement) else {
2696 diagnostics.push(IrValidationDiagnostic {
2697 code: "PSIR1024",
2698 message: format!(
2699 "capability instruction {} has unknown statement {statement}",
2700 instruction.id
2701 ),
2702 });
2703 continue;
2704 };
2705 let registry_operation = EFFECT_CAPABILITY_REGISTRY.operation(operation);
2706 if record.capability_operation != Some(operation)
2707 || registry_operation.is_none_or(|candidate| candidate.kind != expected_kind)
2708 || record.signature_compatibility != EffectCompatibility::Compatible
2709 || record.boundary_compatibility != EffectCompatibility::Compatible
2710 || record.serialization_compatibility != EffectCompatibility::Compatible
2711 || instruction.result.is_some()
2712 || instruction.provenance != record.provenance
2713 {
2714 diagnostics.push(IrValidationDiagnostic {
2715 code: "PSIR1025",
2716 message: format!(
2717 "capability instruction {} conflicts with canonical F4 facts",
2718 instruction.id
2719 ),
2720 });
2721 }
2722 let position = statement_positions.get(statement).copied();
2723 if position.is_none() || previous_statement.is_some_and(|prior| position <= Some(prior))
2724 {
2725 diagnostics.push(IrValidationDiagnostic {
2726 code: "PSIR1026",
2727 message: format!(
2728 "effect instruction {} does not preserve statement order",
2729 instruction.id
2730 ),
2731 });
2732 }
2733 previous_statement = position;
2734 }
2735 }
2736 for effect in model.effects.values() {
2737 if (effect.validation != EffectValidation::Valid || !effect_is_scheduled(model, &effect.id))
2738 && lowered.contains(&effect.id)
2739 {
2740 diagnostics.push(IrValidationDiagnostic {
2741 code: "PSIR1027",
2742 message: format!("invalid or unplanned effect {} has IR", effect.id),
2743 });
2744 }
2745 }
2746 diagnostics
2747}
2748
2749#[allow(clippy::too_many_lines)]
2753#[must_use]
2754pub fn validate_context_ir(
2755 model: &ApplicationSemanticModel,
2756 representation: &IntermediateRepresentation,
2757) -> Vec<IrValidationDiagnostic> {
2758 let mut diagnostics = validate_intermediate_representation(representation);
2759 let planned = model
2760 .context_evaluation
2761 .source_entries
2762 .iter()
2763 .filter(|(_, entry)| entry.status == ContextSourcePlanStatus::Planned)
2764 .collect::<BTreeMap<_, _>>();
2765 let batches = model
2766 .context_evaluation
2767 .evaluation_batches
2768 .iter()
2769 .flat_map(|batch| {
2770 batch
2771 .sources
2772 .iter()
2773 .cloned()
2774 .map(move |source| (source, batch.id.clone()))
2775 })
2776 .collect::<BTreeMap<_, _>>();
2777 let mut source_records = BTreeMap::new();
2778 let mut slots = BTreeSet::new();
2779 let mut functions = BTreeSet::new();
2780
2781 for evaluation in &representation.context_ir.source_evaluations {
2782 let Some(entry) = planned.get(&evaluation.source) else {
2783 diagnostics.push(IrValidationDiagnostic {
2784 code: "PSIR1030",
2785 message: format!("Context source {:?} is not G9-planned", evaluation.source),
2786 });
2787 continue;
2788 };
2789 if source_records
2790 .insert(evaluation.source.clone(), evaluation)
2791 .is_some()
2792 || !slots.insert(evaluation.slot.clone())
2793 || !functions.insert(evaluation.function.clone())
2794 {
2795 diagnostics.push(IrValidationDiagnostic {
2796 code: "PSIR1031",
2797 message: format!(
2798 "Context source {:?} has non-unique IR identities",
2799 evaluation.source
2800 ),
2801 });
2802 }
2803 if evaluation.context != entry.context
2804 || evaluation.function != ContextSourceFunctionId::for_source(&evaluation.source)
2805 || evaluation.slot != ContextValueSlotId::for_source(&evaluation.source)
2806 || evaluation.prerequisite_computed_batches != entry.prerequisite_computed_batches
2807 || batches.get(&evaluation.source) != Some(&evaluation.evaluation_batch)
2808 {
2809 diagnostics.push(IrValidationDiagnostic {
2810 code: "PSIR1032",
2811 message: format!(
2812 "Context source {:?} conflicts with retained G9 facts",
2813 evaluation.source
2814 ),
2815 });
2816 }
2817 let function = representation
2818 .modules
2819 .iter()
2820 .flat_map(|module| &module.functions)
2821 .find(|function| function.id == *evaluation.function.as_semantic_id());
2822 let Some(function) = function else {
2823 diagnostics.push(IrValidationDiagnostic {
2824 code: "PSIR1033",
2825 message: format!(
2826 "Context source {:?} is missing its IR function",
2827 evaluation.source
2828 ),
2829 });
2830 continue;
2831 };
2832 let initializes_slot = function
2833 .blocks
2834 .iter()
2835 .flat_map(|block| &block.instructions)
2836 .any(|instruction| {
2837 matches!(
2838 &instruction.kind,
2839 IrInstructionKind::InitializeContextSlot { slot, value }
2840 if slot == &evaluation.slot && value == &evaluation.result
2841 )
2842 });
2843 if function.entry_block != evaluation.entry_block
2844 || !function.values.contains_key(&evaluation.result)
2845 || !initializes_slot
2846 {
2847 diagnostics.push(IrValidationDiagnostic {
2848 code: "PSIR1034",
2849 message: format!(
2850 "Context source {:?} lacks its exact result or slot initialization",
2851 evaluation.source
2852 ),
2853 });
2854 }
2855 let actual_state = function
2856 .blocks
2857 .iter()
2858 .flat_map(|block| &block.instructions)
2859 .filter_map(|instruction| match &instruction.kind {
2860 IrInstructionKind::LoadStorage { storage } => Some(storage.clone()),
2861 _ => None,
2862 })
2863 .collect::<BTreeSet<_>>();
2864 let expected_state = entry
2865 .required_state
2866 .iter()
2867 .map(IrStorageId::for_semantic_origin)
2868 .collect::<BTreeSet<_>>();
2869 let actual_computed = function
2870 .blocks
2871 .iter()
2872 .flat_map(|block| &block.instructions)
2873 .filter_map(|instruction| match &instruction.kind {
2874 IrInstructionKind::LoadComputed { computed } => Some(computed.clone()),
2875 _ => None,
2876 })
2877 .collect::<BTreeSet<_>>();
2878 let expected_computed = entry
2879 .required_computed
2880 .iter()
2881 .cloned()
2882 .collect::<BTreeSet<_>>();
2883 if !expected_state.is_subset(&actual_state)
2884 || !expected_computed.is_subset(&actual_computed)
2885 {
2886 diagnostics.push(IrValidationDiagnostic {
2887 code: "PSIR1035",
2888 message: format!(
2889 "Context source {:?} has incomplete canonical dependencies",
2890 evaluation.source
2891 ),
2892 });
2893 }
2894 }
2895 for source in planned.keys() {
2896 if !source_records.contains_key(*source) {
2897 diagnostics.push(IrValidationDiagnostic {
2898 code: "PSIR1036",
2899 message: format!("G9-planned Context source {source:?} has no IR"),
2900 });
2901 }
2902 }
2903
2904 let available = model
2905 .context_evaluation
2906 .consumer_entries
2907 .iter()
2908 .filter(|(_, entry)| entry.status == ContextConsumerAvailabilityStatus::Available)
2909 .collect::<BTreeMap<_, _>>();
2910 let mut consumer_records = BTreeSet::new();
2911 for binding in &representation.context_ir.consumer_bindings {
2912 let Some(entry) = available.get(&binding.consumer) else {
2913 diagnostics.push(IrValidationDiagnostic {
2914 code: "PSIR1037",
2915 message: format!(
2916 "unavailable Context Consumer {} has executable IR",
2917 binding.consumer
2918 ),
2919 });
2920 continue;
2921 };
2922 if !consumer_records.insert(binding.consumer.clone()) {
2923 diagnostics.push(IrValidationDiagnostic {
2924 code: "PSIR1038",
2925 message: format!(
2926 "Context Consumer {} has duplicate load IR",
2927 binding.consumer
2928 ),
2929 });
2930 }
2931 let Some(consumer) = model.consumers.get(&binding.consumer) else {
2932 diagnostics.push(IrValidationDiagnostic {
2933 code: "PSIR1039",
2934 message: format!("Context Consumer {} is not canonical", binding.consumer),
2935 });
2936 continue;
2937 };
2938 let Some(source) = entry.selected_source.as_ref() else {
2939 diagnostics.push(IrValidationDiagnostic {
2940 code: "PSIR1040",
2941 message: format!(
2942 "available Context Consumer {} has no selected source",
2943 binding.consumer
2944 ),
2945 });
2946 continue;
2947 };
2948 let source_slot = source_records
2949 .get(source)
2950 .map(|evaluation| &evaluation.slot);
2951 if consumer.context() != Some(&binding.context)
2952 || binding.source != *source
2953 || source_slot != Some(&binding.slot)
2954 || binding.load.id != ContextConsumerLoadId::for_consumer(&binding.consumer)
2955 || binding.load.slot != binding.slot
2956 || binding.load.result != IrValueId::for_function(binding.load.id.as_semantic_id(), 0)
2957 || binding.semantic_type != consumer.requested_type_id
2958 {
2959 diagnostics.push(IrValidationDiagnostic {
2960 code: "PSIR1041",
2961 message: format!(
2962 "Context Consumer {} conflicts with G4/G9 IR facts",
2963 binding.consumer
2964 ),
2965 });
2966 }
2967 }
2968 for consumer in available.keys() {
2969 if !consumer_records.contains(*consumer) {
2970 diagnostics.push(IrValidationDiagnostic {
2971 code: "PSIR1042",
2972 message: format!("available Context Consumer {consumer} has no load IR"),
2973 });
2974 }
2975 }
2976 diagnostics
2977}
2978
2979#[allow(clippy::too_many_lines)]
2983#[must_use]
2984pub fn validate_optimized_context_ir(
2985 model: &ApplicationSemanticModel,
2986 input: &IntermediateRepresentation,
2987 report: &OptimizedContextIrReport,
2988) -> Vec<IrValidationDiagnostic> {
2989 let mut diagnostics = validate_context_ir(model, &report.optimized_module);
2990 if report.source_report != input.context_ir
2991 || report.optimized_module.context_ir != report.source_report
2992 {
2993 diagnostics.push(IrValidationDiagnostic {
2994 code: "PSIR1043",
2995 message: "optimized Context IR does not retain its exact G10 source report".to_string(),
2996 });
2997 }
2998
2999 let expected_evaluations = input
3000 .context_ir
3001 .source_evaluations
3002 .iter()
3003 .map(OptimizedIrContextSourceEvaluation::from)
3004 .collect::<Vec<_>>();
3005 if report.source_evaluations != expected_evaluations {
3006 diagnostics.push(IrValidationDiagnostic {
3007 code: "PSIR1044",
3008 message: "optimized Context source evaluations changed frozen identities or order"
3009 .to_string(),
3010 });
3011 }
3012 if report.optimized_module.context_ir.consumer_bindings != input.context_ir.consumer_bindings {
3013 diagnostics.push(IrValidationDiagnostic {
3014 code: "PSIR1045",
3015 message: "optimized Context IR changed compiler-selected Consumer slot bindings"
3016 .to_string(),
3017 });
3018 }
3019
3020 let source_functions = input
3021 .context_ir
3022 .source_evaluations
3023 .iter()
3024 .map(|evaluation| evaluation.function.as_semantic_id().clone())
3025 .collect::<BTreeSet<_>>();
3026 for evaluation in &report.source_evaluations {
3027 let functions = report
3028 .optimized_module
3029 .modules
3030 .iter()
3031 .flat_map(|module| &module.functions)
3032 .filter(|function| function.id == *evaluation.function.as_semantic_id())
3033 .collect::<Vec<_>>();
3034 if functions.len() != 1 {
3035 diagnostics.push(IrValidationDiagnostic {
3036 code: "PSIR1046",
3037 message: format!(
3038 "optimized Context source {:?} has {} IR functions instead of one",
3039 evaluation.source,
3040 functions.len()
3041 ),
3042 });
3043 continue;
3044 }
3045 let function = functions[0];
3046 let initializations = report
3047 .optimized_module
3048 .modules
3049 .iter()
3050 .flat_map(|module| &module.functions)
3051 .flat_map(|function| &function.blocks)
3052 .flat_map(|block| &block.instructions)
3053 .filter(|instruction| {
3054 matches!(
3055 &instruction.kind,
3056 IrInstructionKind::InitializeContextSlot { slot, .. } if slot == &evaluation.slot
3057 )
3058 })
3059 .collect::<Vec<_>>();
3060 let exact_initializations = function
3061 .blocks
3062 .iter()
3063 .flat_map(|block| &block.instructions)
3064 .filter(|instruction| {
3065 matches!(
3066 &instruction.kind,
3067 IrInstructionKind::InitializeContextSlot { slot, value }
3068 if slot == &evaluation.slot && value == &evaluation.result
3069 )
3070 })
3071 .count();
3072 if initializations.len() != 1
3073 || exact_initializations != 1
3074 || function.entry_block != evaluation.entry_block
3075 || !function.values.contains_key(&evaluation.result)
3076 {
3077 diagnostics.push(IrValidationDiagnostic {
3078 code: "PSIR1047",
3079 message: format!(
3080 "optimized Context source {:?} does not retain one exact observable slot initialization",
3081 evaluation.source
3082 ),
3083 });
3084 }
3085 }
3086
3087 let optimized_modules = report
3088 .optimized_module
3089 .modules
3090 .iter()
3091 .map(|module| (&module.path, module))
3092 .collect::<BTreeMap<_, _>>();
3093 for module in &input.modules {
3094 let Some(optimized) = optimized_modules.get(&module.path) else {
3095 diagnostics.push(IrValidationDiagnostic {
3096 code: "PSIR1048",
3097 message: format!(
3098 "optimized Context IR is missing module {}",
3099 module.path.display()
3100 ),
3101 });
3102 continue;
3103 };
3104 if module.components != optimized.components
3105 || module.storages != optimized.storages
3106 || module.storage_initializers != optimized.storage_initializers
3107 || module.template_entrypoints != optimized.template_entrypoints
3108 || module.computed_evaluations != optimized.computed_evaluations
3109 || module.effect_executions != optimized.effect_executions
3110 || module.functions.len() != optimized.functions.len()
3111 {
3112 diagnostics.push(IrValidationDiagnostic {
3113 code: "PSIR1049",
3114 message: format!(
3115 "optimized Context IR changed a non-Context module product in {}",
3116 module.path.display()
3117 ),
3118 });
3119 }
3120 for function in &module.functions {
3121 if source_functions.contains(&function.id) {
3122 continue;
3123 }
3124 if optimized
3125 .functions
3126 .iter()
3127 .find(|candidate| candidate.id == function.id)
3128 != Some(function)
3129 {
3130 diagnostics.push(IrValidationDiagnostic {
3131 code: "PSIR1050",
3132 message: format!(
3133 "optimized Context IR changed unrelated function {}",
3134 function.id
3135 ),
3136 });
3137 }
3138 }
3139 }
3140 diagnostics
3141}
3142
3143fn validate_function(
3144 function: &IrFunction,
3145 storage_ids: &BTreeSet<IrStorageId>,
3146 computed_evaluation_ids: &BTreeSet<SemanticId>,
3147 instruction_ids: &mut BTreeSet<IrInstructionId>,
3148 diagnostics: &mut Vec<IrValidationDiagnostic>,
3149) {
3150 let function_instruction_ids = function
3151 .blocks
3152 .iter()
3153 .flat_map(|block| {
3154 block
3155 .instructions
3156 .iter()
3157 .map(|instruction| instruction.id.clone())
3158 })
3159 .collect::<BTreeSet<_>>();
3160 for block in &function.blocks {
3161 for instruction in &block.instructions {
3162 validate_instruction(
3163 instruction,
3164 Some(function),
3165 &function_instruction_ids,
3166 storage_ids,
3167 computed_evaluation_ids,
3168 instruction_ids,
3169 diagnostics,
3170 );
3171 }
3172 }
3173 for (id, value) in &function.values {
3174 validate_value(function, id, value, &function_instruction_ids, diagnostics);
3175 }
3176}
3177
3178fn validate_value(
3179 function: &IrFunction,
3180 id: &IrValueId,
3181 value: &IrValue,
3182 instruction_ids: &BTreeSet<IrInstructionId>,
3183 diagnostics: &mut Vec<IrValidationDiagnostic>,
3184) {
3185 if id != &value.id {
3186 diagnostics.push(IrValidationDiagnostic {
3187 code: "PSIR1001",
3188 message: format!(
3189 "value registry key {id} does not match value ID {}",
3190 value.id
3191 ),
3192 });
3193 }
3194 match &value.definition {
3195 IrValueDefinition::Instruction(instruction) if !instruction_ids.contains(instruction) => {
3196 diagnostics.push(IrValidationDiagnostic {
3197 code: "PSIR1002",
3198 message: format!(
3199 "value {id} references missing defining instruction {instruction}"
3200 ),
3201 });
3202 }
3203 IrValueDefinition::Parameter {
3204 function: owner, ..
3205 } if owner != &function.id => {
3206 diagnostics.push(IrValidationDiagnostic {
3207 code: "PSIR1003",
3208 message: format!(
3209 "value {id} belongs to parameter function {owner}, not {}",
3210 function.id
3211 ),
3212 });
3213 }
3214 IrValueDefinition::BlockParameter { block, .. } if function.block(block).is_none() => {
3215 diagnostics.push(IrValidationDiagnostic {
3216 code: "PSIR1004",
3217 message: format!("value {id} references missing block parameter owner {block}"),
3218 });
3219 }
3220 _ => {}
3221 }
3222}
3223
3224fn validate_computed_evaluations(module: &IrModule, diagnostics: &mut Vec<IrValidationDiagnostic>) {
3225 for evaluation in &module.computed_evaluations {
3226 if evaluation.computed != evaluation.function {
3227 diagnostics.push(IrValidationDiagnostic {
3228 code: "PSIR1010",
3229 message: format!(
3230 "computed evaluation {} must use its computed ID as function {}",
3231 evaluation.computed, evaluation.function
3232 ),
3233 });
3234 continue;
3235 }
3236 let Some(function) = module
3237 .functions
3238 .iter()
3239 .find(|function| function.id == evaluation.function)
3240 else {
3241 diagnostics.push(IrValidationDiagnostic {
3242 code: "PSIR1010",
3243 message: format!(
3244 "computed evaluation {} references missing function {}",
3245 evaluation.computed, evaluation.function
3246 ),
3247 });
3248 continue;
3249 };
3250 if !function.values.contains_key(&evaluation.result) {
3251 diagnostics.push(IrValidationDiagnostic {
3252 code: "PSIR1011",
3253 message: format!(
3254 "computed evaluation {} references missing result {}",
3255 evaluation.computed, evaluation.result
3256 ),
3257 });
3258 }
3259 }
3260}
3261
3262fn validate_effect_executions(
3263 module: &IrModule,
3264 effect_ids: &mut BTreeSet<SemanticId>,
3265 diagnostics: &mut Vec<IrValidationDiagnostic>,
3266) {
3267 for execution in &module.effect_executions {
3268 if !effect_ids.insert(execution.effect.clone()) || execution.effect != execution.function {
3269 diagnostics.push(IrValidationDiagnostic {
3270 code: "PSIR1013",
3271 message: format!(
3272 "effect execution {} has a non-unique function identity",
3273 execution.effect
3274 ),
3275 });
3276 continue;
3277 }
3278 let Some(function) = module
3279 .functions
3280 .iter()
3281 .find(|function| function.id == execution.function)
3282 else {
3283 diagnostics.push(IrValidationDiagnostic {
3284 code: "PSIR1014",
3285 message: format!(
3286 "effect execution {} references a missing function",
3287 execution.effect
3288 ),
3289 });
3290 continue;
3291 };
3292 if function.entry_block != execution.entry_block {
3293 diagnostics.push(IrValidationDiagnostic {
3294 code: "PSIR1015",
3295 message: format!(
3296 "effect execution {} has an inconsistent entry block",
3297 execution.effect
3298 ),
3299 });
3300 }
3301 let operations = function
3302 .blocks
3303 .iter()
3304 .flat_map(|block| &block.instructions)
3305 .filter_map(|instruction| match instruction.kind {
3306 IrInstructionKind::CapabilityCall { operation, .. }
3307 | IrInstructionKind::CapabilityAssign { operation, .. } => Some(operation),
3308 _ => None,
3309 })
3310 .collect::<Vec<_>>();
3311 if operations != execution.capability_operations {
3312 diagnostics.push(IrValidationDiagnostic {
3313 code: "PSIR1016",
3314 message: format!(
3315 "effect execution {} has inconsistent capability operations",
3316 execution.effect
3317 ),
3318 });
3319 }
3320 match (&execution.cleanup_function, &execution.cleanup_entry_block) {
3321 (Some(cleanup_id), Some(cleanup_entry)) if cleanup_id != &execution.effect => {
3322 let Some(cleanup) = module
3323 .functions
3324 .iter()
3325 .find(|function| function.id == *cleanup_id)
3326 else {
3327 diagnostics.push(IrValidationDiagnostic {
3328 code: "PSIR1023",
3329 message: format!(
3330 "effect execution {} references a missing cleanup function",
3331 execution.effect
3332 ),
3333 });
3334 continue;
3335 };
3336 if cleanup.entry_block != *cleanup_entry {
3337 diagnostics.push(IrValidationDiagnostic {
3338 code: "PSIR1024",
3339 message: format!(
3340 "effect execution {} has an inconsistent cleanup entry block",
3341 execution.effect
3342 ),
3343 });
3344 }
3345 let operations = cleanup
3346 .blocks
3347 .iter()
3348 .flat_map(|block| &block.instructions)
3349 .filter_map(|instruction| match instruction.kind {
3350 IrInstructionKind::CapabilityCall { operation, .. }
3351 | IrInstructionKind::CapabilityAssign { operation, .. } => Some(operation),
3352 _ => None,
3353 })
3354 .collect::<Vec<_>>();
3355 if operations != execution.cleanup_capability_operations {
3356 diagnostics.push(IrValidationDiagnostic {
3357 code: "PSIR1025",
3358 message: format!(
3359 "effect execution {} has inconsistent cleanup capability operations",
3360 execution.effect
3361 ),
3362 });
3363 }
3364 }
3365 (None, None) if execution.cleanup_capability_operations.is_empty() => {}
3366 _ => diagnostics.push(IrValidationDiagnostic {
3367 code: "PSIR1026",
3368 message: format!(
3369 "effect execution {} has malformed cleanup program metadata",
3370 execution.effect
3371 ),
3372 }),
3373 }
3374 }
3375}
3376
3377fn validate_instruction(
3378 instruction: &IrInstruction,
3379 function: Option<&IrFunction>,
3380 function_instruction_ids: &BTreeSet<IrInstructionId>,
3381 storage_ids: &BTreeSet<IrStorageId>,
3382 computed_evaluation_ids: &BTreeSet<SemanticId>,
3383 instruction_ids: &mut BTreeSet<IrInstructionId>,
3384 diagnostics: &mut Vec<IrValidationDiagnostic>,
3385) {
3386 if !instruction_ids.insert(instruction.id.clone()) {
3387 diagnostics.push(IrValidationDiagnostic {
3388 code: "PSIR1005",
3389 message: format!("duplicate instruction ID {}", instruction.id),
3390 });
3391 }
3392 if let (Some(function), Some(result)) = (function, &instruction.result) {
3393 match function.values.get(result) {
3394 Some(value)
3395 if value.definition == IrValueDefinition::Instruction(instruction.id.clone()) => {}
3396 _ => diagnostics.push(IrValidationDiagnostic {
3397 code: "PSIR1006",
3398 message: format!(
3399 "instruction {} result {result} lacks a matching value definition",
3400 instruction.id
3401 ),
3402 }),
3403 }
3404 }
3405 for operand in ir_instruction_operands(&instruction.kind) {
3406 if let IrOperand::Value(value) = operand {
3407 if function.is_none_or(|function| !function.values.contains_key(&value)) {
3408 diagnostics.push(IrValidationDiagnostic {
3409 code: "PSIR1007",
3410 message: format!(
3411 "instruction {} references unknown value {value}",
3412 instruction.id
3413 ),
3414 });
3415 }
3416 }
3417 }
3418 for storage in instruction_storages(&instruction.kind) {
3419 if !storage_ids.contains(storage) {
3420 diagnostics.push(IrValidationDiagnostic {
3421 code: "PSIR1008",
3422 message: format!(
3423 "instruction {} references unknown storage {storage}",
3424 instruction.id
3425 ),
3426 });
3427 }
3428 }
3429 if let IrInstructionKind::LoadComputed { computed } = &instruction.kind {
3430 if !computed_evaluation_ids.contains(computed) {
3431 diagnostics.push(IrValidationDiagnostic {
3432 code: "PSIR1012",
3433 message: format!(
3434 "instruction {} references unknown computed evaluation {computed}",
3435 instruction.id
3436 ),
3437 });
3438 }
3439 }
3440 if let IrInstructionKind::CapabilityCall { operation, .. }
3441 | IrInstructionKind::CapabilityAssign { operation, .. } = &instruction.kind
3442 {
3443 if EFFECT_CAPABILITY_REGISTRY.operation(*operation).is_none() {
3444 diagnostics.push(IrValidationDiagnostic {
3445 code: "PSIR1017",
3446 message: format!(
3447 "instruction {} references unknown capability operation {}",
3448 instruction.id, operation.0
3449 ),
3450 });
3451 }
3452 }
3453 if let Some(result) = &instruction.result {
3454 if !function_instruction_ids.contains(&instruction.id) {
3455 diagnostics.push(IrValidationDiagnostic {
3456 code: "PSIR1009",
3457 message: format!(
3458 "module instruction {} must not produce value {result}",
3459 instruction.id
3460 ),
3461 });
3462 }
3463 }
3464}
3465
3466#[must_use]
3471pub fn ir_instruction_operands(kind: &IrInstructionKind) -> Vec<IrOperand> {
3472 match kind {
3473 IrInstructionKind::StoreStorage { value, .. }
3474 | IrInstructionKind::Unary { operand: value, .. }
3475 | IrInstructionKind::Copy { source: value }
3476 | IrInstructionKind::GetMember { object: value, .. } => vec![value.clone()],
3477 IrInstructionKind::GetIndex { object, index } => vec![object.clone(), index.clone()],
3478 IrInstructionKind::Select {
3479 condition,
3480 when_true,
3481 when_false,
3482 } => vec![condition.clone(), when_true.clone(), when_false.clone()],
3483 IrInstructionKind::Binary { left, right, .. } => vec![left.clone(), right.clone()],
3484 IrInstructionKind::CapabilityCall { arguments, .. } => {
3485 arguments.iter().cloned().map(IrOperand::Value).collect()
3486 }
3487 IrInstructionKind::PurePackageCall { arguments, .. } => {
3488 arguments.iter().cloned().map(IrOperand::Value).collect()
3489 }
3490 IrInstructionKind::Template { expressions, .. } => {
3491 expressions.iter().cloned().map(IrOperand::Value).collect()
3492 }
3493 IrInstructionKind::CapabilityAssign { value, .. } => vec![IrOperand::Value(value.clone())],
3494 IrInstructionKind::InitializeContextSlot { value, .. } => {
3495 vec![IrOperand::Value(value.clone())]
3496 }
3497 IrInstructionKind::Nop
3498 | IrInstructionKind::Constant { .. }
3499 | IrInstructionKind::InitializeStorage { .. }
3500 | IrInstructionKind::LoadStorage { .. }
3501 | IrInstructionKind::LoadContextSlot { .. }
3502 | IrInstructionKind::LoadComputed { .. }
3503 | IrInstructionKind::LoadResource { .. } => Vec::new(),
3504 }
3505}
3506
3507fn instruction_storages(kind: &IrInstructionKind) -> Vec<&IrStorageId> {
3508 match kind {
3509 IrInstructionKind::InitializeStorage { storage }
3510 | IrInstructionKind::LoadStorage { storage }
3511 | IrInstructionKind::StoreStorage { storage, .. } => vec![storage],
3512 IrInstructionKind::Nop
3513 | IrInstructionKind::Constant { .. }
3514 | IrInstructionKind::Copy { .. }
3515 | IrInstructionKind::InitializeContextSlot { .. }
3516 | IrInstructionKind::LoadComputed { .. }
3517 | IrInstructionKind::LoadResource { .. }
3518 | IrInstructionKind::LoadContextSlot { .. }
3519 | IrInstructionKind::GetMember { .. }
3520 | IrInstructionKind::GetIndex { .. }
3521 | IrInstructionKind::Select { .. }
3522 | IrInstructionKind::Template { .. }
3523 | IrInstructionKind::PurePackageCall { .. }
3524 | IrInstructionKind::CapabilityCall { .. }
3525 | IrInstructionKind::CapabilityAssign { .. }
3526 | IrInstructionKind::Binary { .. }
3527 | IrInstructionKind::Unary { .. } => Vec::new(),
3528 }
3529}
3530
3531#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3533pub struct IrLoopId(String);
3534
3535impl IrLoopId {
3536 #[must_use]
3537 pub fn for_function(function: &SemanticId, name: &str) -> Self {
3538 Self(format!("{function}/loop:{name}"))
3539 }
3540
3541 #[must_use]
3542 pub fn as_str(&self) -> &str {
3543 &self.0
3544 }
3545}
3546
3547impl std::fmt::Display for IrLoopId {
3548 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3549 formatter.write_str(&self.0)
3550 }
3551}
3552
3553#[derive(Debug, Clone, PartialEq, Eq)]
3555pub struct IrBlock {
3556 pub id: IrBlockId,
3557 pub provenance: SourceProvenance,
3558 pub instructions: Vec<IrInstruction>,
3559}
3560
3561#[derive(Debug, Clone, PartialEq, Eq)]
3563pub struct IrBranchEdge {
3564 pub from: IrBlockId,
3565 pub to: IrBlockId,
3566 pub arm: IrBranchArm,
3567 pub provenance: SourceProvenance,
3568}
3569
3570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3572pub enum IrBranchArm {
3573 True,
3574 False,
3575}
3576
3577#[derive(Debug, Clone, PartialEq, Eq)]
3579pub struct IrLoop {
3580 pub id: IrLoopId,
3581 pub header: IrBlockId,
3582 pub body: Vec<IrBlockId>,
3583 pub latches: Vec<IrBlockId>,
3584 pub exits: Vec<IrBlockId>,
3585 pub provenance: SourceProvenance,
3586}
3587
3588#[derive(Debug, Clone, PartialEq, Eq)]
3590pub struct IrDominatorTree {
3591 pub function: SemanticId,
3592 pub dominators: BTreeMap<IrBlockId, Vec<IrBlockId>>,
3593}
3594
3595impl IrDominatorTree {
3596 #[must_use]
3597 pub fn dominators_of(&self, block: &IrBlockId) -> Option<&[IrBlockId]> {
3598 self.dominators.get(block).map(Vec::as_slice)
3599 }
3600
3601 #[must_use]
3602 pub fn dominates(&self, dominator: &IrBlockId, block: &IrBlockId) -> bool {
3603 self.dominators_of(block)
3604 .is_some_and(|dominators| dominators.contains(dominator))
3605 }
3606}
3607
3608#[derive(Debug, Clone, PartialEq, Eq)]
3610pub struct IrPostDominatorTree {
3611 pub function: SemanticId,
3612 pub post_dominators: BTreeMap<IrBlockId, Vec<IrBlockId>>,
3613}
3614
3615impl IrPostDominatorTree {
3616 #[must_use]
3617 pub fn post_dominators_of(&self, block: &IrBlockId) -> Option<&[IrBlockId]> {
3618 self.post_dominators.get(block).map(Vec::as_slice)
3619 }
3620
3621 #[must_use]
3622 pub fn post_dominates(&self, post_dominator: &IrBlockId, block: &IrBlockId) -> bool {
3623 self.post_dominators_of(block)
3624 .is_some_and(|post_dominators| post_dominators.contains(post_dominator))
3625 }
3626}
3627
3628#[must_use]
3630pub fn compute_dominators(function: &IrFunction) -> IrDominatorTree {
3631 let block_ids = function
3632 .blocks
3633 .iter()
3634 .map(|block| block.id.clone())
3635 .collect::<BTreeSet<_>>();
3636 let mut predecessors = block_ids
3637 .iter()
3638 .cloned()
3639 .map(|block| (block, BTreeSet::new()))
3640 .collect::<BTreeMap<_, _>>();
3641 for edge in &function.branch_edges {
3642 if block_ids.contains(&edge.from) && block_ids.contains(&edge.to) {
3643 predecessors
3644 .entry(edge.to.clone())
3645 .or_default()
3646 .insert(edge.from.clone());
3647 }
3648 }
3649
3650 let mut dominators = block_ids
3651 .iter()
3652 .cloned()
3653 .map(|block| {
3654 let initial = if block == function.entry_block {
3655 BTreeSet::from([block.clone()])
3656 } else {
3657 block_ids.clone()
3658 };
3659 (block, initial)
3660 })
3661 .collect::<BTreeMap<_, _>>();
3662 let mut changed = true;
3663 while changed {
3664 changed = false;
3665 for block in &block_ids {
3666 if *block == function.entry_block {
3667 continue;
3668 }
3669 let mut next = predecessors[block]
3670 .iter()
3671 .filter_map(|predecessor| dominators.get(predecessor).cloned())
3672 .reduce(|mut shared, predecessor_dominators| {
3673 shared.retain(|candidate| predecessor_dominators.contains(candidate));
3674 shared
3675 })
3676 .unwrap_or_default();
3677 next.insert(block.clone());
3678 if dominators.get(block) != Some(&next) {
3679 dominators.insert(block.clone(), next);
3680 changed = true;
3681 }
3682 }
3683 }
3684
3685 IrDominatorTree {
3686 function: function.id.clone(),
3687 dominators: dominators
3688 .into_iter()
3689 .map(|(block, dominators)| (block, dominators.into_iter().collect()))
3690 .collect(),
3691 }
3692}
3693
3694#[must_use]
3696pub fn compute_post_dominators(function: &IrFunction) -> IrPostDominatorTree {
3697 let block_ids = function
3698 .blocks
3699 .iter()
3700 .map(|block| block.id.clone())
3701 .collect::<BTreeSet<_>>();
3702 let mut successors = block_ids
3703 .iter()
3704 .cloned()
3705 .map(|block| (block, BTreeSet::new()))
3706 .collect::<BTreeMap<_, _>>();
3707 for edge in &function.branch_edges {
3708 if block_ids.contains(&edge.from) && block_ids.contains(&edge.to) {
3709 successors
3710 .entry(edge.from.clone())
3711 .or_default()
3712 .insert(edge.to.clone());
3713 }
3714 }
3715 let exits = successors
3716 .iter()
3717 .filter_map(|(block, successors)| successors.is_empty().then_some(block.clone()))
3718 .collect::<BTreeSet<_>>();
3719
3720 let mut post_dominators = block_ids
3721 .iter()
3722 .cloned()
3723 .map(|block| {
3724 let initial = if exits.contains(&block) {
3725 BTreeSet::from([block.clone()])
3726 } else {
3727 block_ids.clone()
3728 };
3729 (block, initial)
3730 })
3731 .collect::<BTreeMap<_, _>>();
3732 let mut changed = true;
3733 while changed {
3734 changed = false;
3735 for block in &block_ids {
3736 if exits.contains(block) {
3737 continue;
3738 }
3739 let mut next = successors[block]
3740 .iter()
3741 .filter_map(|successor| post_dominators.get(successor).cloned())
3742 .reduce(|mut shared, successor_post_dominators| {
3743 shared.retain(|candidate| successor_post_dominators.contains(candidate));
3744 shared
3745 })
3746 .unwrap_or_default();
3747 next.insert(block.clone());
3748 if post_dominators.get(block) != Some(&next) {
3749 post_dominators.insert(block.clone(), next);
3750 changed = true;
3751 }
3752 }
3753 }
3754
3755 IrPostDominatorTree {
3756 function: function.id.clone(),
3757 post_dominators: post_dominators
3758 .into_iter()
3759 .map(|(block, post_dominators)| (block, post_dominators.into_iter().collect()))
3760 .collect(),
3761 }
3762}
3763
3764#[derive(Debug, Clone, PartialEq, Eq)]
3766pub struct IrInstruction {
3767 pub id: IrInstructionId,
3768 pub provenance: SourceProvenance,
3769 pub result: Option<IrValueId>,
3770 pub semantic_origin: Option<SemanticId>,
3771 pub kind: IrInstructionKind,
3772}
3773
3774#[derive(Debug, Clone, PartialEq, Eq)]
3776pub enum IrInstructionKind {
3777 Nop,
3778 Constant {
3779 value: IrConstant,
3780 },
3781 Copy {
3782 source: IrOperand,
3783 },
3784 InitializeStorage {
3785 storage: IrStorageId,
3786 },
3787 InitializeContextSlot {
3789 slot: ContextValueSlotId,
3790 value: IrValueId,
3791 },
3792 LoadStorage {
3793 storage: IrStorageId,
3794 },
3795 LoadContextSlot {
3797 slot: ContextValueSlotId,
3798 },
3799 LoadComputed {
3800 computed: SemanticId,
3801 },
3802 LoadResource {
3806 declaration: SemanticId,
3807 },
3808 GetMember {
3809 object: IrOperand,
3810 property: String,
3811 optional: bool,
3812 },
3813 GetIndex {
3814 object: IrOperand,
3815 index: IrOperand,
3816 },
3817 Select {
3818 condition: IrOperand,
3819 when_true: IrOperand,
3820 when_false: IrOperand,
3821 },
3822 Template {
3824 quasis: Vec<String>,
3825 expressions: Vec<IrValueId>,
3826 },
3827 PurePackageCall {
3829 package: String,
3830 version: String,
3831 integrity: String,
3832 export: String,
3833 runtime_module: String,
3834 resume_policy: String,
3835 operation: crate::semantic_package::SemanticPackagePureOperation,
3836 arguments: Vec<IrValueId>,
3837 },
3838 StoreStorage {
3839 storage: IrStorageId,
3840 value: IrOperand,
3841 },
3842 CapabilityCall {
3844 operation: CapabilityOperationId,
3845 arguments: Vec<IrValueId>,
3846 },
3847 CapabilityAssign {
3849 operation: CapabilityOperationId,
3850 value: IrValueId,
3851 },
3852 Binary {
3853 operation: IrBinaryOperation,
3854 left: IrOperand,
3855 right: IrOperand,
3856 },
3857 Unary {
3858 operation: IrUnaryOperation,
3859 operand: IrOperand,
3860 },
3861}
3862
3863impl IrInstructionKind {
3864 #[must_use]
3867 pub const fn is_observable_side_effect(&self) -> bool {
3868 matches!(
3869 self,
3870 Self::CapabilityCall { .. }
3871 | Self::CapabilityAssign { .. }
3872 | Self::InitializeContextSlot { .. }
3873 )
3874 }
3875}
3876
3877pub struct IrCopyPropagationPass;
3879
3880impl IrOptimizationPass for IrCopyPropagationPass {
3881 fn name(&self) -> &'static str {
3882 "copy-propagation"
3883 }
3884
3885 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
3886 let mut output = input.clone();
3887 for module in &mut output.modules {
3888 for function in &mut module.functions {
3889 let mut copies = BTreeMap::new();
3890 for block in &mut function.blocks {
3891 for instruction in &mut block.instructions {
3892 replace_copy_operands(&mut instruction.kind, &copies);
3893 if let (Some(result), IrInstructionKind::Copy { source }) =
3894 (&instruction.result, &instruction.kind)
3895 {
3896 copies.insert(result.clone(), source.clone());
3897 }
3898 }
3899 }
3900 }
3901 }
3902 output
3903 }
3904}
3905
3906pub struct IrCommonSubexpressionEliminationPass;
3908
3909impl IrOptimizationPass for IrCommonSubexpressionEliminationPass {
3910 fn name(&self) -> &'static str {
3911 "common-subexpression-elimination"
3912 }
3913
3914 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
3915 let mut output = input.clone();
3916 for module in &mut output.modules {
3917 for function in &mut module.functions {
3918 let mut expressions = BTreeMap::<String, IrValueId>::new();
3919 for block in &mut function.blocks {
3920 for instruction in &mut block.instructions {
3921 let Some(result) = instruction.result.clone() else {
3922 continue;
3923 };
3924 let key = match &instruction.kind {
3925 IrInstructionKind::Unary { .. } | IrInstructionKind::Binary { .. } => {
3926 format!("{:?}", instruction.kind)
3927 }
3928 _ => continue,
3929 };
3930 if let Some(existing) = expressions.get(&key) {
3931 instruction.kind = IrInstructionKind::Copy {
3932 source: IrOperand::Value(existing.clone()),
3933 };
3934 } else {
3935 expressions.insert(key, result);
3936 }
3937 }
3938 }
3939 }
3940 }
3941 output
3942 }
3943}
3944
3945pub struct IrInstructionSimplificationPass;
3947
3948impl IrOptimizationPass for IrInstructionSimplificationPass {
3949 fn name(&self) -> &'static str {
3950 "instruction-simplification"
3951 }
3952
3953 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
3954 let mut output = input.clone();
3955 for module in &mut output.modules {
3956 for function in &mut module.functions {
3957 let constants = analyze_constant_propagation(function).constants;
3958 for block in &mut function.blocks {
3959 for instruction in &mut block.instructions {
3960 if let IrInstructionKind::Copy { source } = &instruction.kind {
3961 if let Some(value) = resolve_constant(source, &constants) {
3962 instruction.kind = IrInstructionKind::Constant { value };
3963 }
3964 }
3965 }
3966 }
3967 }
3968 }
3969 output
3970 }
3971}
3972
3973pub struct IrCfgCleanupPass;
3975
3976impl IrOptimizationPass for IrCfgCleanupPass {
3977 fn name(&self) -> &'static str {
3978 "cfg-cleanup"
3979 }
3980
3981 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
3982 let mut output = input.clone();
3983 for module in &mut output.modules {
3984 for function in &mut module.functions {
3985 let reachable = analyze_reachability(function)
3986 .reachable
3987 .into_iter()
3988 .collect::<BTreeSet<_>>();
3989 function
3990 .blocks
3991 .retain(|block| reachable.contains(&block.id));
3992 function
3993 .branch_edges
3994 .retain(|edge| reachable.contains(&edge.from) && reachable.contains(&edge.to));
3995 function
3996 .loops
3997 .retain(|loop_region| reachable.contains(&loop_region.header));
3998 let instructions = function
3999 .blocks
4000 .iter()
4001 .flat_map(|block| {
4002 block
4003 .instructions
4004 .iter()
4005 .map(|instruction| instruction.id.clone())
4006 })
4007 .collect::<BTreeSet<_>>();
4008 function.values.retain(|_, value| !matches!(&value.definition, IrValueDefinition::Instruction(instruction) if !instructions.contains(instruction)));
4009 }
4010 }
4011 output
4012 }
4013}
4014
4015fn replace_copy_operands(kind: &mut IrInstructionKind, copies: &BTreeMap<IrValueId, IrOperand>) {
4016 let resolve = |operand: &mut IrOperand| {
4017 while let IrOperand::Value(value) = operand {
4018 let Some(replacement) = copies.get(value) else {
4019 break;
4020 };
4021 *operand = replacement.clone();
4022 }
4023 };
4024 match kind {
4025 IrInstructionKind::Copy { source }
4026 | IrInstructionKind::StoreStorage { value: source, .. }
4027 | IrInstructionKind::Unary {
4028 operand: source, ..
4029 }
4030 | IrInstructionKind::GetMember { object: source, .. } => resolve(source),
4031 IrInstructionKind::GetIndex { object, index } => {
4032 resolve(object);
4033 resolve(index);
4034 }
4035 IrInstructionKind::Select {
4036 condition,
4037 when_true,
4038 when_false,
4039 } => {
4040 resolve(condition);
4041 resolve(when_true);
4042 resolve(when_false);
4043 }
4044 IrInstructionKind::Binary { left, right, .. } => {
4045 resolve(left);
4046 resolve(right);
4047 }
4048 _ => {}
4049 }
4050}
4051
4052pub struct IrConstantFoldingPass;
4054
4055impl IrOptimizationPass for IrConstantFoldingPass {
4056 fn name(&self) -> &'static str {
4057 "constant-folding"
4058 }
4059
4060 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
4061 let mut output = input.clone();
4062 for module in &mut output.modules {
4063 for function in &mut module.functions {
4064 let constants = analyze_constant_propagation(function).constants;
4065 for block in &mut function.blocks {
4066 for instruction in &mut block.instructions {
4067 if let Some(result) = &instruction.result {
4068 if let Some(value) = constants.get(result) {
4069 instruction.kind = IrInstructionKind::Constant {
4070 value: value.clone(),
4071 };
4072 }
4073 }
4074 }
4075 }
4076 }
4077 }
4078 output
4079 }
4080}
4081
4082pub struct IrDeadCodeEliminationPass;
4084
4085impl IrOptimizationPass for IrDeadCodeEliminationPass {
4086 fn name(&self) -> &'static str {
4087 "dead-code-elimination"
4088 }
4089
4090 fn run(&self, input: &IntermediateRepresentation) -> IntermediateRepresentation {
4091 let mut output = input.clone();
4092 for module in &mut output.modules {
4093 for function in &mut module.functions {
4094 let preserved_values = module
4095 .computed_evaluations
4096 .iter()
4097 .filter(|evaluation| evaluation.function == function.id)
4098 .map(|evaluation| evaluation.result.clone())
4099 .collect::<BTreeSet<_>>();
4100 loop {
4101 let dead = analyze_dead_assignments_preserving(function, &preserved_values)
4102 .instructions
4103 .into_iter()
4104 .collect::<BTreeSet<_>>();
4105 if dead.is_empty() {
4106 break;
4107 }
4108 for block in &mut function.blocks {
4109 block
4110 .instructions
4111 .retain(|instruction| !dead.contains(&instruction.id));
4112 }
4113 function.values.retain(|_, value| {
4114 !matches!(
4115 &value.definition,
4116 IrValueDefinition::Instruction(instruction) if dead.contains(instruction)
4117 )
4118 });
4119 }
4120 }
4121 }
4122 output
4123 }
4124}
4125
4126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4128pub enum IrBinaryOperation {
4129 Add,
4130 Subtract,
4131 Multiply,
4132 Divide,
4133 Remainder,
4134 Equal,
4135 NotEqual,
4136 LessThan,
4137 LessThanOrEqual,
4138 GreaterThan,
4139 GreaterThanOrEqual,
4140 And,
4141 Or,
4142 NullishCoalesce,
4143 Min,
4144 Max,
4145}
4146
4147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4149pub enum IrUnaryOperation {
4150 Not,
4151 Identity,
4152 Negate,
4153 Abs,
4154 Floor,
4155 Ceil,
4156 Round,
4157}
4158
4159#[cfg(test)]
4160mod tests {
4161 use super::{
4162 compute_dominators, compute_post_dominators, lower_components_to_ir, optimize_computed_ir,
4163 optimize_context_ir, optimize_effect_ir, validate_context_ir, validate_effect_ir,
4164 validate_intermediate_representation, validate_optimized_context_ir, ContextIrReport,
4165 IntermediateRepresentation, IrBinaryOperation, IrBlock, IrBlockId, IrBranchArm,
4166 IrBranchEdge, IrConstant, IrFunction, IrInstruction, IrInstructionId, IrInstructionKind,
4167 IrLoop, IrLoopId, IrModule, IrOperand, IrStorageId, IrUnaryOperation, IrValue,
4168 IrValueDefinition, IrValueId,
4169 };
4170 use crate::{
4171 build_application_semantic_model, CapabilityOperationId, ConsumerId, ContextId,
4172 ContextValueSourceId, ProviderId, SemanticId, SemanticType, SourceProvenance,
4173 };
4174 use std::collections::BTreeMap;
4175
4176 #[test]
4177 fn represents_backend_neutral_ir_structure_with_provenance() {
4178 let provenance = SourceProvenance::new(
4179 "src/Counter.tsx",
4180 presolve_parser::SourceSpan {
4181 start: 0,
4182 end: 1,
4183 line: 1,
4184 column: 1,
4185 },
4186 );
4187 let function = IrFunction {
4188 id: SemanticId::component(Some("x-counter"), "Counter").method("increment"),
4189 name: "increment".to_string(),
4190 provenance: provenance.clone(),
4191 entry_block: IrBlockId::entry_for(
4192 &SemanticId::component(Some("x-counter"), "Counter").method("increment"),
4193 ),
4194 blocks: vec![IrBlock {
4195 id: IrBlockId::entry_for(
4196 &SemanticId::component(Some("x-counter"), "Counter").method("increment"),
4197 ),
4198 provenance: provenance.clone(),
4199 instructions: vec![IrInstruction {
4200 id: IrInstructionId::for_block(
4201 &IrBlockId::entry_for(
4202 &SemanticId::component(Some("x-counter"), "Counter")
4203 .method("increment"),
4204 ),
4205 0,
4206 ),
4207 provenance,
4208 result: None,
4209 semantic_origin: None,
4210 kind: IrInstructionKind::Nop,
4211 }],
4212 }],
4213 branch_edges: Vec::new(),
4214 values: BTreeMap::new(),
4215 loops: Vec::new(),
4216 };
4217 let ir = IntermediateRepresentation {
4218 modules: vec![IrModule {
4219 path: "src/Counter.tsx".into(),
4220 components: vec![SemanticId::component(Some("x-counter"), "Counter")],
4221 storages: Vec::new(),
4222 storage_initializers: Vec::new(),
4223 template_entrypoints: Vec::new(),
4224 functions: vec![function],
4225 computed_evaluations: Vec::new(),
4226 effect_executions: Vec::new(),
4227 }],
4228 context_ir: ContextIrReport::default(),
4229 };
4230
4231 assert_eq!(ir.modules[0].functions[0].blocks[0].instructions.len(), 1);
4232 }
4233
4234 #[test]
4235 fn represents_provenanced_conditional_branch_edges() {
4236 let provenance = SourceProvenance::new(
4237 "src/Counter.tsx",
4238 presolve_parser::SourceSpan {
4239 start: 0,
4240 end: 1,
4241 line: 1,
4242 column: 1,
4243 },
4244 );
4245 let function = SemanticId::component(Some("x-counter"), "Counter").method("render");
4246 let entry = IrBlockId::entry_for(&function);
4247 let when_true = IrBlockId::for_function(&function, "when-true");
4248 let branch = IrBranchEdge {
4249 from: entry,
4250 to: when_true,
4251 arm: IrBranchArm::True,
4252 provenance,
4253 };
4254
4255 assert_eq!(
4256 branch.from.as_str(),
4257 "component:x-counter/method:render/block:entry"
4258 );
4259 assert_eq!(
4260 branch.to.as_str(),
4261 "component:x-counter/method:render/block:when-true"
4262 );
4263 assert_eq!(branch.arm, IrBranchArm::True);
4264 }
4265
4266 #[test]
4267 fn keeps_ir_identity_domains_distinct_and_deterministic() {
4268 let function = SemanticId::component(Some("x-counter"), "Counter").method("increment");
4269 let block = IrBlockId::entry_for(&function);
4270 let instruction = IrInstructionId::for_block(&block, 0);
4271 let value = IrValueId::for_function(&function, 0);
4272 let storage = IrStorageId::for_semantic_origin(
4273 &SemanticId::component(Some("x-counter"), "Counter").state_field("count"),
4274 );
4275
4276 assert_eq!(
4277 instruction.as_str(),
4278 "component:x-counter/method:increment/block:entry/instruction:0"
4279 );
4280 assert_eq!(
4281 value.as_str(),
4282 "component:x-counter/method:increment/value:0"
4283 );
4284 assert_eq!(storage.as_str(), "storage:component:x-counter/state:count");
4285 assert_ne!(instruction.as_str(), value.as_str());
4286 assert_ne!(value.as_str(), storage.as_str());
4287 }
4288
4289 #[test]
4290 fn represents_closed_ir_operands_without_semantic_identity_operands() {
4291 let function = SemanticId::component(Some("x-counter"), "Counter").method("increment");
4292 let value = IrOperand::Value(IrValueId::for_function(&function, 0));
4293 let constant = IrOperand::Constant(IrConstant::Number("1".to_string()));
4294 let storage = IrOperand::Storage(IrStorageId::for_semantic_origin(
4295 &SemanticId::component(Some("x-counter"), "Counter").state_field("count"),
4296 ));
4297
4298 assert!(matches!(value, IrOperand::Value(_)));
4299 assert!(
4300 matches!(constant, IrOperand::Constant(IrConstant::Number(number)) if number == "1")
4301 );
4302 assert!(matches!(storage, IrOperand::Storage(_)));
4303 }
4304
4305 #[test]
4306 fn records_instruction_results_separately_from_operation_identity() {
4307 let provenance = SourceProvenance::new(
4308 "src/Counter.tsx",
4309 presolve_parser::SourceSpan {
4310 start: 0,
4311 end: 1,
4312 line: 1,
4313 column: 1,
4314 },
4315 );
4316 let function = SemanticId::component(Some("x-counter"), "Counter").method("increment");
4317 let block = IrBlockId::entry_for(&function);
4318 let storage = IrStorageId::for_semantic_origin(
4319 &SemanticId::component(Some("x-counter"), "Counter").state_field("count"),
4320 );
4321 let instruction = IrInstruction {
4322 id: IrInstructionId::for_block(&block, 0),
4323 provenance,
4324 result: Some(IrValueId::for_function(&function, 0)),
4325 semantic_origin: Some(
4326 SemanticId::component(Some("x-counter"), "Counter").state_field("count"),
4327 ),
4328 kind: IrInstructionKind::LoadStorage { storage },
4329 };
4330
4331 assert_eq!(
4332 instruction.id.as_str(),
4333 "component:x-counter/method:increment/block:entry/instruction:0"
4334 );
4335 assert_eq!(
4336 instruction.result.expect("load result").as_str(),
4337 "component:x-counter/method:increment/value:0"
4338 );
4339 assert!(matches!(
4340 instruction.kind,
4341 IrInstructionKind::LoadStorage { .. }
4342 ));
4343 }
4344
4345 #[test]
4346 fn indexes_values_by_function_scoped_value_identity() {
4347 let provenance = SourceProvenance::new(
4348 "src/Counter.tsx",
4349 presolve_parser::SourceSpan {
4350 start: 0,
4351 end: 1,
4352 line: 1,
4353 column: 1,
4354 },
4355 );
4356 let id = SemanticId::component(Some("x-counter"), "Counter").method("increment");
4357 let entry = IrBlockId::entry_for(&id);
4358 let value_id = IrValueId::for_function(&id, 0);
4359 let value = IrValue {
4360 id: value_id.clone(),
4361 definition: IrValueDefinition::Instruction(IrInstructionId::for_block(&entry, 0)),
4362 semantic_type: SemanticType::Number,
4363 provenance: provenance.clone(),
4364 semantic_origin: Some(
4365 SemanticId::component(Some("x-counter"), "Counter").state_field("count"),
4366 ),
4367 };
4368 let function = IrFunction {
4369 id,
4370 name: "increment".to_string(),
4371 provenance: provenance.clone(),
4372 entry_block: entry.clone(),
4373 blocks: vec![IrBlock {
4374 id: entry,
4375 provenance,
4376 instructions: Vec::new(),
4377 }],
4378 branch_edges: Vec::new(),
4379 values: BTreeMap::from([(value_id.clone(), value)]),
4380 loops: Vec::new(),
4381 };
4382
4383 assert_eq!(
4384 function.value(&value_id).expect("value").semantic_type,
4385 SemanticType::Number
4386 );
4387 assert!(matches!(
4388 function.value(&value_id).expect("value").definition,
4389 IrValueDefinition::Instruction(_)
4390 ));
4391 }
4392
4393 #[test]
4394 fn represents_natural_loops_with_header_latches_and_exits() {
4395 let provenance = SourceProvenance::new(
4396 "src/Counter.tsx",
4397 presolve_parser::SourceSpan {
4398 start: 0,
4399 end: 1,
4400 line: 1,
4401 column: 1,
4402 },
4403 );
4404 let function = SemanticId::component(Some("x-counter"), "Counter").method("render");
4405 let header = IrBlockId::for_function(&function, "loop-header");
4406 let latch = IrBlockId::for_function(&function, "loop-latch");
4407 let exit = IrBlockId::for_function(&function, "loop-exit");
4408 let loop_region = IrLoop {
4409 id: IrLoopId::for_function(&function, "items"),
4410 header: header.clone(),
4411 body: vec![header, latch.clone()],
4412 latches: vec![latch],
4413 exits: vec![exit],
4414 provenance,
4415 };
4416
4417 assert_eq!(
4418 loop_region.id.as_str(),
4419 "component:x-counter/method:render/loop:items"
4420 );
4421 assert_eq!(loop_region.body[0], loop_region.header);
4422 assert_eq!(loop_region.body[1], loop_region.latches[0]);
4423 assert_eq!(
4424 loop_region.exits[0].as_str(),
4425 "component:x-counter/method:render/block:loop-exit"
4426 );
4427 }
4428
4429 #[test]
4430 fn computes_dominators_from_canonical_branch_edges() {
4431 let provenance = SourceProvenance::new(
4432 "src/Counter.tsx",
4433 presolve_parser::SourceSpan {
4434 start: 0,
4435 end: 1,
4436 line: 1,
4437 column: 1,
4438 },
4439 );
4440 let id = SemanticId::component(Some("x-counter"), "Counter").method("render");
4441 let entry = IrBlockId::entry_for(&id);
4442 let when_true = IrBlockId::for_function(&id, "when-true");
4443 let when_false = IrBlockId::for_function(&id, "when-false");
4444 let merge = IrBlockId::for_function(&id, "merge");
4445 let function = IrFunction {
4446 id: id.clone(),
4447 name: "render".to_string(),
4448 provenance: provenance.clone(),
4449 entry_block: entry.clone(),
4450 blocks: vec![
4451 IrBlock {
4452 id: entry.clone(),
4453 provenance: provenance.clone(),
4454 instructions: Vec::new(),
4455 },
4456 IrBlock {
4457 id: when_true.clone(),
4458 provenance: provenance.clone(),
4459 instructions: Vec::new(),
4460 },
4461 IrBlock {
4462 id: when_false.clone(),
4463 provenance: provenance.clone(),
4464 instructions: Vec::new(),
4465 },
4466 IrBlock {
4467 id: merge.clone(),
4468 provenance: provenance.clone(),
4469 instructions: Vec::new(),
4470 },
4471 ],
4472 branch_edges: vec![
4473 IrBranchEdge {
4474 from: entry.clone(),
4475 to: when_true.clone(),
4476 arm: IrBranchArm::True,
4477 provenance: provenance.clone(),
4478 },
4479 IrBranchEdge {
4480 from: entry.clone(),
4481 to: when_false.clone(),
4482 arm: IrBranchArm::False,
4483 provenance: provenance.clone(),
4484 },
4485 IrBranchEdge {
4486 from: when_true,
4487 to: merge.clone(),
4488 arm: IrBranchArm::True,
4489 provenance: provenance.clone(),
4490 },
4491 IrBranchEdge {
4492 from: when_false,
4493 to: merge.clone(),
4494 arm: IrBranchArm::False,
4495 provenance,
4496 },
4497 ],
4498 values: BTreeMap::new(),
4499 loops: Vec::new(),
4500 };
4501
4502 let tree = compute_dominators(&function);
4503
4504 assert_eq!(tree.function, id);
4505 assert_eq!(tree.dominators[&entry], vec![entry.clone()]);
4506 assert_eq!(tree.dominators[&merge], vec![entry, merge]);
4507 }
4508
4509 #[test]
4510 fn computes_post_dominators_from_canonical_branch_edges() {
4511 let provenance = SourceProvenance::new(
4512 "src/Counter.tsx",
4513 presolve_parser::SourceSpan {
4514 start: 0,
4515 end: 1,
4516 line: 1,
4517 column: 1,
4518 },
4519 );
4520 let id = SemanticId::component(Some("x-counter"), "Counter").method("render");
4521 let entry = IrBlockId::entry_for(&id);
4522 let when_true = IrBlockId::for_function(&id, "when-true");
4523 let when_false = IrBlockId::for_function(&id, "when-false");
4524 let merge = IrBlockId::for_function(&id, "merge");
4525 let function = IrFunction {
4526 id: id.clone(),
4527 name: "render".to_string(),
4528 provenance: provenance.clone(),
4529 entry_block: entry.clone(),
4530 blocks: [
4531 entry.clone(),
4532 when_true.clone(),
4533 when_false.clone(),
4534 merge.clone(),
4535 ]
4536 .into_iter()
4537 .map(|id| IrBlock {
4538 id,
4539 provenance: provenance.clone(),
4540 instructions: Vec::new(),
4541 })
4542 .collect(),
4543 branch_edges: vec![
4544 IrBranchEdge {
4545 from: entry.clone(),
4546 to: when_true.clone(),
4547 arm: IrBranchArm::True,
4548 provenance: provenance.clone(),
4549 },
4550 IrBranchEdge {
4551 from: entry.clone(),
4552 to: when_false.clone(),
4553 arm: IrBranchArm::False,
4554 provenance: provenance.clone(),
4555 },
4556 IrBranchEdge {
4557 from: when_true,
4558 to: merge.clone(),
4559 arm: IrBranchArm::True,
4560 provenance: provenance.clone(),
4561 },
4562 IrBranchEdge {
4563 from: when_false,
4564 to: merge.clone(),
4565 arm: IrBranchArm::False,
4566 provenance,
4567 },
4568 ],
4569 values: BTreeMap::new(),
4570 loops: Vec::new(),
4571 };
4572
4573 let tree = compute_post_dominators(&function);
4574
4575 assert_eq!(tree.function, id);
4576 assert_eq!(tree.post_dominators[&merge], vec![merge.clone()]);
4577 assert_eq!(tree.post_dominators[&entry], vec![entry, merge]);
4578 }
4579
4580 #[test]
4581 fn queries_canonical_cfg_connectivity_and_dominance() {
4582 let provenance = SourceProvenance::new(
4583 "src/Counter.tsx",
4584 presolve_parser::SourceSpan {
4585 start: 0,
4586 end: 1,
4587 line: 1,
4588 column: 1,
4589 },
4590 );
4591 let id = SemanticId::component(Some("x-counter"), "Counter").method("render");
4592 let entry = IrBlockId::entry_for(&id);
4593 let exit = IrBlockId::for_function(&id, "exit");
4594 let function = IrFunction {
4595 id,
4596 name: "render".to_string(),
4597 provenance: provenance.clone(),
4598 entry_block: entry.clone(),
4599 blocks: [entry.clone(), exit.clone()]
4600 .into_iter()
4601 .map(|id| IrBlock {
4602 id,
4603 provenance: provenance.clone(),
4604 instructions: Vec::new(),
4605 })
4606 .collect(),
4607 branch_edges: vec![IrBranchEdge {
4608 from: entry.clone(),
4609 to: exit.clone(),
4610 arm: IrBranchArm::True,
4611 provenance,
4612 }],
4613 values: BTreeMap::new(),
4614 loops: Vec::new(),
4615 };
4616 let dominators = compute_dominators(&function);
4617 let post_dominators = compute_post_dominators(&function);
4618
4619 assert_eq!(function.block(&entry).expect("entry").id, entry);
4620 assert_eq!(
4621 function.successor_blocks(&function.entry_block),
4622 vec![exit.clone()]
4623 );
4624 assert_eq!(
4625 function.predecessor_blocks(&exit),
4626 vec![function.entry_block.clone()]
4627 );
4628 assert!(function.is_exit_block(&exit));
4629 assert!(dominators.dominates(&function.entry_block, &exit));
4630 assert!(post_dominators.post_dominates(&exit, &function.entry_block));
4631 }
4632
4633 #[test]
4634 fn lowers_components_into_modules_with_entry_blocks() {
4635 let parsed = presolve_parser::parse_file(
4636 "src/Counter.tsx",
4637 "@component(\"x-counter\") class Counter extends Component { count = state(0); increment() {} render() { return <p>{this.count}</p>; } }",
4638 );
4639 let model = crate::build_application_semantic_model(&parsed);
4640 let ir = lower_components_to_ir(&model);
4641
4642 assert_eq!(ir.modules.len(), 1);
4643 assert_eq!(
4644 ir.modules[0].components,
4645 vec![model.components[0].id.clone()]
4646 );
4647 assert_eq!(ir.modules[0].functions[0].name, "increment");
4648 assert_eq!(ir.modules[0].functions[0].blocks.len(), 1);
4649 assert_eq!(
4650 ir.modules[0].functions[0].entry_block,
4651 ir.modules[0].functions[0].blocks[0].id
4652 );
4653 assert_eq!(
4654 ir.modules[0].functions[0].entry_block.as_str(),
4655 format!("{}/block:entry", ir.modules[0].functions[0].id).as_str()
4656 );
4657 assert!(ir.modules[0].functions[0].blocks[0].instructions.is_empty());
4658 assert!(ir.modules[0].functions[0].branch_edges.is_empty());
4659 assert!(ir.modules[0].functions[0].loops.is_empty());
4660 assert!(matches!(
4661 ir.modules[0].storage_initializers[0].kind,
4662 IrInstructionKind::InitializeStorage { .. }
4663 ));
4664 assert_eq!(ir.modules[0].storages.len(), 1);
4665 assert_eq!(
4666 ir.modules[0].storages[0].id.as_str(),
4667 format!("storage:{}", model.components[0].state_fields[0].id).as_str()
4668 );
4669 assert_eq!(
4670 ir.modules[0].storage_initializers[0].semantic_origin,
4671 Some(model.components[0].state_fields[0].id.clone())
4672 );
4673 assert_eq!(ir.modules[0].template_entrypoints.len(), 1);
4674 assert_eq!(
4675 ir.modules[0].template_entrypoints[0].render_method,
4676 model.components[0]
4677 .methods
4678 .iter()
4679 .find(|method| method.name == "render")
4680 .expect("render")
4681 .id
4682 );
4683 }
4684
4685 fn computed_ir_fixture() -> (
4686 IntermediateRepresentation,
4687 SemanticId,
4688 SemanticId,
4689 SemanticId,
4690 ) {
4691 let parsed = presolve_parser::parse_file(
4692 "src/ComputedIr.tsx",
4693 r#"
4694@component("x-computed-ir")
4695class ComputedIr extends Component {
4696 count = state(1);
4697 profile = state({ hidden: false });
4698
4699 @computed()
4700 get doubled() { return this.count * 2; }
4701
4702 @computed()
4703 get visible() { return ((this.doubled >= 2 && !this.profile.hidden) ?? false) || true; }
4704}
4705"#,
4706 );
4707 let model = crate::build_application_semantic_model(&parsed);
4708 let component = &model.components[0];
4709 let count = component.id.state_field("count");
4710 let doubled = component.id.computed("doubled");
4711 let visible = component.id.computed("visible");
4712 (lower_components_to_ir(&model), count, doubled, visible)
4713 }
4714
4715 #[test]
4716 fn lowers_planned_computed_evaluations_into_canonical_ir_functions() {
4717 let (ir, count, doubled, visible) = computed_ir_fixture();
4718 let module = &ir.modules[0];
4719 let doubled_function = module
4720 .functions
4721 .iter()
4722 .find(|function| function.id == doubled)
4723 .expect("computed doubled function");
4724 let visible_function = module
4725 .functions
4726 .iter()
4727 .find(|function| function.id == visible)
4728 .expect("computed visible function");
4729
4730 assert_eq!(module.computed_evaluations.len(), 2);
4731 assert_eq!(doubled_function.name, "doubled");
4732 assert!(doubled_function.blocks[0]
4733 .instructions
4734 .iter()
4735 .any(|instruction| {
4736 matches!(
4737 instruction.kind,
4738 IrInstructionKind::LoadStorage { ref storage }
4739 if storage == &IrStorageId::for_semantic_origin(&count)
4740 )
4741 }));
4742 assert!(doubled_function.blocks[0]
4743 .instructions
4744 .iter()
4745 .any(|instruction| {
4746 matches!(
4747 instruction.kind,
4748 IrInstructionKind::Binary {
4749 operation: IrBinaryOperation::Multiply,
4750 ..
4751 }
4752 )
4753 }));
4754 assert!(visible_function.blocks[0]
4755 .instructions
4756 .iter()
4757 .any(|instruction| {
4758 matches!(
4759 instruction.kind,
4760 IrInstructionKind::LoadComputed { ref computed } if computed == &doubled
4761 )
4762 }));
4763 assert!(visible_function.blocks[0]
4764 .instructions
4765 .iter()
4766 .any(|instruction| {
4767 matches!(instruction.kind, IrInstructionKind::GetMember { .. })
4768 }));
4769 for operation in [
4770 IrBinaryOperation::GreaterThanOrEqual,
4771 IrBinaryOperation::And,
4772 IrBinaryOperation::NullishCoalesce,
4773 IrBinaryOperation::Or,
4774 ] {
4775 assert!(visible_function.blocks[0]
4776 .instructions
4777 .iter()
4778 .any(|instruction| {
4779 matches!(
4780 instruction.kind,
4781 IrInstructionKind::Binary {
4782 operation: candidate,
4783 ..
4784 } if candidate == operation
4785 )
4786 }));
4787 }
4788 assert!(visible_function.blocks[0]
4789 .instructions
4790 .iter()
4791 .any(|instruction| {
4792 matches!(
4793 instruction.kind,
4794 IrInstructionKind::Unary {
4795 operation: IrUnaryOperation::Not,
4796 ..
4797 }
4798 )
4799 }));
4800 assert!(module.computed_evaluations.iter().all(|evaluation| {
4801 module
4802 .functions
4803 .iter()
4804 .find(|function| function.id == evaluation.function)
4805 .is_some_and(|function| function.values.contains_key(&evaluation.result))
4806 }));
4807 assert!(validate_intermediate_representation(&ir).is_empty());
4808 }
4809
4810 #[test]
4811 #[allow(clippy::too_many_lines)]
4812 fn lowers_one_effect_function_with_generic_capability_instructions() {
4813 let parsed = presolve_parser::parse_file(
4814 "src/EffectIr.tsx",
4815 r#"
4816@component("x-effect-ir")
4817class EffectIr extends Component {
4818 title = state("Presolve");
4819 theme = state("light");
4820 count = state(1);
4821
4822 @computed()
4823 get total() { return this.count * 2; }
4824
4825 @action()
4826 refreshTitle() { this.title = "Refreshed"; }
4827
4828 @action()
4829 refreshTheme() { this.theme = "dark"; }
4830
4831 @effect()
4832 syncAndReport() {
4833 document.title = this.title;
4834 console.log("total", this.total);
4835 localStorage.setItem("theme", this.theme);
4836 }
4837
4838 @effect()
4839 logReady() { console.log("ready"); }
4840
4841 @effect()
4842 invalidMutation() { this.title = "invalid"; }
4843
4844 render() { return <p />; }
4845}
4846"#,
4847 );
4848 let model = crate::build_application_semantic_model(&parsed);
4849 let component = &model.components[0];
4850 let sync = component.id.effect("syncAndReport");
4851 let ready = component.id.effect("logReady");
4852 let invalid = component.id.effect("invalidMutation");
4853 let ir = lower_components_to_ir(&model);
4854 let module = &ir.modules[0];
4855 let execution = module
4856 .effect_executions
4857 .iter()
4858 .find(|execution| execution.effect == sync)
4859 .expect("sync effect execution");
4860 let function = module
4861 .functions
4862 .iter()
4863 .find(|function| function.id == sync)
4864 .expect("sync effect function");
4865
4866 assert_eq!(module.effect_executions.len(), 2);
4867 assert_eq!(execution.function, sync);
4868 assert_ne!(execution.function, component.id.method("syncAndReport"));
4869 assert_eq!(
4870 execution.capability_operations,
4871 vec![
4872 CapabilityOperationId("builtin.browser.document.title.assign"),
4873 CapabilityOperationId("builtin.browser.console.log"),
4874 CapabilityOperationId("builtin.browser.local_storage.set_item"),
4875 ]
4876 );
4877 let capability_instructions = function.blocks[0]
4878 .instructions
4879 .iter()
4880 .filter(|instruction| {
4881 matches!(
4882 instruction.kind,
4883 IrInstructionKind::CapabilityCall { .. }
4884 | IrInstructionKind::CapabilityAssign { .. }
4885 )
4886 })
4887 .collect::<Vec<_>>();
4888 assert_eq!(capability_instructions.len(), 3);
4889 assert!(matches!(
4890 capability_instructions[0].kind,
4891 IrInstructionKind::CapabilityAssign { ref operation, .. }
4892 if operation == &CapabilityOperationId("builtin.browser.document.title.assign")
4893 ));
4894 assert!(matches!(
4895 capability_instructions[1].kind,
4896 IrInstructionKind::CapabilityCall { ref operation, ref arguments }
4897 if operation == &CapabilityOperationId("builtin.browser.console.log")
4898 && arguments.len() == 2
4899 ));
4900 assert!(matches!(
4901 capability_instructions[2].kind,
4902 IrInstructionKind::CapabilityCall { ref operation, ref arguments }
4903 if operation == &CapabilityOperationId("builtin.browser.local_storage.set_item")
4904 && arguments.len() == 2
4905 ));
4906 assert!(capability_instructions
4907 .iter()
4908 .all(|instruction| instruction.result.is_none()
4909 && instruction.kind.is_observable_side_effect()));
4910 assert!(function.blocks[0].instructions.iter().any(|instruction| {
4911 matches!(
4912 instruction.kind,
4913 IrInstructionKind::LoadStorage { ref storage }
4914 if storage == &IrStorageId::for_semantic_origin(&component.id.state_field("title"))
4915 )
4916 }));
4917 assert!(function.blocks[0].instructions.iter().any(|instruction| {
4918 matches!(
4919 instruction.kind,
4920 IrInstructionKind::LoadComputed { ref computed }
4921 if computed == &component.id.computed("total")
4922 )
4923 }));
4924 let ready_function = module
4925 .functions
4926 .iter()
4927 .find(|function| function.id == ready)
4928 .expect("dependency-free effect function");
4929 assert!(ready_function.blocks[0]
4930 .instructions
4931 .iter()
4932 .all(|instruction| {
4933 !matches!(
4934 instruction.kind,
4935 IrInstructionKind::LoadStorage { .. } | IrInstructionKind::LoadComputed { .. }
4936 )
4937 }));
4938 assert!(!module
4939 .effect_executions
4940 .iter()
4941 .any(|execution| execution.effect == invalid));
4942 assert_eq!(ir.effect_ir_function(&sync), Some(&sync));
4943 assert!(validate_intermediate_representation(&ir).is_empty());
4944 assert!(validate_effect_ir(&model, &ir).is_empty());
4945 }
4946
4947 #[test]
4948 fn optimizes_effect_operands_without_removing_or_reordering_capabilities() {
4949 let parsed = presolve_parser::parse_file(
4950 "src/OptimizedEffectIr.tsx",
4951 r#"
4952@component("x-optimized-effect-ir")
4953class OptimizedEffectIr extends Component {
4954 title = state("Presolve");
4955
4956 @computed()
4957 get unrelated() { return 4 + 5; }
4958
4959 @effect()
4960 report() {
4961 console.log(1 + 2);
4962 document.title = this.title;
4963 console.log("after");
4964 }
4965
4966 render() { return <p />; }
4967}
4968"#,
4969 );
4970 let model = crate::build_application_semantic_model(&parsed);
4971 let component = &model.components[0];
4972 let effect = component.id.effect("report");
4973 let computed = component.id.computed("unrelated");
4974 let input = lower_components_to_ir(&model);
4975 let original_computed = input.modules[0]
4976 .functions
4977 .iter()
4978 .find(|function| function.id == computed)
4979 .expect("original computed function")
4980 .clone();
4981 let report = optimize_effect_ir(&input);
4982 let output = &report.output;
4983 let optimized_effect = output.modules[0]
4984 .functions
4985 .iter()
4986 .find(|function| function.id == effect)
4987 .expect("optimized effect function");
4988 let optimized_computed = output.modules[0]
4989 .functions
4990 .iter()
4991 .find(|function| function.id == computed)
4992 .expect("preserved computed function");
4993 let operations = optimized_effect.blocks[0]
4994 .instructions
4995 .iter()
4996 .filter_map(|instruction| match instruction.kind {
4997 IrInstructionKind::CapabilityCall { operation, .. }
4998 | IrInstructionKind::CapabilityAssign { operation, .. } => Some(operation),
4999 _ => None,
5000 })
5001 .collect::<Vec<_>>();
5002
5003 assert_eq!(input, lower_components_to_ir(&model));
5004 assert_eq!(optimized_computed, &original_computed);
5005 assert_eq!(
5006 operations,
5007 vec![
5008 CapabilityOperationId("builtin.browser.console.log"),
5009 CapabilityOperationId("builtin.browser.document.title.assign"),
5010 CapabilityOperationId("builtin.browser.console.log"),
5011 ]
5012 );
5013 assert!(optimized_effect.blocks[0]
5014 .instructions
5015 .iter()
5016 .any(|instruction| {
5017 matches!(
5018 instruction.kind,
5019 IrInstructionKind::Constant {
5020 value: IrConstant::Number(ref value)
5021 } if value == "3"
5022 )
5023 }));
5024 assert!(optimized_effect.blocks[0]
5025 .instructions
5026 .iter()
5027 .all(|instruction| {
5028 !instruction.kind.is_observable_side_effect() || instruction.result.is_none()
5029 }));
5030 assert_eq!(
5031 report
5032 .passes
5033 .iter()
5034 .map(|pass| pass.name)
5035 .collect::<Vec<_>>(),
5036 vec![
5037 "common-subexpression-elimination",
5038 "copy-propagation",
5039 "constant-folding",
5040 "instruction-simplification",
5041 "dead-code-elimination",
5042 "cfg-cleanup",
5043 ]
5044 );
5045 assert!(validate_intermediate_representation(output).is_empty());
5046 assert!(validate_effect_ir(&model, output).is_empty());
5047 }
5048
5049 #[test]
5050 fn lowers_state_backed_provider_once_for_shared_consumers() {
5051 let model = build_application_semantic_model(&presolve_parser::parse_file(
5052 "src/App.tsx",
5053 r#"
5054@component("x-app")
5055class App extends Component {
5056 selected: string = state("dark");
5057 @context()
5058 theme!: string;
5059 @provide(App.theme)
5060 providedTheme: string = this.selected;
5061 @consume(App.theme)
5062 first!: string;
5063 @consume(App.theme)
5064 second!: string;
5065 render() { return <main />; }
5066}
5067"#,
5068 ));
5069 let component = &model.components[0].id;
5070 let provider = ProviderId::for_component(component, "providedTheme");
5071 let first = ConsumerId::for_component(component, "first");
5072 let second = ConsumerId::for_component(component, "second");
5073 let ir = lower_components_to_ir(&model);
5074 let evaluation = ir
5075 .context_source_evaluation(&ContextValueSourceId::Provider(provider))
5076 .expect("planned Provider source evaluation");
5077 let function = ir
5078 .modules
5079 .iter()
5080 .flat_map(|module| &module.functions)
5081 .find(|function| function.id == *evaluation.function.as_semantic_id())
5082 .expect("Provider source function");
5083 assert!(function.blocks[0].instructions.iter().any(|instruction| {
5084 matches!(
5085 instruction.kind,
5086 IrInstructionKind::LoadStorage { ref storage }
5087 if storage == &IrStorageId::for_semantic_origin(&component.state_field("selected"))
5088 )
5089 }));
5090 assert!(function.blocks[0].instructions.iter().any(|instruction| {
5091 matches!(
5092 instruction.kind,
5093 IrInstructionKind::InitializeContextSlot { ref slot, ref value }
5094 if slot == &evaluation.slot && value == &evaluation.result
5095 )
5096 }));
5097 let first_binding = ir.context_consumer_binding(&first).expect("first load");
5098 let second_binding = ir.context_consumer_binding(&second).expect("second load");
5099 assert_eq!(first_binding.slot, evaluation.slot);
5100 assert_eq!(second_binding.slot, evaluation.slot);
5101 assert_ne!(first_binding.load.id, second_binding.load.id);
5102 assert!(matches!(
5103 first_binding.load.kind(),
5104 IrInstructionKind::LoadContextSlot { slot } if slot == evaluation.slot
5105 ));
5106 assert!(validate_context_ir(&model, &ir).is_empty());
5107 }
5108
5109 #[test]
5110 fn lowers_default_and_computed_provider_with_distinct_source_slots() {
5111 let model = build_application_semantic_model(&presolve_parser::parse_file(
5112 "src/App.tsx",
5113 r#"
5114@component("x-app")
5115class App extends Component {
5116 selected: string = state("dark");
5117 @computed()
5118 get derivedTheme(): string { return this.selected; }
5119 @context()
5120 theme!: string;
5121 @provide(App.theme)
5122 providedTheme: string = this.derivedTheme;
5123 @consume(App.theme)
5124 theme!: string;
5125 @context()
5126 locale: string = "en";
5127 @consume(App.locale)
5128 locale!: string;
5129 render() { return <main />; }
5130}
5131"#,
5132 ));
5133 let component = &model.components[0].id;
5134 let provider =
5135 ContextValueSourceId::Provider(ProviderId::for_component(component, "providedTheme"));
5136 let locale =
5137 ContextValueSourceId::ContextDefault(ContextId::for_component(component, "locale"));
5138 let ir = lower_components_to_ir(&model);
5139 let provider_evaluation = ir
5140 .context_source_evaluation(&provider)
5141 .expect("Provider IR");
5142 let default_evaluation = ir.context_source_evaluation(&locale).expect("default IR");
5143 assert_ne!(provider_evaluation.function, default_evaluation.function);
5144 assert_ne!(provider_evaluation.slot, default_evaluation.slot);
5145 assert!(!provider_evaluation.prerequisite_computed_batches.is_empty());
5146 let provider_function = ir
5147 .modules
5148 .iter()
5149 .flat_map(|module| &module.functions)
5150 .find(|function| function.id == *provider_evaluation.function.as_semantic_id())
5151 .expect("Provider function");
5152 assert!(provider_function.blocks[0]
5153 .instructions
5154 .iter()
5155 .any(|instruction| {
5156 matches!(instruction.kind, IrInstructionKind::LoadComputed { .. })
5157 }));
5158 let locale_consumer = ConsumerId::for_component(component, "locale");
5159 assert_eq!(
5160 ir.context_consumer_binding(&locale_consumer)
5161 .expect("default Consumer load")
5162 .slot,
5163 default_evaluation.slot
5164 );
5165 assert!(validate_context_ir(&model, &ir).is_empty());
5166 }
5167
5168 #[test]
5169 fn omits_unused_blocked_and_unavailable_context_ir() {
5170 let model = build_application_semantic_model(&presolve_parser::parse_file(
5171 "src/App.tsx",
5172 r#"
5173@component("x-app")
5174class App extends Component {
5175 @context()
5176 unused!: string;
5177 @provide(App.unused)
5178 unusedProvider: string = "unused";
5179 @context()
5180 broken!: number;
5181 @provide(App.broken)
5182 brokenProvider: string = "wrong";
5183 @consume(App.broken)
5184 brokenValue!: number;
5185 @context()
5186 missing!: string;
5187 @consume(App.missing)
5188 missingValue!: string;
5189 render() { return <main />; }
5190}
5191"#,
5192 ));
5193 let component = &model.components[0].id;
5194 let unused =
5195 ContextValueSourceId::Provider(ProviderId::for_component(component, "unusedProvider"));
5196 let broken =
5197 ContextValueSourceId::Provider(ProviderId::for_component(component, "brokenProvider"));
5198 let broken_consumer = ConsumerId::for_component(component, "brokenValue");
5199 assert!(model
5200 .context_evaluation_plan()
5201 .context_source_plan(&unused)
5202 .is_some());
5203 assert!(model
5204 .context_evaluation_plan()
5205 .context_source_plan(&broken)
5206 .is_some());
5207 let ir = lower_components_to_ir(&model);
5208 assert!(ir.context_source_evaluation(&unused).is_none());
5209 assert!(ir.context_source_evaluation(&broken).is_none());
5210 assert!(ir.context_consumer_binding(&broken_consumer).is_none());
5211 assert!(validate_context_ir(&model, &ir).is_empty());
5212 }
5213
5214 #[test]
5215 fn optimizes_context_sources_without_changing_slots_or_unrelated_ir() {
5216 let model = build_application_semantic_model(&presolve_parser::parse_file(
5217 "src/App.tsx",
5218 r#"
5219@component("x-app")
5220class App extends Component {
5221 @computed()
5222 get unrelated(): number { return 4 + 5; }
5223 @context()
5224 count!: number;
5225 @provide(App.count)
5226 providedCount: number = 1 + 2;
5227 @consume(App.count)
5228 first!: number;
5229 @consume(App.count)
5230 second!: number;
5231 render() { return <main />; }
5232}
5233"#,
5234 ));
5235 let component = &model.components[0].id;
5236 let source =
5237 ContextValueSourceId::Provider(ProviderId::for_component(component, "providedCount"));
5238 let unrelated = component.computed("unrelated");
5239 let input = lower_components_to_ir(&model);
5240 let original_unrelated = input.modules[0]
5241 .functions
5242 .iter()
5243 .find(|function| function.id == unrelated)
5244 .expect("unrelated computed function")
5245 .clone();
5246 let original_source = input
5247 .context_source_evaluation(&source)
5248 .expect("planned Context source")
5249 .clone();
5250
5251 let report = optimize_context_ir(&input);
5252 let optimized_source = report
5253 .optimized_module
5254 .modules
5255 .iter()
5256 .flat_map(|module| &module.functions)
5257 .find(|function| function.id == *original_source.function.as_semantic_id())
5258 .expect("optimized Context source function");
5259 let optimized_unrelated = report.optimized_module.modules[0]
5260 .functions
5261 .iter()
5262 .find(|function| function.id == unrelated)
5263 .expect("preserved unrelated computed function");
5264
5265 assert_eq!(input, lower_components_to_ir(&model));
5266 assert_eq!(optimized_unrelated, &original_unrelated);
5267 assert_eq!(
5268 report.source_evaluations,
5269 vec![super::OptimizedIrContextSourceEvaluation::from(
5270 &original_source
5271 )]
5272 );
5273 assert_eq!(
5274 report.optimized_module.context_ir.consumer_bindings,
5275 input.context_ir.consumer_bindings
5276 );
5277 assert_eq!(optimized_source.blocks[0].instructions.len(), 2);
5278 assert!(matches!(
5279 optimized_source.blocks[0].instructions[0].kind,
5280 IrInstructionKind::Constant {
5281 value: IrConstant::Number(ref value)
5282 } if value == "3"
5283 ));
5284 assert!(matches!(
5285 optimized_source.blocks[0].instructions[1].kind,
5286 IrInstructionKind::InitializeContextSlot { ref slot, ref value }
5287 if slot == &original_source.slot && value == &original_source.result
5288 ));
5289 assert_eq!(
5290 report
5291 .pass_metrics
5292 .iter()
5293 .map(|pass| pass.name)
5294 .collect::<Vec<_>>(),
5295 vec![
5296 "common-subexpression-elimination",
5297 "copy-propagation",
5298 "constant-folding",
5299 "instruction-simplification",
5300 "dead-code-elimination",
5301 "cfg-cleanup",
5302 ]
5303 );
5304 assert!(validate_optimized_context_ir(&model, &input, &report).is_empty());
5305 }
5306
5307 #[test]
5308 fn optimizes_computed_ir_immutably_and_preserves_evaluation_results() {
5309 let parsed = presolve_parser::parse_file(
5310 "src/OptimizedComputedIr.tsx",
5311 r#"
5312@component("x-optimized-computed-ir")
5313class OptimizedComputedIr extends Component {
5314 @computed()
5315 get total() { return 1 + 2; }
5316}
5317"#,
5318 );
5319 let model = crate::build_application_semantic_model(&parsed);
5320 let total = model.components[0].id.computed("total");
5321 let ir = lower_components_to_ir(&model);
5322 let original = ir.modules[0]
5323 .functions
5324 .iter()
5325 .find(|function| function.id == total)
5326 .expect("original computed function");
5327 assert_eq!(original.blocks[0].instructions.len(), 3);
5328 assert!(original.blocks[0].instructions.iter().any(|instruction| {
5329 matches!(
5330 instruction.kind,
5331 IrInstructionKind::Binary {
5332 operation: IrBinaryOperation::Add,
5333 ..
5334 }
5335 )
5336 }));
5337
5338 let report = optimize_computed_ir(&ir);
5339 let optimized_module = &report.output.modules[0];
5340 let evaluation = optimized_module
5341 .computed_evaluations
5342 .iter()
5343 .find(|evaluation| evaluation.computed == total)
5344 .expect("computed evaluation");
5345 let optimized = optimized_module
5346 .functions
5347 .iter()
5348 .find(|function| function.id == total)
5349 .expect("optimized computed function");
5350
5351 assert_eq!(
5352 report
5353 .passes
5354 .iter()
5355 .map(|pass| pass.name)
5356 .collect::<Vec<_>>(),
5357 vec![
5358 "common-subexpression-elimination",
5359 "copy-propagation",
5360 "constant-folding",
5361 "instruction-simplification",
5362 "dead-code-elimination",
5363 "cfg-cleanup",
5364 ]
5365 );
5366 assert_eq!(optimized.blocks[0].instructions.len(), 1);
5367 assert!(matches!(
5368 optimized.blocks[0].instructions[0].kind,
5369 IrInstructionKind::Constant {
5370 value: IrConstant::Number(ref value)
5371 } if value == "3"
5372 ));
5373 assert_eq!(optimized.values.len(), 1);
5374 assert!(optimized.values.contains_key(&evaluation.result));
5375 assert!(validate_intermediate_representation(&ir).is_empty());
5376 assert!(validate_intermediate_representation(&report.output).is_empty());
5377 }
5378
5379 #[test]
5380 fn validates_result_value_definitions_and_storage_references() {
5381 let parsed = presolve_parser::parse_file(
5382 "src/Counter.tsx",
5383 "@component(\"x-counter\") class Counter extends Component { count = state(0); increment() {} }",
5384 );
5385 let model = crate::build_application_semantic_model(&parsed);
5386 let mut representation = lower_components_to_ir(&model);
5387 assert!(validate_intermediate_representation(&representation).is_empty());
5388
5389 let storage = representation.modules[0].storages[0].id.clone();
5390 let function = &mut representation.modules[0].functions[0];
5391 let result = IrValueId::for_function(&function.id, 0);
5392 function.blocks[0].instructions.push(IrInstruction {
5393 id: IrInstructionId::for_block(&function.entry_block, 0),
5394 provenance: function.provenance.clone(),
5395 result: Some(result),
5396 semantic_origin: None,
5397 kind: IrInstructionKind::LoadStorage { storage },
5398 });
5399
5400 assert!(validate_intermediate_representation(&representation)
5401 .iter()
5402 .any(|diagnostic| diagnostic.code == "PSIR1006"));
5403 }
5404}