Skip to main content

presolve_compiler/
expression_graph.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::component_graph::{BuiltinPureOperation, UnaryOperator};
5use crate::{
6    ComponentNode, ComputedExpression, ComputedExpressionKind, ConstantEvaluationError,
7    ConstantExpression, ConstantExpressionKind, EffectExpression, EffectExpressionKind,
8    EffectStatementSyntaxKind, SemanticId, SerializableValue, SourceProvenance,
9};
10
11/// Canonical compiler-owned graph for all lowered state initializer and computed expressions.
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub struct ExpressionGraph {
14    pub roots: BTreeMap<SemanticId, SemanticId>,
15    pub nodes: BTreeMap<SemanticId, ExpressionNode>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ExpressionNode {
20    pub id: SemanticId,
21    pub owner: SemanticId,
22    pub kind: ExpressionNodeKind,
23    pub provenance: SourceProvenance,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ExpressionNodeKind {
28    Literal(SerializableValue),
29    Boolean(bool),
30    Identifier(String),
31    ThisMember {
32        name: String,
33    },
34    MemberAccess {
35        object: SemanticId,
36        property: String,
37        optional: bool,
38    },
39    IndexAccess {
40        object: SemanticId,
41        index: SemanticId,
42    },
43    Conditional {
44        condition: SemanticId,
45        when_true: SemanticId,
46        when_false: SemanticId,
47    },
48    Template {
49        quasis: Vec<String>,
50        expressions: Vec<SemanticId>,
51    },
52    Call {
53        callee: String,
54        arguments: Vec<SemanticId>,
55    },
56    BuiltinPureCall {
57        operation: BuiltinPureOperation,
58        arguments: Vec<SemanticId>,
59    },
60    SemanticPackagePureCall {
61        package: String,
62        version: String,
63        integrity: String,
64        export: String,
65        runtime_module: String,
66        resume_policy: String,
67        operation: crate::semantic_package::SemanticPackagePureOperation,
68        arguments: Vec<SemanticId>,
69    },
70    Arithmetic {
71        left: SemanticId,
72        right: SemanticId,
73        operator: crate::ArithmeticOperator,
74    },
75    Comparison {
76        left: SemanticId,
77        right: SemanticId,
78        operator: crate::ComparisonOperator,
79    },
80    Logical {
81        left: SemanticId,
82        right: SemanticId,
83        operator: crate::LogicalOperator,
84    },
85    NullishCoalescing {
86        left: SemanticId,
87        right: SemanticId,
88    },
89    Unary {
90        operand: SemanticId,
91        operator: UnaryOperator,
92    },
93}
94
95impl ExpressionNode {
96    #[must_use]
97    pub fn dependencies(&self) -> Vec<&SemanticId> {
98        match &self.kind {
99            ExpressionNodeKind::Literal(_)
100            | ExpressionNodeKind::Boolean(_)
101            | ExpressionNodeKind::Identifier(_)
102            | ExpressionNodeKind::ThisMember { .. } => Vec::new(),
103            ExpressionNodeKind::MemberAccess { object, .. } => vec![object],
104            ExpressionNodeKind::IndexAccess { object, index } => vec![object, index],
105            ExpressionNodeKind::Conditional {
106                condition,
107                when_true,
108                when_false,
109            } => vec![condition, when_true, when_false],
110            ExpressionNodeKind::Template { expressions, .. } => expressions.iter().collect(),
111            ExpressionNodeKind::Call { arguments, .. }
112            | ExpressionNodeKind::BuiltinPureCall { arguments, .. }
113            | ExpressionNodeKind::SemanticPackagePureCall { arguments, .. } => {
114                arguments.iter().collect()
115            }
116            ExpressionNodeKind::Arithmetic { left, right, .. }
117            | ExpressionNodeKind::Comparison { left, right, .. }
118            | ExpressionNodeKind::Logical { left, right, .. }
119            | ExpressionNodeKind::NullishCoalescing { left, right } => vec![left, right],
120            ExpressionNodeKind::Unary { operand, .. } => vec![operand],
121        }
122    }
123}
124
125impl ExpressionGraph {
126    /// # Panics
127    ///
128    /// Panics when an expression owner has no canonical source provenance.
129    #[allow(clippy::too_many_lines)]
130    #[must_use]
131    pub fn from_components(
132        components: &[ComponentNode],
133        provenance: &BTreeMap<SemanticId, SourceProvenance>,
134    ) -> Self {
135        let mut graph = Self::default();
136        for component in components {
137            for field in &component.state_fields {
138                let Some(expression) = &field.initial_expression else {
139                    continue;
140                };
141                let field_provenance = provenance
142                    .get(&field.id)
143                    .expect("state fields with expressions should have source provenance");
144                let root = graph.insert_expression(&field.id, "root", expression, field_provenance);
145                graph.roots.insert(field.id.clone(), root);
146            }
147            for context in &component.context_declarations {
148                let Some(expression) = &context.default_expression else {
149                    continue;
150                };
151                let context_id = component.id.context(&context.name);
152                let root = graph.insert_expression(
153                    &context_id,
154                    "default",
155                    expression,
156                    &context.provenance,
157                );
158                graph.roots.insert(context_id, root);
159            }
160            for provider in &component.provider_declarations {
161                let provider_id = component.id.provider(&provider.name);
162                let root = graph.insert_computed_expression(
163                    &provider_id,
164                    "value",
165                    &provider.value_expression,
166                    &provider.provenance,
167                );
168                graph.roots.insert(provider_id, root);
169            }
170            for method in component
171                .methods
172                .iter()
173                .filter(|method| method.is_computed())
174            {
175                let Some(expression) = &method.computed_expression else {
176                    continue;
177                };
178                let method_provenance = provenance
179                    .get(&method.id)
180                    .expect("computed methods with expressions should have source provenance");
181                let computed = component.id.computed(&method.name);
182                let root = graph.insert_computed_expression(
183                    &computed,
184                    "root",
185                    expression,
186                    method_provenance,
187                );
188                graph.roots.insert(computed, root);
189            }
190            for method in component.methods.iter().filter(|method| method.is_effect()) {
191                let Some(body) = &method.effect_body else {
192                    continue;
193                };
194                let effect = component.id.effect(&method.name);
195                let provenance = provenance
196                    .get(&method.id)
197                    .expect("effect methods should have canonical provenance");
198                insert_effect_body_expressions(&mut graph, &effect, body, provenance);
199            }
200            for field in &component.effect_fields {
201                let effect = component.id.effect(&field.name);
202                insert_effect_body_expressions(&mut graph, &effect, &field.body, &field.provenance);
203            }
204        }
205        graph
206    }
207
208    #[must_use]
209    pub fn root_for(&self, owner: &SemanticId) -> Option<&SemanticId> {
210        self.roots.get(owner)
211    }
212
213    #[must_use]
214    pub fn node(&self, id: &SemanticId) -> Option<&ExpressionNode> {
215        self.nodes.get(id)
216    }
217
218    #[must_use]
219    pub fn nodes_for(&self, owner: &SemanticId) -> Vec<&ExpressionNode> {
220        self.nodes
221            .values()
222            .filter(|node| node.owner == *owner)
223            .collect()
224    }
225
226    #[must_use]
227    pub fn dependencies_of(&self, id: &SemanticId) -> Vec<&SemanticId> {
228        self.node(id)
229            .map_or_else(Vec::new, ExpressionNode::dependencies)
230    }
231
232    #[must_use]
233    pub fn dependents_of(&self, id: &SemanticId) -> Vec<&ExpressionNode> {
234        self.nodes
235            .values()
236            .filter(|node| node.dependencies().contains(&id))
237            .collect()
238    }
239
240    #[must_use]
241    pub fn owner_of(&self, id: &SemanticId) -> Option<&SemanticId> {
242        self.node(id).map(|node| &node.owner)
243    }
244
245    #[must_use]
246    pub fn provenance_of(&self, id: &SemanticId) -> Option<&SourceProvenance> {
247        self.node(id).map(|node| &node.provenance)
248    }
249
250    #[must_use]
251    pub fn nodes_in_file(&self, path: &Path) -> Vec<&ExpressionNode> {
252        self.nodes
253            .values()
254            .filter(|node| node.provenance.path == path)
255            .collect()
256    }
257
258    #[must_use]
259    pub fn nodes_at(&self, path: &Path, offset: usize) -> Vec<&ExpressionNode> {
260        self.nodes
261            .values()
262            .filter(|node| {
263                node.provenance.path == path
264                    && node.provenance.span.start <= offset
265                    && offset < node.provenance.span.end
266            })
267            .collect()
268    }
269
270    #[must_use]
271    pub fn evaluate(
272        &self,
273        owner: &SemanticId,
274    ) -> Option<Result<SerializableValue, ConstantEvaluationError>> {
275        Some(self.expression_for(owner)?.evaluate())
276    }
277
278    #[must_use]
279    pub fn render(&self, owner: &SemanticId) -> Option<String> {
280        self.expression_for(owner)
281            .map(|expression| expression.to_string())
282    }
283
284    fn insert_expression(
285        &mut self,
286        owner: &SemanticId,
287        path: &str,
288        expression: &ConstantExpression,
289        owner_provenance: &SourceProvenance,
290    ) -> SemanticId {
291        let id = owner.expression(path);
292        let child = |graph: &mut Self, child_path: &str, child: &ConstantExpression| {
293            graph.insert_expression(owner, child_path, child, owner_provenance)
294        };
295        let kind = match &expression.kind {
296            ConstantExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
297            ConstantExpressionKind::Boolean(value) => ExpressionNodeKind::Boolean(*value),
298            ConstantExpressionKind::Arithmetic(arithmetic) => {
299                return self.insert_arithmetic(owner, path, arithmetic, owner_provenance)
300            }
301            ConstantExpressionKind::Comparison {
302                left,
303                right,
304                operator,
305            } => ExpressionNodeKind::Comparison {
306                left: self.insert_arithmetic(owner, &format!("{path}.0"), left, owner_provenance),
307                right: self.insert_arithmetic(owner, &format!("{path}.1"), right, owner_provenance),
308                operator: *operator,
309            },
310            ConstantExpressionKind::Logical {
311                left,
312                right,
313                operator,
314            } => ExpressionNodeKind::Logical {
315                left: child(self, &format!("{path}.0"), left),
316                right: child(self, &format!("{path}.1"), right),
317                operator: *operator,
318            },
319            ConstantExpressionKind::NullishCoalescing { left, right } => {
320                ExpressionNodeKind::NullishCoalescing {
321                    left: child(self, &format!("{path}.0"), left),
322                    right: child(self, &format!("{path}.1"), right),
323                }
324            }
325            ConstantExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
326                operand: child(self, &format!("{path}.0"), operand),
327                operator: *operator,
328            },
329        };
330        self.nodes.insert(
331            id.clone(),
332            ExpressionNode {
333                id: id.clone(),
334                owner: owner.clone(),
335                kind,
336                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
337            },
338        );
339        id
340    }
341
342    fn insert_arithmetic(
343        &mut self,
344        owner: &SemanticId,
345        path: &str,
346        expression: &crate::ArithmeticExpression,
347        owner_provenance: &SourceProvenance,
348    ) -> SemanticId {
349        let id = owner.expression(path);
350        let kind = match &expression.kind {
351            crate::ArithmeticExpressionKind::Number(value) => {
352                ExpressionNodeKind::Literal(SerializableValue::Number(value.clone()))
353            }
354            crate::ArithmeticExpressionKind::Binary {
355                left,
356                right,
357                operator,
358            } => ExpressionNodeKind::Arithmetic {
359                left: self.insert_arithmetic(owner, &format!("{path}.0"), left, owner_provenance),
360                right: self.insert_arithmetic(owner, &format!("{path}.1"), right, owner_provenance),
361                operator: *operator,
362            },
363        };
364        self.nodes.insert(
365            id.clone(),
366            ExpressionNode {
367                id: id.clone(),
368                owner: owner.clone(),
369                kind,
370                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
371            },
372        );
373        id
374    }
375
376    fn insert_computed_expression(
377        &mut self,
378        owner: &SemanticId,
379        path: &str,
380        expression: &ComputedExpression,
381        owner_provenance: &SourceProvenance,
382    ) -> SemanticId {
383        let id = owner.expression(path);
384        let child = |graph: &mut Self, child_path: &str, child: &ComputedExpression| {
385            graph.insert_computed_expression(owner, child_path, child, owner_provenance)
386        };
387        let kind = match &expression.kind {
388            ComputedExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
389            ComputedExpressionKind::ThisMember(name) => {
390                ExpressionNodeKind::ThisMember { name: name.clone() }
391            }
392            ComputedExpressionKind::MemberAccess {
393                object,
394                property,
395                optional,
396            } => ExpressionNodeKind::MemberAccess {
397                object: child(self, &format!("{path}.0"), object),
398                property: property.clone(),
399                optional: *optional,
400            },
401            ComputedExpressionKind::IndexAccess { object, index } => {
402                ExpressionNodeKind::IndexAccess {
403                    object: child(self, &format!("{path}.0"), object),
404                    index: child(self, &format!("{path}.1"), index),
405                }
406            }
407            ComputedExpressionKind::Conditional {
408                condition,
409                when_true,
410                when_false,
411            } => ExpressionNodeKind::Conditional {
412                condition: child(self, &format!("{path}.0"), condition),
413                when_true: child(self, &format!("{path}.1"), when_true),
414                when_false: child(self, &format!("{path}.2"), when_false),
415            },
416            ComputedExpressionKind::Template {
417                quasis,
418                expressions,
419            } => ExpressionNodeKind::Template {
420                quasis: quasis.clone(),
421                expressions: expressions
422                    .iter()
423                    .enumerate()
424                    .map(|(index, expression)| child(self, &format!("{path}.{index}"), expression))
425                    .collect(),
426            },
427            ComputedExpressionKind::Call { callee, arguments } => ExpressionNodeKind::Call {
428                callee: callee.clone(),
429                arguments: arguments
430                    .iter()
431                    .enumerate()
432                    .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
433                    .collect(),
434            },
435            ComputedExpressionKind::BuiltinPureCall {
436                operation,
437                arguments,
438            } => ExpressionNodeKind::BuiltinPureCall {
439                operation: *operation,
440                arguments: arguments
441                    .iter()
442                    .enumerate()
443                    .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
444                    .collect(),
445            },
446            ComputedExpressionKind::SemanticPackagePureCall {
447                local_name: _,
448                package,
449                version,
450                integrity,
451                export,
452                runtime_module,
453                resume_policy,
454                operation,
455                arguments,
456            } => ExpressionNodeKind::SemanticPackagePureCall {
457                package: package.clone(),
458                version: version.clone(),
459                integrity: integrity.clone(),
460                export: export.clone(),
461                runtime_module: runtime_module.clone(),
462                resume_policy: resume_policy.clone(),
463                operation: *operation,
464                arguments: arguments
465                    .iter()
466                    .enumerate()
467                    .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
468                    .collect(),
469            },
470            ComputedExpressionKind::Arithmetic {
471                left,
472                right,
473                operator,
474            } => ExpressionNodeKind::Arithmetic {
475                left: child(self, &format!("{path}.0"), left),
476                right: child(self, &format!("{path}.1"), right),
477                operator: *operator,
478            },
479            ComputedExpressionKind::Comparison {
480                left,
481                right,
482                operator,
483            } => ExpressionNodeKind::Comparison {
484                left: child(self, &format!("{path}.0"), left),
485                right: child(self, &format!("{path}.1"), right),
486                operator: *operator,
487            },
488            ComputedExpressionKind::Logical {
489                left,
490                right,
491                operator,
492            } => ExpressionNodeKind::Logical {
493                left: child(self, &format!("{path}.0"), left),
494                right: child(self, &format!("{path}.1"), right),
495                operator: *operator,
496            },
497            ComputedExpressionKind::NullishCoalescing { left, right } => {
498                ExpressionNodeKind::NullishCoalescing {
499                    left: child(self, &format!("{path}.0"), left),
500                    right: child(self, &format!("{path}.1"), right),
501                }
502            }
503            ComputedExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
504                operand: child(self, &format!("{path}.0"), operand),
505                operator: *operator,
506            },
507        };
508        self.nodes.insert(
509            id.clone(),
510            ExpressionNode {
511                id: id.clone(),
512                owner: owner.clone(),
513                kind,
514                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
515            },
516        );
517        id
518    }
519
520    fn insert_effect_expression(
521        &mut self,
522        owner: &SemanticId,
523        path: &str,
524        expression: &EffectExpression,
525        owner_provenance: &SourceProvenance,
526    ) -> SemanticId {
527        let id = owner.expression(path);
528        let child = |graph: &mut Self, child_path: &str, child: &EffectExpression| {
529            graph.insert_effect_expression(owner, child_path, child, owner_provenance)
530        };
531        let kind = match &expression.kind {
532            EffectExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
533            EffectExpressionKind::Identifier(name) => ExpressionNodeKind::Identifier(name.clone()),
534            EffectExpressionKind::ThisMember(name) => {
535                ExpressionNodeKind::ThisMember { name: name.clone() }
536            }
537            EffectExpressionKind::MemberAccess { object, property } => {
538                ExpressionNodeKind::MemberAccess {
539                    object: child(self, &format!("{path}.0"), object),
540                    property: property.clone(),
541                    optional: false,
542                }
543            }
544            EffectExpressionKind::Arithmetic {
545                left,
546                right,
547                operator,
548            } => ExpressionNodeKind::Arithmetic {
549                left: child(self, &format!("{path}.0"), left),
550                right: child(self, &format!("{path}.1"), right),
551                operator: *operator,
552            },
553            EffectExpressionKind::Comparison {
554                left,
555                right,
556                operator,
557            } => ExpressionNodeKind::Comparison {
558                left: child(self, &format!("{path}.0"), left),
559                right: child(self, &format!("{path}.1"), right),
560                operator: *operator,
561            },
562            EffectExpressionKind::Logical {
563                left,
564                right,
565                operator,
566            } => ExpressionNodeKind::Logical {
567                left: child(self, &format!("{path}.0"), left),
568                right: child(self, &format!("{path}.1"), right),
569                operator: *operator,
570            },
571            EffectExpressionKind::NullishCoalescing { left, right } => {
572                ExpressionNodeKind::NullishCoalescing {
573                    left: child(self, &format!("{path}.0"), left),
574                    right: child(self, &format!("{path}.1"), right),
575                }
576            }
577            EffectExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
578                operand: child(self, &format!("{path}.0"), operand),
579                operator: *operator,
580            },
581        };
582        self.nodes.insert(
583            id.clone(),
584            ExpressionNode {
585                id: id.clone(),
586                owner: owner.clone(),
587                kind,
588                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
589            },
590        );
591        id
592    }
593
594    fn expression_for(&self, owner: &SemanticId) -> Option<ConstantExpression> {
595        self.expression_from_node(self.root_for(owner)?)
596    }
597
598    fn expression_from_node(&self, id: &SemanticId) -> Option<ConstantExpression> {
599        let node = self.nodes.get(id)?;
600        let kind = match &node.kind {
601            ExpressionNodeKind::Literal(value) => ConstantExpressionKind::Literal(value.clone()),
602            ExpressionNodeKind::Boolean(value) => ConstantExpressionKind::Boolean(*value),
603            ExpressionNodeKind::Identifier(_)
604            | ExpressionNodeKind::ThisMember { .. }
605            | ExpressionNodeKind::MemberAccess { .. }
606            | ExpressionNodeKind::IndexAccess { .. }
607            | ExpressionNodeKind::Conditional { .. }
608            | ExpressionNodeKind::Template { .. }
609            | ExpressionNodeKind::Call { .. }
610            | ExpressionNodeKind::BuiltinPureCall { .. }
611            | ExpressionNodeKind::SemanticPackagePureCall { .. } => {
612                return None;
613            }
614            ExpressionNodeKind::Arithmetic {
615                left,
616                right,
617                operator,
618            } => ConstantExpressionKind::Arithmetic(crate::ArithmeticExpression {
619                kind: crate::ArithmeticExpressionKind::Binary {
620                    operator: *operator,
621                    left: Box::new(self.arithmetic_from_node(left)?),
622                    right: Box::new(self.arithmetic_from_node(right)?),
623                },
624                span: node.provenance.span,
625            }),
626            ExpressionNodeKind::Comparison {
627                left,
628                right,
629                operator,
630            } => ConstantExpressionKind::Comparison {
631                operator: *operator,
632                left: self.arithmetic_from_node(left)?,
633                right: self.arithmetic_from_node(right)?,
634            },
635            ExpressionNodeKind::Logical {
636                left,
637                right,
638                operator,
639            } => ConstantExpressionKind::Logical {
640                operator: *operator,
641                left: Box::new(self.expression_from_node(left)?),
642                right: Box::new(self.expression_from_node(right)?),
643            },
644            ExpressionNodeKind::NullishCoalescing { left, right } => {
645                ConstantExpressionKind::NullishCoalescing {
646                    left: Box::new(self.expression_from_node(left)?),
647                    right: Box::new(self.expression_from_node(right)?),
648                }
649            }
650            ExpressionNodeKind::Unary { operand, operator } => ConstantExpressionKind::Unary {
651                operator: *operator,
652                operand: Box::new(self.expression_from_node(operand)?),
653            },
654        };
655        Some(ConstantExpression {
656            kind,
657            span: node.provenance.span,
658        })
659    }
660
661    fn arithmetic_from_node(&self, id: &SemanticId) -> Option<crate::ArithmeticExpression> {
662        let node = self.nodes.get(id)?;
663        let kind = match &node.kind {
664            ExpressionNodeKind::Literal(SerializableValue::Number(value)) => {
665                crate::ArithmeticExpressionKind::Number(value.clone())
666            }
667            ExpressionNodeKind::Arithmetic {
668                left,
669                right,
670                operator,
671            } => crate::ArithmeticExpressionKind::Binary {
672                operator: *operator,
673                left: Box::new(self.arithmetic_from_node(left)?),
674                right: Box::new(self.arithmetic_from_node(right)?),
675            },
676            _ => return None,
677        };
678        Some(crate::ArithmeticExpression {
679            kind,
680            span: node.provenance.span,
681        })
682    }
683}
684
685fn insert_effect_body_expressions(
686    graph: &mut ExpressionGraph,
687    effect: &SemanticId,
688    body: &crate::EffectBodySyntax,
689    provenance: &SourceProvenance,
690) {
691    for (index, statement) in body.statements.iter().enumerate() {
692        let path = format!("statement:{index}");
693        match &statement.kind {
694            EffectStatementSyntaxKind::StaticMemberAssignment { target, value } => {
695                graph.insert_effect_expression(
696                    effect,
697                    &format!("{path}/target"),
698                    target,
699                    provenance,
700                );
701                graph.insert_effect_expression(effect, &format!("{path}/value"), value, provenance);
702            }
703            EffectStatementSyntaxKind::CapabilityCall { callee, arguments } => {
704                graph.insert_effect_expression(
705                    effect,
706                    &format!("{path}/callee"),
707                    callee,
708                    provenance,
709                );
710                for (argument_index, argument) in arguments.iter().enumerate() {
711                    graph.insert_effect_expression(
712                        effect,
713                        &format!("{path}/argument:{argument_index}"),
714                        argument,
715                        provenance,
716                    );
717                }
718            }
719            EffectStatementSyntaxKind::EffectReturn { value: Some(value) } => {
720                graph.insert_effect_expression(
721                    effect,
722                    &format!("{path}/return"),
723                    value,
724                    provenance,
725                );
726            }
727            EffectStatementSyntaxKind::EffectReturn { value: None }
728            | EffectStatementSyntaxKind::Empty
729            | EffectStatementSyntaxKind::Unsupported(_) => {}
730        }
731    }
732    if let Some(cleanup) = &body.cleanup {
733        for (index, statement) in cleanup.body.statements.iter().enumerate() {
734            let path = format!("cleanup/statement:{index}");
735            match &statement.kind {
736                EffectStatementSyntaxKind::StaticMemberAssignment { target, value } => {
737                    graph.insert_effect_expression(
738                        effect,
739                        &format!("{path}/target"),
740                        target,
741                        provenance,
742                    );
743                    graph.insert_effect_expression(
744                        effect,
745                        &format!("{path}/value"),
746                        value,
747                        provenance,
748                    );
749                }
750                EffectStatementSyntaxKind::CapabilityCall { callee, arguments } => {
751                    graph.insert_effect_expression(
752                        effect,
753                        &format!("{path}/callee"),
754                        callee,
755                        provenance,
756                    );
757                    for (argument_index, argument) in arguments.iter().enumerate() {
758                        graph.insert_effect_expression(
759                            effect,
760                            &format!("{path}/argument:{argument_index}"),
761                            argument,
762                            provenance,
763                        );
764                    }
765                }
766                EffectStatementSyntaxKind::EffectReturn { value: Some(value) } => {
767                    graph.insert_effect_expression(
768                        effect,
769                        &format!("{path}/return"),
770                        value,
771                        provenance,
772                    );
773                }
774                EffectStatementSyntaxKind::EffectReturn { value: None }
775                | EffectStatementSyntaxKind::Empty
776                | EffectStatementSyntaxKind::Unsupported(_) => {}
777            }
778        }
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use crate::component_graph::UnaryOperator;
785    use crate::{build_application_semantic_model, ExpressionNodeKind, SerializableValue};
786
787    #[test]
788    fn shares_one_canonical_graph_for_lowered_expression_nodes() {
789        let parsed = presolve_parser::parse_file(
790            "src/Graph.tsx",
791            r#"
792@component("x-graph")
793class Graph extends Component {
794  total = state((1 + 2) * 3);
795}
796"#,
797        );
798        let asm = build_application_semantic_model(&parsed);
799        let field = &asm.components[0].state_fields[0];
800        let root = asm
801            .expression_graph
802            .root_for(&field.id)
803            .expect("expression root");
804
805        assert_eq!(asm.expression_graph.nodes.len(), 5);
806        assert_eq!(
807            asm.expression_graph.evaluate(&field.id),
808            Some(Ok(SerializableValue::Number("9".to_string())))
809        );
810        assert_eq!(
811            asm.expression_graph.render(&field.id).as_deref(),
812            Some("((1 + 2) * 3)")
813        );
814        let root = asm
815            .expression_graph
816            .nodes
817            .get(root)
818            .expect("expression root node");
819        assert_eq!(root.provenance.path, std::path::Path::new("src/Graph.tsx"));
820        assert_eq!(root.provenance.span.line, 4);
821        assert!(asm.expression_graph.nodes.values().all(|node| {
822            node.provenance.path == std::path::Path::new("src/Graph.tsx")
823                && root.provenance.span.start <= node.provenance.span.start
824                && node.provenance.span.end <= root.provenance.span.end
825        }));
826    }
827
828    #[test]
829    fn lowers_supported_computed_getter_expressions_into_the_canonical_graph() {
830        let parsed = presolve_parser::parse_file(
831            "src/ComputedExpressions.tsx",
832            r#"
833@component("x-computed-expressions")
834class ComputedExpressions extends Component {
835  count = state(1);
836
837  @computed()
838  get doubled(): number { return this.count * 2; }
839
840  @computed()
841  get adjusted(): number { return +this.doubled - -1; }
842
843  @computed()
844  get visible(): boolean {
845    return ((this.doubled >= 2 && !this.profile.hidden) ?? false) || true;
846  }
847}
848"#,
849        );
850        let asm = build_application_semantic_model(&parsed);
851        let component = &asm.components[0];
852        let doubled = component.id.computed("doubled");
853        let visible = component.id.computed("visible");
854        assert!(asm.expression_graph.root_for(&doubled).is_some());
855        let root = asm
856            .expression_graph
857            .root_for(&visible)
858            .expect("computed expression root");
859
860        assert_eq!(root.as_str(), "module:src/ComputedExpressions.tsx/component:x-computed-expressions/computed:visible/expression:root");
861        assert!(asm
862            .expression_graph
863            .nodes_for(&visible)
864            .iter()
865            .all(|node| node.owner == visible));
866        assert!(asm.expression_graph.nodes.values().any(|node| {
867            matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "count")
868        }));
869        assert!(asm.expression_graph.nodes.values().any(|node| {
870            matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "doubled")
871        }));
872        assert!(asm.expression_graph.nodes.values().any(|node| {
873            matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "profile")
874        }));
875        assert!(asm.expression_graph.nodes.values().any(|node| {
876            matches!(&node.kind, ExpressionNodeKind::MemberAccess { property, .. } if property == "hidden")
877        }));
878        assert!(asm
879            .expression_graph
880            .nodes
881            .values()
882            .any(|node| { matches!(node.kind, ExpressionNodeKind::Arithmetic { .. }) }));
883        assert!(asm
884            .expression_graph
885            .nodes
886            .values()
887            .any(|node| { matches!(node.kind, ExpressionNodeKind::Comparison { .. }) }));
888        assert!(asm
889            .expression_graph
890            .nodes
891            .values()
892            .any(|node| { matches!(node.kind, ExpressionNodeKind::Logical { .. }) }));
893        assert!(asm
894            .expression_graph
895            .nodes
896            .values()
897            .any(|node| { matches!(node.kind, ExpressionNodeKind::NullishCoalescing { .. }) }));
898        let unary_operators = asm
899            .expression_graph
900            .nodes
901            .values()
902            .filter_map(|node| match node.kind {
903                ExpressionNodeKind::Unary { operator, .. } => Some(operator),
904                _ => None,
905            })
906            .collect::<Vec<_>>();
907        assert!(unary_operators.contains(&UnaryOperator::Not));
908        assert!(unary_operators.contains(&UnaryOperator::Plus));
909        assert!(unary_operators.contains(&UnaryOperator::Minus));
910        assert!(asm
911            .expression_graph
912            .nodes_for(&visible)
913            .iter()
914            .all(|node| asm.semantic_types.assignments.contains_key(&node.id)));
915        assert!(asm.references.iter().any(|reference| {
916            reference.kind == crate::SemanticReferenceKind::ComputedState
917                && reference.source == doubled
918        }));
919    }
920}