1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use miette::NamedSource;
5
6use crate::registry::declared_type::{IndexTypeRef, StructTypeRef};
7use crate::registry::resolve_types::{ExpectedFail, ExpectedFailKey, ExpectedFailKeyPart};
8use crate::syntax::dimension::Dimension;
9use crate::syntax::names::{
10 IndexName, IndexVariantName, ResolvedName, ScopedName, StructTypeName, namespace,
11};
12
13use crate::registry::builtins::builtin_functions;
14use crate::registry::error::GraphcalError;
15use crate::registry::time_scale::TimeScale;
16use crate::registry::types::Registry;
17use crate::tir::typed::{NatLinearForm, NatRangeIndexIdentity};
18
19pub(crate) use helpers::{expect_scalar, format_inferred_type};
20
21use helpers::{format_declared_type, is_bool_type, resolved_type_matches_inferred, types_match};
22
23mod builtins;
24mod helpers;
25#[expect(
26 clippy::too_many_arguments,
27 clippy::too_many_lines,
28 reason = "inference functions pass compilation context through many parameters; \
29 large match on ExprKind variants is inherently long"
30)]
31mod infer;
32mod plot;
33#[cfg(test)]
34mod tests;
35
36pub use crate::registry::declared_type::DeclaredType;
37
38#[derive(Debug, Clone, Eq)]
44pub struct InferredIndex {
45 reference: IndexTypeRef,
46}
47
48impl InferredIndex {
49 #[must_use]
50 pub fn with_owner(owner: crate::dag_id::DagId, name: IndexName) -> Self {
51 Self::from_ref(IndexTypeRef::with_owner(owner, name))
52 }
53
54 #[must_use]
55 pub fn from_resolved(resolved: ResolvedName<namespace::Index>) -> Self {
56 Self {
57 reference: IndexTypeRef::from_resolved(resolved),
58 }
59 }
60
61 #[must_use]
62 pub const fn from_ref(reference: IndexTypeRef) -> Self {
63 Self { reference }
64 }
65
66 pub fn from_nat_range_identity(
72 identity: &NatRangeIndexIdentity,
73 ) -> Result<Self, crate::registry::types::NatRangeIndexError> {
74 Ok(Self {
75 reference: identity.to_index_type_ref()?,
76 })
77 }
78
79 pub fn from_nat_range_form(
85 form: NatLinearForm,
86 ) -> Result<Self, crate::registry::types::NatRangeIndexError> {
87 Self::from_nat_range_identity(&NatRangeIndexIdentity::try_from_form(form)?)
88 }
89
90 #[must_use]
91 pub const fn type_ref(&self) -> &IndexTypeRef {
92 &self.reference
93 }
94
95 #[must_use]
96 pub fn name(&self) -> IndexName {
97 self.reference.display_name()
98 }
99
100 #[must_use]
101 pub const fn declared_resolved(&self) -> Option<&ResolvedName<namespace::Index>> {
102 self.reference.declared_resolved()
103 }
104
105 #[must_use]
106 pub const fn concrete_nat_range(&self) -> Option<crate::registry::types::NatRangeIndex> {
107 self.reference.nat_range()
108 }
109
110 #[must_use]
111 pub fn nat_range_form(&self) -> Option<NatLinearForm> {
112 self.reference.nat_range_form()
113 }
114
115 #[must_use]
116 pub fn matches_resolved(&self, expected: &ResolvedName<namespace::Index>) -> bool {
117 self.declared_resolved() == Some(expected)
118 }
119
120 #[must_use]
121 pub fn matches_ref(&self, expected: &IndexTypeRef) -> bool {
122 self.reference.matches_ref(expected)
123 }
124}
125
126impl PartialEq for InferredIndex {
127 fn eq(&self, other: &Self) -> bool {
128 self.reference.matches_ref(&other.reference)
129 }
130}
131
132impl std::fmt::Display for InferredIndex {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 self.reference.fmt(f)
135 }
136}
137
138#[derive(Debug, Clone, Eq)]
143pub struct InferredStructType {
144 reference: StructTypeRef,
145}
146
147impl InferredStructType {
148 #[must_use]
149 pub fn with_owner(owner: crate::dag_id::DagId, name: StructTypeName) -> Self {
150 Self {
151 reference: StructTypeRef::with_owner(owner, name),
152 }
153 }
154
155 #[must_use]
156 pub fn from_resolved(resolved: ResolvedName<namespace::StructType>) -> Self {
157 Self {
158 reference: StructTypeRef::from_resolved(resolved),
159 }
160 }
161
162 #[must_use]
163 pub const fn from_ref(reference: StructTypeRef) -> Self {
164 Self { reference }
165 }
166
167 #[must_use]
168 pub const fn type_ref(&self) -> &StructTypeRef {
169 &self.reference
170 }
171
172 #[must_use]
173 pub const fn name(&self) -> &StructTypeName {
174 self.reference.name()
175 }
176
177 #[must_use]
178 pub const fn resolved(&self) -> &ResolvedName<namespace::StructType> {
179 self.reference.resolved()
180 }
181
182 #[must_use]
183 pub fn matches_resolved(&self, expected: &ResolvedName<namespace::StructType>) -> bool {
184 self.resolved() == expected
185 }
186
187 #[must_use]
188 pub fn matches_ref(&self, expected: &StructTypeRef) -> bool {
189 self.reference.matches_ref(expected)
190 }
191}
192
193impl PartialEq for InferredStructType {
194 fn eq(&self, other: &Self) -> bool {
195 self.reference.matches_ref(&other.reference)
196 }
197}
198
199impl std::fmt::Display for InferredStructType {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 self.reference.fmt(f)
202 }
203}
204
205impl std::ops::Deref for InferredStructType {
206 type Target = StructTypeName;
207
208 fn deref(&self) -> &Self::Target {
209 self.name()
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum InferredType {
216 Scalar(Dimension),
217 Bool,
218 Int,
219 Fin(NatLinearForm),
227 Datetime(TimeScale),
229 NamedIndex(InferredIndex),
234 Struct(InferredStructType, Vec<Self>),
236 Indexed {
237 element: Box<Self>,
238 index: InferredIndex,
239 },
240}
241
242impl InferredType {
243 #[must_use]
245 pub const fn is_int_like(&self) -> bool {
246 matches!(self, Self::Int | Self::Fin(_))
247 }
248}
249
250#[derive(Clone, Copy)]
257struct DimCheckContext<'a> {
258 declared_types: &'a HashMap<ScopedName, DeclaredType>,
259 dag: Option<&'a crate::tir::typed::DagTIR>,
260 tir: &'a crate::tir::typed::TIR,
261 registry: &'a Registry,
262 builtin_fns: &'a HashMap<&'a str, crate::registry::builtins::BuiltinFunction>,
263 src: &'a NamedSource<Arc<String>>,
264}
265
266impl<'a> DimCheckContext<'a> {
267 const fn for_body(self, body_src: &'a NamedSource<Arc<String>>) -> Self {
273 Self {
274 src: body_src,
275 ..self
276 }
277 }
278}
279
280impl DimCheckContext<'_> {
281 fn hir_expr_for_decl(
283 &self,
284 name: &crate::syntax::names::ScopedName,
285 ) -> Option<&crate::hir::Expr> {
286 let dag = self.dag?;
287 let key = dag.resolved_decl_key_for_local(name)?;
288 dag.semantic
289 .expressions
290 .consts
291 .get(&key)
292 .or_else(|| dag.semantic.expressions.runtime_expr(&key))
293 }
294
295 fn hir_assert_body(
297 &self,
298 name: &crate::syntax::names::ScopedName,
299 span: crate::syntax::span::Span,
300 ) -> Result<&crate::hir::AssertBody, GraphcalError> {
301 let dag = self.dag.ok_or_else(|| GraphcalError::InternalError {
302 message: "HIR assertion lookup requires semantic DAG context".to_string(),
303 src: self.src.clone(),
304 span: span.into(),
305 })?;
306 let key =
307 dag.resolved_decl_key_for_local(name)
308 .ok_or_else(|| GraphcalError::InternalError {
309 message: format!("semantic declaration key missing for assertion `{name}`"),
310 src: self.src.clone(),
311 span: span.into(),
312 })?;
313 dag.semantic
314 .expressions
315 .asserts
316 .get(&key)
317 .ok_or_else(|| GraphcalError::InternalError {
318 message: format!("semantic HIR body missing for assertion `{name}`"),
319 src: self.src.clone(),
320 span: span.into(),
321 })
322 }
323
324 fn infer_hir(&self, expr: &crate::hir::Expr) -> Result<InferredType, GraphcalError> {
326 let dag = self.dag.ok_or_else(|| GraphcalError::InternalError {
327 message: "HIR assertion inference requires semantic DAG context".to_string(),
328 src: self.src.clone(),
329 span: expr.span.into(),
330 })?;
331 infer::hir::infer_hir_type_with_owner(
332 expr,
333 None,
334 self.declared_types,
335 dag,
336 self.tir,
337 self.registry,
338 self.builtin_fns,
339 self.src,
340 )
341 }
342}
343
344fn check_decl_expr_type(
346 ctx: &DimCheckContext<'_>,
347 name: &crate::syntax::names::ScopedName,
348 type_ann_span: &crate::syntax::span::Span,
349) -> Result<(), GraphcalError> {
350 let declared = ctx
351 .declared_types
352 .get(name)
353 .ok_or_else(|| GraphcalError::InternalError {
354 message: format!("no declared type recorded for `{name}`"),
355 src: ctx.src.clone(),
356 span: (*type_ann_span).into(),
357 })?;
358 let dag = ctx.dag.ok_or_else(|| GraphcalError::InternalError {
359 message: format!("semantic DAG missing while checking `{name}`"),
360 src: ctx.src.clone(),
361 span: (*type_ann_span).into(),
362 })?;
363 let hir_expr = ctx
364 .hir_expr_for_decl(name)
365 .ok_or_else(|| GraphcalError::InternalError {
366 message: format!("semantic HIR expression missing for declaration `{name}`"),
367 src: ctx.src.clone(),
368 span: (*type_ann_span).into(),
369 })?;
370 let inferred = infer::hir::infer_hir_type_with_owner(
371 hir_expr,
372 Some(name.member()),
373 ctx.declared_types,
374 dag,
375 ctx.tir,
376 ctx.registry,
377 ctx.builtin_fns,
378 ctx.src,
379 )?;
380 let matches = ctx
381 .dag
382 .and_then(|dag| dag.resolved_decl_types.get(name))
383 .map_or_else(
384 || types_match(declared, &inferred),
385 |resolved| resolved_type_matches_inferred(resolved, &inferred),
386 );
387 if !matches {
388 return Err(GraphcalError::DimensionMismatchInAnnotation {
389 declared: format_declared_type(declared, ctx.registry),
390 inferred: format_inferred_type(&inferred, ctx.registry),
391 src: ctx.src.clone(),
392 span: (*type_ann_span).into(),
393 });
394 }
395 check_ineffective_conversions(hir_expr, true, ctx.src)?;
396 Ok(())
397}
398
399fn check_ineffective_conversions(
409 expr: &crate::hir::Expr,
410 display_position: bool,
411 src: &NamedSource<Arc<String>>,
412) -> Result<(), GraphcalError> {
413 crate::stack::with_stack_growth(|| {
415 check_ineffective_conversions_inner(expr, display_position, src)
416 })
417}
418
419fn check_ineffective_conversions_inner(
420 expr: &crate::hir::Expr,
421 display_position: bool,
422 src: &NamedSource<Arc<String>>,
423) -> Result<(), GraphcalError> {
424 use crate::hir::ExprKind;
425 match &expr.kind {
426 ExprKind::Convert { expr: inner, .. } | ExprKind::DisplayTimezone { expr: inner, .. } => {
427 if !display_position {
428 return Err(GraphcalError::IneffectiveConversion {
429 src: src.clone(),
430 span: expr.span.into(),
431 });
432 }
433 check_ineffective_conversions(inner, false, src)
436 }
437 ExprKind::If {
438 condition,
439 then_branch,
440 else_branch,
441 } => {
442 check_ineffective_conversions(condition, false, src)?;
443 check_ineffective_conversions(then_branch, display_position, src)?;
444 check_ineffective_conversions(else_branch, display_position, src)
445 }
446 ExprKind::Match { scrutinee, arms } => {
447 check_ineffective_conversions(scrutinee, false, src)?;
448 for arm in arms {
449 check_ineffective_conversions(&arm.body, display_position, src)?;
450 }
451 Ok(())
452 }
453 ExprKind::ConstructorCall { fields, .. } => {
454 for init in fields {
455 check_ineffective_conversions(&init.value, display_position, src)?;
456 }
457 Ok(())
458 }
459 ExprKind::MapLiteral { entries } => {
460 for entry in entries {
461 check_ineffective_conversions(&entry.value, display_position, src)?;
462 }
463 Ok(())
464 }
465 ExprKind::ForComp { body, .. } => {
466 check_ineffective_conversions(body, display_position, src)
467 }
468 ExprKind::Scan {
469 source, init, body, ..
470 } => {
471 check_ineffective_conversions(source, false, src)?;
472 check_ineffective_conversions(init, display_position, src)?;
473 check_ineffective_conversions(body, false, src)
474 }
475 ExprKind::Unfold { init, body, .. } => {
476 check_ineffective_conversions(init, display_position, src)?;
477 check_ineffective_conversions(body, false, src)
478 }
479 ExprKind::BinOp { lhs, rhs, .. } => {
480 check_ineffective_conversions(lhs, false, src)?;
481 check_ineffective_conversions(rhs, false, src)
482 }
483 ExprKind::UnaryOp { operand, .. } => check_ineffective_conversions(operand, false, src),
484 ExprKind::FnCall { args, .. } => {
485 for arg in args {
486 check_ineffective_conversions(arg, false, src)?;
487 }
488 Ok(())
489 }
490 ExprKind::FieldAccess { expr: inner, .. } => {
491 check_ineffective_conversions(inner, false, src)
492 }
493 ExprKind::IndexAccess { expr: inner, args } => {
494 check_ineffective_conversions(inner, false, src)?;
495 for arg in args {
496 if let crate::hir::expr::IndexArg::Expr(e) = arg {
497 check_ineffective_conversions(e, false, src)?;
498 }
499 }
500 Ok(())
501 }
502 ExprKind::InlineDagRef { args, .. } => {
505 for binding in args {
506 check_ineffective_conversions(&binding.value, display_position, src)?;
507 }
508 Ok(())
509 }
510 ExprKind::Error
511 | ExprKind::Number(_)
512 | ExprKind::Integer(_)
513 | ExprKind::Bool(_)
514 | ExprKind::StringLiteral(_)
515 | ExprKind::TypeSystemRef(_)
516 | ExprKind::GraphRef(_)
517 | ExprKind::ConstRef(_)
518 | ExprKind::LocalRef(_)
519 | ExprKind::UnitLiteral { .. }
520 | ExprKind::VariantLiteral(_) => Ok(()),
521 }
522}
523
524#[derive(Debug)]
525struct AssertionIndexShape {
526 axes: Vec<InferredIndex>,
527}
528
529impl AssertionIndexShape {
530 fn from_bool_type(ty: &InferredType) -> Self {
531 Self {
532 axes: peel_index_axes(ty).0,
533 }
534 }
535
536 const fn is_indexed(&self) -> bool {
537 !self.axes.is_empty()
538 }
539
540 const fn rank(&self) -> usize {
541 self.axes.len()
542 }
543}
544
545fn check_hir_assert_body(
547 ctx: &DimCheckContext<'_>,
548 body: &crate::hir::AssertBody,
549 span: crate::syntax::span::Span,
550) -> Result<AssertionIndexShape, GraphcalError> {
551 let registry = ctx.registry;
552 let src = ctx.src;
553 match body {
554 crate::hir::AssertBody::Expr(body_expr) => {
555 let inferred = ctx.infer_hir(body_expr)?;
556 if !is_bool_type(&inferred) {
557 return Err(GraphcalError::AssertBodyNotBool {
558 found: format_inferred_type(&inferred, registry),
559 src: src.clone(),
560 span: span.into(),
561 });
562 }
563 Ok(AssertionIndexShape::from_bool_type(&inferred))
564 }
565 crate::hir::AssertBody::Tolerance {
566 actual,
567 expected,
568 tolerance,
569 is_relative,
570 } => {
571 let actual_type = ctx.infer_hir(actual)?;
572 let expected_type = ctx.infer_hir(expected)?;
573 let tolerance_type = ctx.infer_hir(tolerance)?;
574
575 let (actual_axes, actual_elem) = peel_index_axes(&actual_type);
579 let expected_elem = broadcast_operand_element(
580 &actual_axes,
581 &actual_type,
582 &expected_type,
583 expected.span,
584 registry,
585 src,
586 )?;
587 let tolerance_elem = broadcast_operand_element(
588 &actual_axes,
589 &actual_type,
590 &tolerance_type,
591 tolerance.span,
592 registry,
593 src,
594 )?;
595
596 let actual_dim = expect_scalar(actual_elem, registry, src, actual.span)?;
597 let expected_dim = expect_scalar(expected_elem, registry, src, expected.span)?;
598 if actual_dim != expected_dim {
599 return Err(GraphcalError::DimensionMismatch {
600 expected: registry.dimensions.format_dimension(&actual_dim),
601 found: registry.dimensions.format_dimension(&expected_dim),
602 help: "actual and expected in tolerance assertion must have the same dimension"
603 .to_string(),
604 src: src.clone(),
605 span: expected.span.into(),
606 });
607 }
608
609 let tolerance_ok = if *is_relative {
610 tolerance_elem.is_int_like()
611 || matches!(tolerance_elem, InferredType::Scalar(d) if d.is_dimensionless())
612 } else {
613 let tolerance_dim = expect_scalar(tolerance_elem, registry, src, tolerance.span)?;
614 tolerance_dim == actual_dim
615 };
616 if !tolerance_ok {
617 let (expected_str, help_str) = if *is_relative {
618 (
619 "Dimensionless".to_string(),
620 "relative tolerance (%) must be dimensionless".to_string(),
621 )
622 } else {
623 (
624 registry.dimensions.format_dimension(&actual_dim),
625 "absolute tolerance must have the same dimension as actual/expected"
626 .to_string(),
627 )
628 };
629 return Err(GraphcalError::DimensionMismatch {
630 expected: expected_str,
631 found: format_inferred_type(&tolerance_type, registry),
632 help: help_str,
633 src: src.clone(),
634 span: tolerance.span.into(),
635 });
636 }
637
638 if let Some(value) = statically_known_tolerance(tolerance)
643 && value < 0.0
644 {
645 return Err(GraphcalError::NegativeTolerance {
646 found: crate::registry::format::format_number(value),
647 src: src.clone(),
648 span: tolerance.span.into(),
649 });
650 }
651 Ok(AssertionIndexShape { axes: actual_axes })
652 }
653 }
654}
655
656fn peel_index_axes(ty: &InferredType) -> (Vec<InferredIndex>, &InferredType) {
658 let mut axes = Vec::new();
659 let mut current = ty;
660 while let InferredType::Indexed { element, index } = current {
661 axes.push(index.clone());
662 current = element;
663 }
664 (axes, current)
665}
666
667fn broadcast_operand_element<'a>(
672 actual_axes: &[InferredIndex],
673 actual_type: &InferredType,
674 operand_type: &'a InferredType,
675 operand_span: crate::syntax::span::Span,
676 registry: &Registry,
677 src: &NamedSource<Arc<String>>,
678) -> Result<&'a InferredType, GraphcalError> {
679 let (operand_axes, operand_elem) = peel_index_axes(operand_type);
680 if !operand_axes.is_empty() && operand_axes != *actual_axes {
681 return Err(GraphcalError::IndexedShapeMismatch {
682 context: "tolerance assertion".to_string(),
683 lhs: format_inferred_type(actual_type, registry),
684 rhs: format_inferred_type(operand_type, registry),
685 src: src.clone(),
686 span: operand_span.into(),
687 });
688 }
689 Ok(operand_elem)
690}
691
692fn statically_known_tolerance(expr: &crate::hir::Expr) -> Option<f64> {
698 match &expr.kind {
699 crate::hir::ExprKind::Number(n) => Some(*n),
700 #[expect(
701 clippy::cast_precision_loss,
702 reason = "tolerance literals are small integers"
703 )]
704 crate::hir::ExprKind::Integer(i) => Some(*i as f64),
705 crate::hir::ExprKind::UnitLiteral { value, .. } => Some(*value),
706 crate::hir::ExprKind::UnaryOp {
707 op: crate::syntax::ast::UnaryOp::Neg,
708 operand,
709 } => statically_known_tolerance(operand).map(|v| -v),
710 _ => None,
711 }
712}
713
714fn expected_fail_key_span(key: &ExpectedFailKey) -> crate::syntax::span::Span {
715 key.iter()
716 .map(ExpectedFailKeyPart::span)
717 .reduce(crate::syntax::span::Span::merge)
718 .unwrap_or_else(|| crate::syntax::span::Span::new(0, 0))
719}
720
721fn expected_fail_key_signature(
722 key: &ExpectedFailKey,
723) -> Vec<(Option<IndexTypeRef>, IndexVariantName)> {
724 key.iter()
725 .map(|part| (part.named_index().cloned(), part.variant()))
726 .collect()
727}
728
729fn validate_expected_fail_key(
730 key: &ExpectedFailKey,
731 shape: &AssertionIndexShape,
732 src: &NamedSource<Arc<String>>,
733) -> Result<(), GraphcalError> {
734 if key.len() != shape.rank() {
735 return Err(GraphcalError::ExpectedFailKeyShapeMismatch {
736 expected: shape.rank(),
737 found: key.len(),
738 src: src.clone(),
739 span: expected_fail_key_span(key).into(),
740 });
741 }
742
743 for (part, expected_axis) in key.iter().zip(&shape.axes) {
744 match part {
745 ExpectedFailKeyPart::Named { index, .. } => {
746 if !index.matches_ref(expected_axis.type_ref()) {
747 return Err(GraphcalError::ExpectedFailKeyIndexMismatch {
748 expected: expected_axis.name().to_string(),
749 found: part.display(),
750 src: src.clone(),
751 span: part.span().into(),
752 });
753 }
754 }
755 ExpectedFailKeyPart::RangeStep { step, span } => {
756 let Some(range) = expected_axis.type_ref().nat_range_ref() else {
757 return Err(GraphcalError::ExpectedFailKeyIndexMismatch {
758 expected: expected_axis.name().to_string(),
759 found: part.display(),
760 src: src.clone(),
761 span: (*span).into(),
762 });
763 };
764 if let Some(concrete) = range.concrete_index()
769 && *step >= concrete.size_u64()
770 {
771 return Err(GraphcalError::ExpectedFailRangeStepOutOfBounds {
772 step: *step,
773 size: concrete.size_u64(),
774 src: src.clone(),
775 span: (*span).into(),
776 });
777 }
778 }
779 }
780 }
781
782 Ok(())
783}
784
785fn validate_expected_fail(
786 expected_fail: &ExpectedFail,
787 shape: &AssertionIndexShape,
788 src: &NamedSource<Arc<String>>,
789 assert_span: crate::syntax::span::Span,
790) -> Result<(), GraphcalError> {
791 match expected_fail {
792 ExpectedFail::All if shape.is_indexed() => Err(GraphcalError::ExpectedFailAllOnIndexed {
793 src: src.clone(),
794 span: assert_span.into(),
795 }),
796 ExpectedFail::All => Ok(()),
797 ExpectedFail::Variants(keys) if !shape.is_indexed() => {
798 Err(GraphcalError::ExpectedFailNotIndexed {
799 src: src.clone(),
800 span: keys
801 .first()
802 .map_or(assert_span, expected_fail_key_span)
803 .into(),
804 })
805 }
806 ExpectedFail::Variants(keys) => {
807 let mut seen = HashSet::new();
808 for key in keys {
809 validate_expected_fail_key(key, shape, src)?;
810 if !seen.insert(expected_fail_key_signature(key)) {
811 return Err(GraphcalError::ExpectedFailDuplicateKey {
812 src: src.clone(),
813 span: expected_fail_key_span(key).into(),
814 });
815 }
816 }
817 Ok(())
818 }
819 }
820}
821
822pub fn check_dimensions_tir(
835 tir: &crate::tir::typed::TIR,
836 src: &NamedSource<Arc<String>>,
837) -> Result<(), GraphcalError> {
838 detect_decl_cycles(tir, src)?;
839 detect_cross_dag_cycles(tir, src)?;
840 let builtin_fns = builtin_functions();
841
842 for (id, dag) in &tir.dags {
848 if id == &tir.root_dag_id || id.parent().as_ref() == Some(&tir.root_dag_id) {
849 check_dimensions_dag(dag, tir, &tir.registry, builtin_fns, src)?;
850 }
851 }
852
853 let declared_types = tir.build_declared_types(src)?;
858 check_no_constraints_on_generic_type_args(tir, src)?;
859 check_field_domain_constraint_targets(tir, src)?;
860 check_field_domain_constraint_dimensions(
861 tir,
862 &declared_types,
863 &tir.registry,
864 builtin_fns,
865 src,
866 )?;
867
868 Ok(())
869}
870
871fn check_dimensions_dag(
874 dag: &crate::tir::typed::DagTIR,
875 tir: &crate::tir::typed::TIR,
876 registry: &crate::registry::types::Registry,
877 builtin_fns: &HashMap<&str, crate::registry::builtins::BuiltinFunction>,
878 src: &NamedSource<Arc<String>>,
879) -> Result<(), GraphcalError> {
880 let declared_types = dag.build_declared_types(src)?;
881 let ctx = DimCheckContext {
882 declared_types: &declared_types,
883 dag: Some(dag),
884 tir,
885 registry,
886 builtin_fns,
887 src,
888 };
889
890 for entry in &dag.consts {
893 let entry_ctx = ctx.for_body(entry.src.resolve(src));
894 check_decl_expr_type(&entry_ctx, &entry.name, &entry.type_ann.span)?;
895 }
896 for entry in &dag.nodes {
897 let entry_ctx = ctx.for_body(entry.src.resolve(src));
898 check_decl_expr_type(&entry_ctx, &entry.name, &entry.type_ann.span)?;
899 }
900 for entry in &dag.params {
901 let Some(_value_expr) = entry.default_expr.as_ref() else {
902 continue;
903 };
904 let entry_ctx = ctx.for_body(entry.src.resolve(src));
905 check_decl_expr_type(&entry_ctx, &entry.name, &entry.type_ann.span)?;
906 }
907
908 for entry in &dag.asserts {
909 let body_src = entry.src.resolve(src);
910 let entry_ctx = ctx.for_body(body_src);
911 let body = entry_ctx.hir_assert_body(&entry.name, entry.span)?;
912 let shape = check_hir_assert_body(&entry_ctx, body, entry.span)?;
913 if let Some(expected_fail) = dag.expected_fail.get(&entry.name) {
914 validate_expected_fail(expected_fail, &shape, body_src, entry.span)?;
915 }
916 match body {
919 crate::hir::expr::AssertBody::Expr(e) => {
920 check_ineffective_conversions(e, false, body_src)?;
921 }
922 crate::hir::expr::AssertBody::Tolerance {
923 actual,
924 expected,
925 tolerance,
926 ..
927 } => {
928 check_ineffective_conversions(actual, false, body_src)?;
929 check_ineffective_conversions(expected, false, body_src)?;
930 check_ineffective_conversions(tolerance, false, body_src)?;
931 }
932 }
933 }
934
935 plot::check_plot_properties_dag(&ctx)?;
936
937 check_domain_constraint_targets_dag(dag, src)?;
938 check_domain_constraint_dimensions_dag(dag, &declared_types, tir, registry, builtin_fns, src)?;
939
940 Ok(())
941}
942
943enum ExpectedBound {
945 Scalar(Dimension),
947 Int,
949}
950
951fn check_domain_constraint_dimensions_dag(
963 dag: &crate::tir::typed::DagTIR,
964 declared_types: &HashMap<ScopedName, DeclaredType>,
965 tir: &crate::tir::typed::TIR,
966 registry: &Registry,
967 builtin_fns: &HashMap<&str, crate::registry::builtins::BuiltinFunction>,
968 src: &NamedSource<Arc<String>>,
969) -> Result<(), GraphcalError> {
970 let decl_iter = dag
973 .consts
974 .iter()
975 .map(|e| (&e.name, &e.src))
976 .chain(dag.params.iter().map(|e| (&e.name, &e.src)))
977 .chain(dag.nodes.iter().map(|e| (&e.name, &e.src)));
978
979 for (name, body_provenance) in decl_iter {
980 let bounds = dag
981 .resolved_decl_key_for_local(name)
982 .and_then(|key| dag.semantic.domain_bounds.get(&key));
983 let Some(bounds) = bounds else {
984 continue;
985 };
986 let body_src = body_provenance.resolve(src);
987
988 let resolved = dag.resolved_decl_types.get(name);
989 let base_resolved = resolved.map(strip_indexed);
990 let expected = match base_resolved {
991 Some(crate::tir::typed::ResolvedTypeExpr::Scalar(dim)) => {
992 ExpectedBound::Scalar(dim.clone())
993 }
994 Some(crate::tir::typed::ResolvedTypeExpr::Dimensionless) => {
995 ExpectedBound::Scalar(Dimension::dimensionless())
996 }
997 Some(crate::tir::typed::ResolvedTypeExpr::Int) => ExpectedBound::Int,
998 _ => continue,
999 };
1000
1001 for bound in bounds {
1002 let inferred = infer::hir::infer_hir_type_with_owner(
1003 &bound.value,
1004 None,
1005 declared_types,
1006 dag,
1007 tir,
1008 registry,
1009 builtin_fns,
1010 body_src,
1011 )?;
1012 check_one_bound(name, bound, &inferred, &expected, registry, body_src)?;
1013 }
1014 }
1015
1016 Ok(())
1017}
1018
1019fn check_one_bound(
1020 name: &crate::syntax::names::ScopedName,
1021 bound: &crate::tir::typed::ResolvedDomainBound,
1022 inferred: &InferredType,
1023 expected: &ExpectedBound,
1024 registry: &Registry,
1025 src: &NamedSource<Arc<String>>,
1026) -> Result<(), GraphcalError> {
1027 match expected {
1028 ExpectedBound::Scalar(target_dim) => {
1029 let ok = match inferred {
1030 InferredType::Scalar(d) => d == target_dim,
1031 InferredType::Int => target_dim.is_dimensionless(),
1032 _ => false,
1033 };
1034 if ok {
1035 return Ok(());
1036 }
1037 let bound_dim_str = match inferred {
1038 InferredType::Scalar(d) => registry.dimensions.format_dimension(d),
1039 other => format_inferred_type(other, registry),
1040 };
1041 Err(GraphcalError::DomainDimensionMismatch {
1042 name: name.to_string(),
1043 type_dim: registry.dimensions.format_dimension(target_dim),
1044 bound_name: bound.kind.to_string(),
1045 bound_dim: bound_dim_str,
1046 src: src.clone(),
1047 span: bound.span.into(),
1048 })
1049 }
1050 ExpectedBound::Int => {
1051 let ok = match inferred {
1052 InferredType::Int => true,
1053 InferredType::Scalar(d) => d.is_dimensionless(),
1054 _ => false,
1055 };
1056 if ok {
1057 return Ok(());
1058 }
1059 Err(GraphcalError::IntDomainBoundNotUnitless {
1060 name: name.to_string(),
1061 bound_name: bound.kind.to_string(),
1062 bound_type: format_inferred_type(inferred, registry),
1063 src: src.clone(),
1064 span: bound.span.into(),
1065 })
1066 }
1067 }
1068}
1069
1070fn check_domain_constraint_targets_dag(
1077 dag: &crate::tir::typed::DagTIR,
1078 src: &NamedSource<Arc<String>>,
1079) -> Result<(), GraphcalError> {
1080 let decl_iter = dag
1081 .consts
1082 .iter()
1083 .map(|e| (&e.name, &e.type_ann, e.span))
1084 .chain(dag.params.iter().map(|e| (&e.name, &e.type_ann, e.span)))
1085 .chain(dag.nodes.iter().map(|e| (&e.name, &e.type_ann, e.span)));
1086
1087 for (name, type_ann, decl_span) in decl_iter {
1088 if extract_domain_bounds(type_ann).is_empty() {
1089 continue;
1090 }
1091 let Some(resolved) = dag.resolved_decl_types.get(name) else {
1092 continue;
1093 };
1094 let type_kind = match strip_indexed(resolved) {
1095 crate::tir::typed::ResolvedTypeExpr::Bool => "Bool".to_string(),
1096 crate::tir::typed::ResolvedTypeExpr::Datetime(_) => "Datetime".to_string(),
1097 crate::tir::typed::ResolvedTypeExpr::IndexArg(index) => {
1098 format!("index {}", index.format_for_diagnostic())
1099 }
1100 crate::tir::typed::ResolvedTypeExpr::Struct(struct_name, _)
1101 | crate::tir::typed::ResolvedTypeExpr::GenericStruct {
1102 name: struct_name, ..
1103 } => format!("struct `{}`", struct_name.as_str()),
1104 crate::tir::typed::ResolvedTypeExpr::Scalar(_)
1105 | crate::tir::typed::ResolvedTypeExpr::Dimensionless
1106 | crate::tir::typed::ResolvedTypeExpr::Int
1107 | crate::tir::typed::ResolvedTypeExpr::GenericDimParam(_, _)
1108 | crate::tir::typed::ResolvedTypeExpr::GenericTypeParam(_, _)
1109 | crate::tir::typed::ResolvedTypeExpr::GenericDimExpr { .. }
1110 | crate::tir::typed::ResolvedTypeExpr::Indexed { .. } => continue,
1111 };
1112 return Err(GraphcalError::InvalidDomainTarget {
1113 type_kind,
1114 src: src.clone(),
1115 span: decl_span.into(),
1116 });
1117 }
1118 Ok(())
1119}
1120
1121fn extract_domain_bounds(
1126 type_ann: &crate::desugar::desugared_ast::TypeExpr,
1127) -> &[crate::desugar::desugared_ast::DomainBound] {
1128 if !type_ann.constraints.is_empty() {
1129 return &type_ann.constraints;
1130 }
1131 if let crate::desugar::desugared_ast::TypeExprKind::Indexed { base, .. } = &type_ann.kind {
1132 return &base.constraints;
1133 }
1134 &[]
1135}
1136
1137fn check_field_domain_constraint_targets(
1145 tir: &crate::tir::typed::TIR,
1146 src: &NamedSource<Arc<String>>,
1147) -> Result<(), GraphcalError> {
1148 for type_def in tir.registry.types.all_types() {
1149 let members: &[crate::registry::types::UnionMemberDef] =
1152 type_def.union_members().unwrap_or(&[]);
1153 for field in members.iter().flat_map(|m| m.fields.iter()) {
1154 if extract_domain_bounds(&field.type_ann).is_empty() {
1155 continue;
1156 }
1157 let kind = field_constraint_target_kind(&field.type_ann, &tir.registry);
1158 if let Some(type_kind) = kind {
1159 return Err(GraphcalError::InvalidDomainTarget {
1160 type_kind,
1161 src: src.clone(),
1162 span: field.type_ann.span.into(),
1163 });
1164 }
1165 }
1166 }
1167 Ok(())
1168}
1169
1170fn field_constraint_target_kind(
1176 type_ann: &crate::desugar::desugared_ast::TypeExpr,
1177 registry: &Registry,
1178) -> Option<String> {
1179 use crate::desugar::desugared_ast::TypeExprKind;
1180 let base = match &type_ann.kind {
1181 TypeExprKind::Indexed { base, .. } => base.as_ref(),
1182 _ => type_ann,
1183 };
1184 match &base.kind {
1185 TypeExprKind::Bool => Some("Bool".to_string()),
1186 TypeExprKind::Datetime | TypeExprKind::DatetimeApplication { .. } => {
1187 Some("Datetime".to_string())
1188 }
1189 TypeExprKind::TypeApplication { name, .. } => {
1190 Some(format!("struct `{}`", name.value.display_path()))
1191 }
1192 TypeExprKind::Dimensionless | TypeExprKind::Int | TypeExprKind::Indexed { .. } => None,
1196 TypeExprKind::DimExpr(dim_expr) => {
1197 if dim_expr.terms.len() == 1
1201 && dim_expr.terms[0].term.power.is_none()
1202 && let Some(item) = dim_expr.terms.first()
1203 {
1204 let Some(name) = item
1205 .term
1206 .name
1207 .value
1208 .as_bare()
1209 .map(super::super::syntax::names::NameAtom::as_str)
1210 else {
1211 return None;
1214 };
1215 if registry.dimensions.get_dimension(name).is_some() {
1216 None
1217 } else if registry.types.get_type(name).is_some() {
1218 Some(format!("struct `{name}`"))
1219 } else if registry.indexes.get_index(name).is_some() {
1220 Some(format!("index `{name}`"))
1221 } else {
1222 None
1225 }
1226 } else {
1227 None
1230 }
1231 }
1232 }
1233}
1234
1235fn check_field_domain_constraint_dimensions(
1243 tir: &crate::tir::typed::TIR,
1244 declared_types: &HashMap<ScopedName, DeclaredType>,
1245 registry: &Registry,
1246 builtin_fns: &HashMap<&str, crate::registry::builtins::BuiltinFunction>,
1247 src: &NamedSource<Arc<String>>,
1248) -> Result<(), GraphcalError> {
1249 let mut seen: std::collections::HashSet<&crate::tir::typed::ResolvedStructFieldTypeKey> =
1250 std::collections::HashSet::new();
1251 for (id, dag) in &tir.dags {
1252 if id != &tir.root_dag_id && id.parent().as_ref() != Some(&tir.root_dag_id) {
1253 continue;
1254 }
1255 for (key, bounds) in &dag.semantic.type_defs.field_bounds {
1256 if !seen.insert(key) {
1257 continue;
1258 }
1259 let Some(type_def) = dag.semantic.type_defs.struct_types.get(&key.owning_type) else {
1260 continue;
1261 };
1262 let Some((variant, field)) = type_def.union_members().and_then(|members| {
1263 members
1264 .iter()
1265 .flat_map(|m| m.fields.iter().map(move |f| (m, f)))
1266 .find(|(m, f)| m.name == key.constructor && f.name == key.field)
1267 }) else {
1268 continue;
1269 };
1270 let Some(expected) = field_expected_bound(&field.type_ann, registry, src)? else {
1271 continue;
1272 };
1273 let display_name = if variant.name.as_str() == type_def.name.as_str() {
1278 format!("{}.{}", type_def.name, field.name)
1279 } else {
1280 format!("{}.{}.{}", type_def.name, variant.name, field.name)
1281 };
1282 for bound in bounds {
1283 let inferred = infer::hir::infer_hir_type_with_owner(
1284 &bound.value,
1285 None,
1286 declared_types,
1287 dag,
1288 tir,
1289 registry,
1290 builtin_fns,
1291 src,
1292 )?;
1293 check_one_bound_with_display_name(
1294 &display_name,
1295 bound,
1296 &inferred,
1297 &expected,
1298 registry,
1299 src,
1300 )?;
1301 }
1302 }
1303 }
1304 Ok(())
1305}
1306
1307fn field_expected_bound(
1313 type_ann: &crate::desugar::desugared_ast::TypeExpr,
1314 registry: &Registry,
1315 src: &NamedSource<Arc<String>>,
1316) -> Result<Option<ExpectedBound>, GraphcalError> {
1317 use crate::desugar::desugared_ast::TypeExprKind;
1318 let base = match &type_ann.kind {
1319 TypeExprKind::Indexed { base, .. } => base.as_ref(),
1320 _ => type_ann,
1321 };
1322 match &base.kind {
1323 TypeExprKind::Dimensionless => Ok(Some(ExpectedBound::Scalar(Dimension::dimensionless()))),
1324 TypeExprKind::Int => Ok(Some(ExpectedBound::Int)),
1325 TypeExprKind::DimExpr(_) => Ok(registry
1326 .dimensions
1327 .resolve_type_expr(base)
1328 .map_err(|_| GraphcalError::DimensionOverflow {
1329 src: src.clone(),
1330 span: base.span.into(),
1331 })?
1332 .map(ExpectedBound::Scalar)),
1333 _ => Ok(None),
1334 }
1335}
1336
1337fn check_one_bound_with_display_name(
1341 display_name: &str,
1342 bound: &crate::tir::typed::ResolvedDomainBound,
1343 inferred: &InferredType,
1344 expected: &ExpectedBound,
1345 registry: &Registry,
1346 src: &NamedSource<Arc<String>>,
1347) -> Result<(), GraphcalError> {
1348 match expected {
1349 ExpectedBound::Scalar(target_dim) => {
1350 let ok = match inferred {
1351 InferredType::Scalar(d) => d == target_dim,
1352 InferredType::Int => target_dim.is_dimensionless(),
1353 _ => false,
1354 };
1355 if ok {
1356 return Ok(());
1357 }
1358 let bound_dim_str = match inferred {
1359 InferredType::Scalar(d) => registry.dimensions.format_dimension(d),
1360 other => format_inferred_type(other, registry),
1361 };
1362 Err(GraphcalError::DomainDimensionMismatch {
1363 name: display_name.to_string(),
1364 type_dim: registry.dimensions.format_dimension(target_dim),
1365 bound_name: bound.kind.to_string(),
1366 bound_dim: bound_dim_str,
1367 src: src.clone(),
1368 span: bound.span.into(),
1369 })
1370 }
1371 ExpectedBound::Int => {
1372 let ok = match inferred {
1373 InferredType::Int => true,
1374 InferredType::Scalar(d) => d.is_dimensionless(),
1375 _ => false,
1376 };
1377 if ok {
1378 return Ok(());
1379 }
1380 Err(GraphcalError::IntDomainBoundNotUnitless {
1381 name: display_name.to_string(),
1382 bound_name: bound.kind.to_string(),
1383 bound_type: format_inferred_type(inferred, registry),
1384 src: src.clone(),
1385 span: bound.span.into(),
1386 })
1387 }
1388 }
1389}
1390
1391fn check_no_constraints_on_generic_type_args(
1402 tir: &crate::tir::typed::TIR,
1403 src: &NamedSource<Arc<String>>,
1404) -> Result<(), GraphcalError> {
1405 let walk = |type_expr: &crate::desugar::desugared_ast::TypeExpr| -> Result<(), GraphcalError> {
1406 check_type_expr_for_generic_arg_constraints(type_expr, src)
1407 };
1408 for (id, dag) in &tir.dags {
1409 if id != &tir.root_dag_id && id.parent().as_ref() != Some(&tir.root_dag_id) {
1410 continue;
1411 }
1412 for entry in &dag.consts {
1413 walk(&entry.type_ann)?;
1414 }
1415 for entry in &dag.params {
1416 walk(&entry.type_ann)?;
1417 }
1418 for entry in &dag.nodes {
1419 walk(&entry.type_ann)?;
1420 }
1421 }
1422 for type_def in tir.registry.types.all_types() {
1423 for field in type_def.fields() {
1424 walk(&field.type_ann)?;
1425 }
1426 }
1427 Ok(())
1428}
1429
1430fn check_type_expr_for_generic_arg_constraints(
1435 type_expr: &crate::desugar::desugared_ast::TypeExpr,
1436 src: &NamedSource<Arc<String>>,
1437) -> Result<(), GraphcalError> {
1438 use crate::desugar::desugared_ast::TypeExprKind;
1439 match &type_expr.kind {
1440 TypeExprKind::Indexed { base, .. } => {
1441 check_type_expr_for_generic_arg_constraints(base, src)
1442 }
1443 TypeExprKind::TypeApplication { type_args, .. }
1444 | TypeExprKind::DatetimeApplication { type_args } => {
1445 for arg in type_args {
1446 if let Some(bound) = arg.constraints.first() {
1447 return Err(GraphcalError::GenericTypeArgDomainConstraint {
1448 src: src.clone(),
1449 span: bound.span.into(),
1450 });
1451 }
1452 check_type_expr_for_generic_arg_constraints(arg, src)?;
1454 }
1455 Ok(())
1456 }
1457 TypeExprKind::Dimensionless
1458 | TypeExprKind::Bool
1459 | TypeExprKind::Int
1460 | TypeExprKind::Datetime
1461 | TypeExprKind::DimExpr(_) => Ok(()),
1462 }
1463}
1464
1465fn strip_indexed(
1467 resolved: &crate::tir::typed::ResolvedTypeExpr,
1468) -> &crate::tir::typed::ResolvedTypeExpr {
1469 match resolved {
1470 crate::tir::typed::ResolvedTypeExpr::Indexed { base, .. } => strip_indexed(base),
1471 other => other,
1472 }
1473}
1474
1475#[expect(
1487 clippy::implicit_hasher,
1488 reason = "internal API always uses default hasher"
1489)]
1490pub fn check_override_dimension(
1491 param_name: &str,
1492 declared_types: &HashMap<ScopedName, DeclaredType>,
1493 tir: &crate::tir::typed::TIR,
1494 registry: &Registry,
1495 src: &NamedSource<Arc<String>>,
1496) -> Result<(), GraphcalError> {
1497 let builtin_fns = builtin_functions();
1498
1499 let param_key = ScopedName::local(param_name);
1502 let declared =
1503 declared_types
1504 .get(¶m_key)
1505 .ok_or_else(|| GraphcalError::OverrideUnknownParam {
1506 name: crate::syntax::names::DeclName::new(param_name.to_string()),
1507 })?;
1508 let dag = tir.root();
1509 let hir_expr = dag
1510 .resolved_decl_key_for_local(¶m_key)
1511 .and_then(|key| dag.semantic.expressions.param_defaults.get(&key))
1512 .ok_or_else(|| GraphcalError::InternalError {
1513 message: format!("override for `{param_name}` was not applied to the root DAG"),
1514 src: src.clone(),
1515 span: crate::syntax::span::Span::new(0, 0).into(),
1516 })?;
1517 let inferred = infer::hir::infer_hir_type_with_owner(
1518 hir_expr,
1519 Some(param_name),
1520 declared_types,
1521 dag,
1522 tir,
1523 registry,
1524 builtin_fns,
1525 src,
1526 )?;
1527
1528 if !types_match(declared, &inferred) {
1529 return Err(GraphcalError::DimensionMismatch {
1530 expected: format_declared_type(declared, registry),
1531 found: format_inferred_type(&inferred, registry),
1532 help: format!(
1533 "override for `{param_name}` must have dimension {}",
1534 format_declared_type(declared, registry)
1535 ),
1536 src: src.clone(),
1537 span: hir_expr.span.into(),
1538 });
1539 }
1540 Ok(())
1541}
1542
1543enum DagCycleFrame {
1555 Enter(crate::dag_id::DagId),
1556 Leave(crate::dag_id::DagId),
1557}
1558
1559fn collect_dag_call_targets_from_dag(
1561 dag: &crate::tir::typed::DagTIR,
1562 out: &mut std::collections::BTreeSet<crate::dag_id::DagId>,
1563) {
1564 out.extend(
1565 dag.semantic
1566 .inline_dag_refs
1567 .calls
1568 .values()
1569 .map(|call| call.target.clone()),
1570 );
1571}
1572
1573fn detect_decl_cycles(
1582 tir: &crate::tir::typed::TIR,
1583 src: &NamedSource<Arc<String>>,
1584) -> Result<(), GraphcalError> {
1585 use std::collections::BTreeSet;
1586
1587 use petgraph::algo::toposort;
1588 use petgraph::graph::DiGraph;
1589
1590 use crate::syntax::names::{ResolvedName, ScopedName, namespace};
1591
1592 type ResolvedDeclKey = ResolvedName<namespace::Decl>;
1593
1594 fn local_resolved_decl_key(
1595 dag: &crate::tir::typed::DagTIR,
1596 name: &ScopedName,
1597 span: crate::syntax::span::Span,
1598 src: &NamedSource<Arc<String>>,
1599 ) -> Result<ResolvedDeclKey, GraphcalError> {
1600 dag.resolved_decl_key_for_local(name)
1601 .ok_or_else(|| GraphcalError::InternalError {
1602 message: format!(
1603 "semantic dependency metadata contains no local canonical key for declaration `{name}`"
1604 ),
1605 src: src.clone(),
1606 span: span.into(),
1607 })
1608 }
1609
1610 fn check_resolved<'a>(
1611 dag: &crate::tir::typed::DagTIR,
1612 names_with_spans: impl Iterator<Item = (&'a ScopedName, crate::syntax::span::Span)>,
1613 deps: &HashMap<ResolvedDeclKey, BTreeSet<ResolvedDeclKey>>,
1614 src: &NamedSource<Arc<String>>,
1615 ) -> Result<(), GraphcalError> {
1616 let mut graph = DiGraph::<ResolvedDeclKey, ()>::new();
1617 let mut index_map: HashMap<ResolvedDeclKey, petgraph::graph::NodeIndex> = HashMap::new();
1618 let mut local_name_by_key: HashMap<ResolvedDeclKey, ScopedName> = HashMap::new();
1619 let mut span_by_key: HashMap<ResolvedDeclKey, crate::syntax::span::Span> = HashMap::new();
1620 for (name, span) in names_with_spans {
1621 let key = local_resolved_decl_key(dag, name, span, src)?;
1622 let idx = graph.add_node(key.clone());
1623 index_map.insert(key.clone(), idx);
1624 local_name_by_key.insert(key.clone(), name.clone());
1625 span_by_key.insert(key, span);
1626 }
1627 if index_map.is_empty() {
1628 return Ok(());
1629 }
1630 for (name, dep_set) in deps {
1631 let Some(&to) = index_map.get(name) else {
1632 continue;
1633 };
1634 for dep in dep_set {
1635 if let Some(&from) = index_map.get(dep) {
1636 graph.add_edge(from, to, ());
1637 }
1638 }
1639 }
1640 toposort(&graph, None).map(|_| ()).map_err(|cycle| {
1641 let cycle_node = &graph[cycle.node_id()];
1642 let span = span_by_key
1643 .get(cycle_node)
1644 .copied()
1645 .unwrap_or_else(|| crate::syntax::span::Span::new(0, 0));
1646 let name = local_name_by_key
1647 .get(cycle_node)
1648 .map_or_else(|| cycle_node.to_string(), std::string::ToString::to_string);
1649 GraphcalError::CyclicDependency {
1650 name,
1651 src: src.clone(),
1652 span: span.into(),
1653 }
1654 })
1655 }
1656
1657 for dag in tir.dags.values() {
1658 let deps = &dag.semantic.dependencies;
1659 check_resolved(
1660 dag,
1661 dag.consts.iter().map(|e| (&e.name, e.span)),
1662 &deps.const_deps,
1663 src,
1664 )?;
1665 check_resolved(
1666 dag,
1667 dag.params
1668 .iter()
1669 .map(|e| (&e.name, e.span))
1670 .chain(dag.nodes.iter().map(|e| (&e.name, e.span))),
1671 &deps.runtime_deps,
1672 src,
1673 )?;
1674 }
1675 Ok(())
1676}
1677
1678fn detect_cross_dag_cycles(
1679 tir: &crate::tir::typed::TIR,
1680 src: &NamedSource<Arc<String>>,
1681) -> Result<(), GraphcalError> {
1682 use std::collections::{BTreeMap, BTreeSet, HashSet};
1683
1684 use crate::dag_id::DagId;
1685
1686 let mut edges: BTreeMap<DagId, BTreeSet<DagId>> = BTreeMap::new();
1687 let mut spans: HashMap<DagId, crate::syntax::span::Span> = HashMap::new();
1688 for (key, dag_tir) in &tir.dags {
1689 let mut targets = BTreeSet::new();
1690 collect_dag_call_targets_from_dag(dag_tir, &mut targets);
1691 edges.insert(key.clone(), targets);
1692 let parent = key.parent();
1696 let span = if parent.as_ref() == Some(&tir.root_dag_id) {
1697 tir.registry
1698 .dags
1699 .get(key.name())
1700 .map_or_else(|| crate::syntax::span::Span::new(0, 0), |d| d.name.span)
1701 } else {
1702 crate::syntax::span::Span::new(0, 0)
1703 };
1704 spans.insert(key.clone(), span);
1705 }
1706
1707 let mut visited: HashSet<DagId> = HashSet::new();
1708 let mut on_stack: HashSet<DagId> = HashSet::new();
1709
1710 for start in edges.keys() {
1711 if visited.contains(start) {
1712 continue;
1713 }
1714 let mut work: Vec<DagCycleFrame> = vec![DagCycleFrame::Enter(start.clone())];
1715 while let Some(frame) = work.pop() {
1716 match frame {
1717 DagCycleFrame::Enter(key) => {
1718 if visited.contains(&key) {
1719 continue;
1720 }
1721 if on_stack.contains(&key) {
1722 let span = spans
1723 .get(&key)
1724 .copied()
1725 .unwrap_or_else(|| crate::syntax::span::Span::new(0, 0));
1726 return Err(GraphcalError::CyclicDependency {
1727 name: key.to_string(),
1728 src: src.clone(),
1729 span: span.into(),
1730 });
1731 }
1732 on_stack.insert(key.clone());
1733 work.push(DagCycleFrame::Leave(key.clone()));
1734 if let Some(targets) = edges.get(&key) {
1735 for t in targets {
1736 if edges.contains_key(t) {
1737 work.push(DagCycleFrame::Enter(t.clone()));
1738 }
1739 }
1740 }
1741 }
1742 DagCycleFrame::Leave(key) => {
1743 on_stack.remove(&key);
1744 visited.insert(key);
1745 }
1746 }
1747 }
1748 }
1749
1750 Ok(())
1751}