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