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                for (index, statement) in body.statements.iter().enumerate() {
199                    let path = format!("statement:{index}");
200                    match &statement.kind {
201                        EffectStatementSyntaxKind::StaticMemberAssignment { target, value } => {
202                            graph.insert_effect_expression(
203                                &effect,
204                                &format!("{path}/target"),
205                                target,
206                                provenance,
207                            );
208                            graph.insert_effect_expression(
209                                &effect,
210                                &format!("{path}/value"),
211                                value,
212                                provenance,
213                            );
214                        }
215                        EffectStatementSyntaxKind::CapabilityCall { callee, arguments } => {
216                            graph.insert_effect_expression(
217                                &effect,
218                                &format!("{path}/callee"),
219                                callee,
220                                provenance,
221                            );
222                            for (argument_index, argument) in arguments.iter().enumerate() {
223                                graph.insert_effect_expression(
224                                    &effect,
225                                    &format!("{path}/argument:{argument_index}"),
226                                    argument,
227                                    provenance,
228                                );
229                            }
230                        }
231                        EffectStatementSyntaxKind::EffectReturn { value: Some(value) } => {
232                            graph.insert_effect_expression(
233                                &effect,
234                                &format!("{path}/return"),
235                                value,
236                                provenance,
237                            );
238                        }
239                        EffectStatementSyntaxKind::EffectReturn { value: None }
240                        | EffectStatementSyntaxKind::Empty
241                        | EffectStatementSyntaxKind::Unsupported(_) => {}
242                    }
243                }
244            }
245        }
246        graph
247    }
248
249    #[must_use]
250    pub fn root_for(&self, owner: &SemanticId) -> Option<&SemanticId> {
251        self.roots.get(owner)
252    }
253
254    #[must_use]
255    pub fn node(&self, id: &SemanticId) -> Option<&ExpressionNode> {
256        self.nodes.get(id)
257    }
258
259    #[must_use]
260    pub fn nodes_for(&self, owner: &SemanticId) -> Vec<&ExpressionNode> {
261        self.nodes
262            .values()
263            .filter(|node| node.owner == *owner)
264            .collect()
265    }
266
267    #[must_use]
268    pub fn dependencies_of(&self, id: &SemanticId) -> Vec<&SemanticId> {
269        self.node(id)
270            .map_or_else(Vec::new, ExpressionNode::dependencies)
271    }
272
273    #[must_use]
274    pub fn dependents_of(&self, id: &SemanticId) -> Vec<&ExpressionNode> {
275        self.nodes
276            .values()
277            .filter(|node| node.dependencies().contains(&id))
278            .collect()
279    }
280
281    #[must_use]
282    pub fn owner_of(&self, id: &SemanticId) -> Option<&SemanticId> {
283        self.node(id).map(|node| &node.owner)
284    }
285
286    #[must_use]
287    pub fn provenance_of(&self, id: &SemanticId) -> Option<&SourceProvenance> {
288        self.node(id).map(|node| &node.provenance)
289    }
290
291    #[must_use]
292    pub fn nodes_in_file(&self, path: &Path) -> Vec<&ExpressionNode> {
293        self.nodes
294            .values()
295            .filter(|node| node.provenance.path == path)
296            .collect()
297    }
298
299    #[must_use]
300    pub fn nodes_at(&self, path: &Path, offset: usize) -> Vec<&ExpressionNode> {
301        self.nodes
302            .values()
303            .filter(|node| {
304                node.provenance.path == path
305                    && node.provenance.span.start <= offset
306                    && offset < node.provenance.span.end
307            })
308            .collect()
309    }
310
311    #[must_use]
312    pub fn evaluate(
313        &self,
314        owner: &SemanticId,
315    ) -> Option<Result<SerializableValue, ConstantEvaluationError>> {
316        Some(self.expression_for(owner)?.evaluate())
317    }
318
319    #[must_use]
320    pub fn render(&self, owner: &SemanticId) -> Option<String> {
321        self.expression_for(owner)
322            .map(|expression| expression.to_string())
323    }
324
325    fn insert_expression(
326        &mut self,
327        owner: &SemanticId,
328        path: &str,
329        expression: &ConstantExpression,
330        owner_provenance: &SourceProvenance,
331    ) -> SemanticId {
332        let id = owner.expression(path);
333        let child = |graph: &mut Self, child_path: &str, child: &ConstantExpression| {
334            graph.insert_expression(owner, child_path, child, owner_provenance)
335        };
336        let kind = match &expression.kind {
337            ConstantExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
338            ConstantExpressionKind::Boolean(value) => ExpressionNodeKind::Boolean(*value),
339            ConstantExpressionKind::Arithmetic(arithmetic) => {
340                return self.insert_arithmetic(owner, path, arithmetic, owner_provenance)
341            }
342            ConstantExpressionKind::Comparison {
343                left,
344                right,
345                operator,
346            } => ExpressionNodeKind::Comparison {
347                left: self.insert_arithmetic(owner, &format!("{path}.0"), left, owner_provenance),
348                right: self.insert_arithmetic(owner, &format!("{path}.1"), right, owner_provenance),
349                operator: *operator,
350            },
351            ConstantExpressionKind::Logical {
352                left,
353                right,
354                operator,
355            } => ExpressionNodeKind::Logical {
356                left: child(self, &format!("{path}.0"), left),
357                right: child(self, &format!("{path}.1"), right),
358                operator: *operator,
359            },
360            ConstantExpressionKind::NullishCoalescing { left, right } => {
361                ExpressionNodeKind::NullishCoalescing {
362                    left: child(self, &format!("{path}.0"), left),
363                    right: child(self, &format!("{path}.1"), right),
364                }
365            }
366            ConstantExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
367                operand: child(self, &format!("{path}.0"), operand),
368                operator: *operator,
369            },
370        };
371        self.nodes.insert(
372            id.clone(),
373            ExpressionNode {
374                id: id.clone(),
375                owner: owner.clone(),
376                kind,
377                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
378            },
379        );
380        id
381    }
382
383    fn insert_arithmetic(
384        &mut self,
385        owner: &SemanticId,
386        path: &str,
387        expression: &crate::ArithmeticExpression,
388        owner_provenance: &SourceProvenance,
389    ) -> SemanticId {
390        let id = owner.expression(path);
391        let kind = match &expression.kind {
392            crate::ArithmeticExpressionKind::Number(value) => {
393                ExpressionNodeKind::Literal(SerializableValue::Number(value.clone()))
394            }
395            crate::ArithmeticExpressionKind::Binary {
396                left,
397                right,
398                operator,
399            } => ExpressionNodeKind::Arithmetic {
400                left: self.insert_arithmetic(owner, &format!("{path}.0"), left, owner_provenance),
401                right: self.insert_arithmetic(owner, &format!("{path}.1"), right, owner_provenance),
402                operator: *operator,
403            },
404        };
405        self.nodes.insert(
406            id.clone(),
407            ExpressionNode {
408                id: id.clone(),
409                owner: owner.clone(),
410                kind,
411                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
412            },
413        );
414        id
415    }
416
417    fn insert_computed_expression(
418        &mut self,
419        owner: &SemanticId,
420        path: &str,
421        expression: &ComputedExpression,
422        owner_provenance: &SourceProvenance,
423    ) -> SemanticId {
424        let id = owner.expression(path);
425        let child = |graph: &mut Self, child_path: &str, child: &ComputedExpression| {
426            graph.insert_computed_expression(owner, child_path, child, owner_provenance)
427        };
428        let kind = match &expression.kind {
429            ComputedExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
430            ComputedExpressionKind::ThisMember(name) => {
431                ExpressionNodeKind::ThisMember { name: name.clone() }
432            }
433            ComputedExpressionKind::MemberAccess {
434                object,
435                property,
436                optional,
437            } => ExpressionNodeKind::MemberAccess {
438                object: child(self, &format!("{path}.0"), object),
439                property: property.clone(),
440                optional: *optional,
441            },
442            ComputedExpressionKind::IndexAccess { object, index } => {
443                ExpressionNodeKind::IndexAccess {
444                    object: child(self, &format!("{path}.0"), object),
445                    index: child(self, &format!("{path}.1"), index),
446                }
447            }
448            ComputedExpressionKind::Conditional {
449                condition,
450                when_true,
451                when_false,
452            } => ExpressionNodeKind::Conditional {
453                condition: child(self, &format!("{path}.0"), condition),
454                when_true: child(self, &format!("{path}.1"), when_true),
455                when_false: child(self, &format!("{path}.2"), when_false),
456            },
457            ComputedExpressionKind::Template {
458                quasis,
459                expressions,
460            } => ExpressionNodeKind::Template {
461                quasis: quasis.clone(),
462                expressions: expressions
463                    .iter()
464                    .enumerate()
465                    .map(|(index, expression)| child(self, &format!("{path}.{index}"), expression))
466                    .collect(),
467            },
468            ComputedExpressionKind::Call { callee, arguments } => ExpressionNodeKind::Call {
469                callee: callee.clone(),
470                arguments: arguments
471                    .iter()
472                    .enumerate()
473                    .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
474                    .collect(),
475            },
476            ComputedExpressionKind::BuiltinPureCall {
477                operation,
478                arguments,
479            } => ExpressionNodeKind::BuiltinPureCall {
480                operation: *operation,
481                arguments: arguments
482                    .iter()
483                    .enumerate()
484                    .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
485                    .collect(),
486            },
487            ComputedExpressionKind::SemanticPackagePureCall {
488                local_name: _,
489                package,
490                version,
491                integrity,
492                export,
493                runtime_module,
494                resume_policy,
495                operation,
496                arguments,
497            } => ExpressionNodeKind::SemanticPackagePureCall {
498                package: package.clone(),
499                version: version.clone(),
500                integrity: integrity.clone(),
501                export: export.clone(),
502                runtime_module: runtime_module.clone(),
503                resume_policy: resume_policy.clone(),
504                operation: *operation,
505                arguments: arguments
506                    .iter()
507                    .enumerate()
508                    .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
509                    .collect(),
510            },
511            ComputedExpressionKind::Arithmetic {
512                left,
513                right,
514                operator,
515            } => ExpressionNodeKind::Arithmetic {
516                left: child(self, &format!("{path}.0"), left),
517                right: child(self, &format!("{path}.1"), right),
518                operator: *operator,
519            },
520            ComputedExpressionKind::Comparison {
521                left,
522                right,
523                operator,
524            } => ExpressionNodeKind::Comparison {
525                left: child(self, &format!("{path}.0"), left),
526                right: child(self, &format!("{path}.1"), right),
527                operator: *operator,
528            },
529            ComputedExpressionKind::Logical {
530                left,
531                right,
532                operator,
533            } => ExpressionNodeKind::Logical {
534                left: child(self, &format!("{path}.0"), left),
535                right: child(self, &format!("{path}.1"), right),
536                operator: *operator,
537            },
538            ComputedExpressionKind::NullishCoalescing { left, right } => {
539                ExpressionNodeKind::NullishCoalescing {
540                    left: child(self, &format!("{path}.0"), left),
541                    right: child(self, &format!("{path}.1"), right),
542                }
543            }
544            ComputedExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
545                operand: child(self, &format!("{path}.0"), operand),
546                operator: *operator,
547            },
548        };
549        self.nodes.insert(
550            id.clone(),
551            ExpressionNode {
552                id: id.clone(),
553                owner: owner.clone(),
554                kind,
555                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
556            },
557        );
558        id
559    }
560
561    fn insert_effect_expression(
562        &mut self,
563        owner: &SemanticId,
564        path: &str,
565        expression: &EffectExpression,
566        owner_provenance: &SourceProvenance,
567    ) -> SemanticId {
568        let id = owner.expression(path);
569        let child = |graph: &mut Self, child_path: &str, child: &EffectExpression| {
570            graph.insert_effect_expression(owner, child_path, child, owner_provenance)
571        };
572        let kind = match &expression.kind {
573            EffectExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
574            EffectExpressionKind::Identifier(name) => ExpressionNodeKind::Identifier(name.clone()),
575            EffectExpressionKind::ThisMember(name) => {
576                ExpressionNodeKind::ThisMember { name: name.clone() }
577            }
578            EffectExpressionKind::MemberAccess { object, property } => {
579                ExpressionNodeKind::MemberAccess {
580                    object: child(self, &format!("{path}.0"), object),
581                    property: property.clone(),
582                    optional: false,
583                }
584            }
585            EffectExpressionKind::Arithmetic {
586                left,
587                right,
588                operator,
589            } => ExpressionNodeKind::Arithmetic {
590                left: child(self, &format!("{path}.0"), left),
591                right: child(self, &format!("{path}.1"), right),
592                operator: *operator,
593            },
594            EffectExpressionKind::Comparison {
595                left,
596                right,
597                operator,
598            } => ExpressionNodeKind::Comparison {
599                left: child(self, &format!("{path}.0"), left),
600                right: child(self, &format!("{path}.1"), right),
601                operator: *operator,
602            },
603            EffectExpressionKind::Logical {
604                left,
605                right,
606                operator,
607            } => ExpressionNodeKind::Logical {
608                left: child(self, &format!("{path}.0"), left),
609                right: child(self, &format!("{path}.1"), right),
610                operator: *operator,
611            },
612            EffectExpressionKind::NullishCoalescing { left, right } => {
613                ExpressionNodeKind::NullishCoalescing {
614                    left: child(self, &format!("{path}.0"), left),
615                    right: child(self, &format!("{path}.1"), right),
616                }
617            }
618            EffectExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
619                operand: child(self, &format!("{path}.0"), operand),
620                operator: *operator,
621            },
622        };
623        self.nodes.insert(
624            id.clone(),
625            ExpressionNode {
626                id: id.clone(),
627                owner: owner.clone(),
628                kind,
629                provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
630            },
631        );
632        id
633    }
634
635    fn expression_for(&self, owner: &SemanticId) -> Option<ConstantExpression> {
636        self.expression_from_node(self.root_for(owner)?)
637    }
638
639    fn expression_from_node(&self, id: &SemanticId) -> Option<ConstantExpression> {
640        let node = self.nodes.get(id)?;
641        let kind = match &node.kind {
642            ExpressionNodeKind::Literal(value) => ConstantExpressionKind::Literal(value.clone()),
643            ExpressionNodeKind::Boolean(value) => ConstantExpressionKind::Boolean(*value),
644            ExpressionNodeKind::Identifier(_)
645            | ExpressionNodeKind::ThisMember { .. }
646            | ExpressionNodeKind::MemberAccess { .. }
647            | ExpressionNodeKind::IndexAccess { .. }
648            | ExpressionNodeKind::Conditional { .. }
649            | ExpressionNodeKind::Template { .. }
650            | ExpressionNodeKind::Call { .. }
651            | ExpressionNodeKind::BuiltinPureCall { .. }
652            | ExpressionNodeKind::SemanticPackagePureCall { .. } => {
653                return None;
654            }
655            ExpressionNodeKind::Arithmetic {
656                left,
657                right,
658                operator,
659            } => ConstantExpressionKind::Arithmetic(crate::ArithmeticExpression {
660                kind: crate::ArithmeticExpressionKind::Binary {
661                    operator: *operator,
662                    left: Box::new(self.arithmetic_from_node(left)?),
663                    right: Box::new(self.arithmetic_from_node(right)?),
664                },
665                span: node.provenance.span,
666            }),
667            ExpressionNodeKind::Comparison {
668                left,
669                right,
670                operator,
671            } => ConstantExpressionKind::Comparison {
672                operator: *operator,
673                left: self.arithmetic_from_node(left)?,
674                right: self.arithmetic_from_node(right)?,
675            },
676            ExpressionNodeKind::Logical {
677                left,
678                right,
679                operator,
680            } => ConstantExpressionKind::Logical {
681                operator: *operator,
682                left: Box::new(self.expression_from_node(left)?),
683                right: Box::new(self.expression_from_node(right)?),
684            },
685            ExpressionNodeKind::NullishCoalescing { left, right } => {
686                ConstantExpressionKind::NullishCoalescing {
687                    left: Box::new(self.expression_from_node(left)?),
688                    right: Box::new(self.expression_from_node(right)?),
689                }
690            }
691            ExpressionNodeKind::Unary { operand, operator } => ConstantExpressionKind::Unary {
692                operator: *operator,
693                operand: Box::new(self.expression_from_node(operand)?),
694            },
695        };
696        Some(ConstantExpression {
697            kind,
698            span: node.provenance.span,
699        })
700    }
701
702    fn arithmetic_from_node(&self, id: &SemanticId) -> Option<crate::ArithmeticExpression> {
703        let node = self.nodes.get(id)?;
704        let kind = match &node.kind {
705            ExpressionNodeKind::Literal(SerializableValue::Number(value)) => {
706                crate::ArithmeticExpressionKind::Number(value.clone())
707            }
708            ExpressionNodeKind::Arithmetic {
709                left,
710                right,
711                operator,
712            } => crate::ArithmeticExpressionKind::Binary {
713                operator: *operator,
714                left: Box::new(self.arithmetic_from_node(left)?),
715                right: Box::new(self.arithmetic_from_node(right)?),
716            },
717            _ => return None,
718        };
719        Some(crate::ArithmeticExpression {
720            kind,
721            span: node.provenance.span,
722        })
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use crate::component_graph::UnaryOperator;
729    use crate::{build_application_semantic_model, ExpressionNodeKind, SerializableValue};
730
731    #[test]
732    fn shares_one_canonical_graph_for_lowered_expression_nodes() {
733        let parsed = presolve_parser::parse_file(
734            "src/Graph.tsx",
735            r#"
736@component("x-graph")
737class Graph extends Component {
738  total = state((1 + 2) * 3);
739}
740"#,
741        );
742        let asm = build_application_semantic_model(&parsed);
743        let field = &asm.components[0].state_fields[0];
744        let root = asm
745            .expression_graph
746            .root_for(&field.id)
747            .expect("expression root");
748
749        assert_eq!(asm.expression_graph.nodes.len(), 5);
750        assert_eq!(
751            asm.expression_graph.evaluate(&field.id),
752            Some(Ok(SerializableValue::Number("9".to_string())))
753        );
754        assert_eq!(
755            asm.expression_graph.render(&field.id).as_deref(),
756            Some("((1 + 2) * 3)")
757        );
758        let root = asm
759            .expression_graph
760            .nodes
761            .get(root)
762            .expect("expression root node");
763        assert_eq!(root.provenance.path, std::path::Path::new("src/Graph.tsx"));
764        assert_eq!(root.provenance.span.line, 4);
765        assert!(asm.expression_graph.nodes.values().all(|node| {
766            node.provenance.path == std::path::Path::new("src/Graph.tsx")
767                && root.provenance.span.start <= node.provenance.span.start
768                && node.provenance.span.end <= root.provenance.span.end
769        }));
770    }
771
772    #[test]
773    fn lowers_supported_computed_getter_expressions_into_the_canonical_graph() {
774        let parsed = presolve_parser::parse_file(
775            "src/ComputedExpressions.tsx",
776            r#"
777@component("x-computed-expressions")
778class ComputedExpressions extends Component {
779  count = state(1);
780
781  @computed()
782  get doubled(): number { return this.count * 2; }
783
784  @computed()
785  get adjusted(): number { return +this.doubled - -1; }
786
787  @computed()
788  get visible(): boolean {
789    return ((this.doubled >= 2 && !this.profile.hidden) ?? false) || true;
790  }
791}
792"#,
793        );
794        let asm = build_application_semantic_model(&parsed);
795        let component = &asm.components[0];
796        let doubled = component.id.computed("doubled");
797        let visible = component.id.computed("visible");
798        assert!(asm.expression_graph.root_for(&doubled).is_some());
799        let root = asm
800            .expression_graph
801            .root_for(&visible)
802            .expect("computed expression root");
803
804        assert_eq!(root.as_str(), "module:src/ComputedExpressions.tsx/component:x-computed-expressions/computed:visible/expression:root");
805        assert!(asm
806            .expression_graph
807            .nodes_for(&visible)
808            .iter()
809            .all(|node| node.owner == visible));
810        assert!(asm.expression_graph.nodes.values().any(|node| {
811            matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "count")
812        }));
813        assert!(asm.expression_graph.nodes.values().any(|node| {
814            matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "doubled")
815        }));
816        assert!(asm.expression_graph.nodes.values().any(|node| {
817            matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "profile")
818        }));
819        assert!(asm.expression_graph.nodes.values().any(|node| {
820            matches!(&node.kind, ExpressionNodeKind::MemberAccess { property, .. } if property == "hidden")
821        }));
822        assert!(asm
823            .expression_graph
824            .nodes
825            .values()
826            .any(|node| { matches!(node.kind, ExpressionNodeKind::Arithmetic { .. }) }));
827        assert!(asm
828            .expression_graph
829            .nodes
830            .values()
831            .any(|node| { matches!(node.kind, ExpressionNodeKind::Comparison { .. }) }));
832        assert!(asm
833            .expression_graph
834            .nodes
835            .values()
836            .any(|node| { matches!(node.kind, ExpressionNodeKind::Logical { .. }) }));
837        assert!(asm
838            .expression_graph
839            .nodes
840            .values()
841            .any(|node| { matches!(node.kind, ExpressionNodeKind::NullishCoalescing { .. }) }));
842        let unary_operators = asm
843            .expression_graph
844            .nodes
845            .values()
846            .filter_map(|node| match node.kind {
847                ExpressionNodeKind::Unary { operator, .. } => Some(operator),
848                _ => None,
849            })
850            .collect::<Vec<_>>();
851        assert!(unary_operators.contains(&UnaryOperator::Not));
852        assert!(unary_operators.contains(&UnaryOperator::Plus));
853        assert!(unary_operators.contains(&UnaryOperator::Minus));
854        assert!(asm
855            .expression_graph
856            .nodes_for(&visible)
857            .iter()
858            .all(|node| asm.semantic_types.assignments.contains_key(&node.id)));
859        assert!(asm.references.iter().any(|reference| {
860            reference.kind == crate::SemanticReferenceKind::ComputedState
861                && reference.source == doubled
862        }));
863    }
864}