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#[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 #[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}