diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize.rs index 1e1bc5a50a2..903e962d766 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize.rs @@ -36,6 +36,19 @@ //! - **Diagnostics:** [`Error::ExcessiveSpecializations`] is a non-fatal //! warning. Other errors are fatal because the intermediate FIR may violate //! downstream invariants. +//! - **Relies on an acyclic UDT graph.** Several type walks in this pass and +//! its submodules expand `Ty::Udt` through the referenced type's definition +//! and keep descending, with no visited set — `ty_contains_arrow_through_udts`, +//! `analysis::extract_arrow_params_from_ty`, `analysis::output_path_resolves_to_arrow`, +//! and the `resolve_udt_ty` helpers. They terminate only because a +//! user-defined type cannot reference itself. The Q# type checker enforces +//! this in `qsc_frontend::typeck::check`, rejecting any cyclic declaration +//! with `Qdk.Qsc.TypeCk.RecursiveUdt` before HIR passes run; the guarantee +//! covers the package under compilation and extends to the whole store only +//! where dependency errors are also gated. Q# has no indirection primitive, +//! so `A[]` and `A -> Int` are descended through exactly as a bare `A` is and +//! are equally fatal — this pass is where the arrow-mediated case was +//! originally observed to overflow the stack. //! - Synthesized expressions use `EMPTY_EXEC_RANGE`; //! `crate::exec_graph_rebuild` repairs exec graphs later. @@ -910,6 +923,8 @@ pub(crate) fn ty_contains_arrow(ty: &Ty) -> bool { /// callable whose parameter is a UDT containing a callable field keeps the loop /// running until that nested callable field is specialized. The rewrite helpers /// still use `ty_contains_arrow`, where UDTs intentionally remain opaque. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn ty_contains_arrow_through_udts(store: &PackageStore, ty: &Ty) -> bool { match ty { Ty::Arrow(_) => true, @@ -1520,6 +1535,7 @@ pub(super) fn has_multiple_forwarded_callable_arrays( static_callable_array_positions(package, group).len() >= 2 } +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn resolve_udt_ty(package: &Package, ty: &Ty) -> Ty { match ty { Ty::Udt(Res::Item(item_id)) => { diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs index 2800cb763b8..b1024115369 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/analysis.rs @@ -193,6 +193,8 @@ struct ArrowParamExtraction<'a> { /// /// UDTs are expanded to their pure type so callable fields inside nested /// newtypes are treated the same way as tuple fields. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn extract_arrow_params_from_ty( context: &ArrowParamExtraction<'_>, param_ty: &Ty, @@ -1452,6 +1454,8 @@ fn resolve_callee_projection( /// Reports whether following `path` into the (possibly nested tuple) type `ty` /// lands on an arrow type. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn output_path_resolves_to_arrow(store: &PackageStore, ty: &Ty, path: &[usize]) -> bool { match ty { Ty::Arrow(_) => path.is_empty(), diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs index 605b119c256..1c1aa7545c8 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs @@ -5049,6 +5049,8 @@ fn local_ty_contains_arrow_through_udts(package: &Package, ty: &Ty) -> bool { /// Resolves a type through user-defined-type wrappers to its underlying /// structural type, recursing into tuples, arrays, and arrow inputs and /// outputs. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn resolve_udt_ty(package: &Package, ty: &Ty) -> Ty { match ty { Ty::Udt(Res::Item(item_id)) => { diff --git a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs index 74046350ae1..853af0a434a 100644 --- a/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs +++ b/source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs @@ -1921,6 +1921,8 @@ fn callable_input_contains_arrow(package: &Package, callable: LocalItemId) -> bo /// A residual `Ty::Udt` (one [`resolve_udt_ty`] could not expand, e.g. a /// foreign or non-`Ty` item) conservatively counts as containing an arrow so an /// unknown shape is never misclassified as arrow-free. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn ty_contains_arrow_through_udts(package: &Package, ty: &Ty) -> bool { match resolve_udt_ty(package, ty) { Ty::Arrow(_) | Ty::Udt(_) => true, @@ -5341,6 +5343,8 @@ fn remove_ty_at_nested_path(package: &Package, ty: &Ty, path: &[usize]) -> Ty { /// structural view that analysis used so a path like `cfg::Inner::Op` can remove the arrow /// field from the specialized callable's input type. Non-UDT leaves are preserved, and nested /// tuples, arrays, and arrows are rebuilt with any UDTs inside them expanded as well. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. fn resolve_udt_ty(package: &Package, ty: &Ty) -> Ty { match ty { Ty::Udt(Res::Item(item_id)) => { diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs b/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs index 3e53227c174..056cd88abaf 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/slot.rs @@ -596,6 +596,8 @@ fn create_default_value_kind( } /// Read-only check whether `ty` has a synthesizable classical default. +/// +/// Unguarded UDT recursion; terminates only because the frontend rejects cyclic UDTs. pub(super) fn is_type_defaultable(package: &Package, package_id: PackageId, ty: &Ty) -> bool { match ty { Ty::Prim( diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/contracts_and_errors.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/contracts_and_errors.rs index 5c387d844c0..18721c2cbc0 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/contracts_and_errors.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/contracts_and_errors.rs @@ -9,6 +9,7 @@ use qsc_fir::{ use rustc_hash::FxHashMap; use crate::fir_builder::alloc_expr_stmt; +use crate::test_utils::frontend_error_codes; use super::*; @@ -611,10 +612,10 @@ fn defaultable_type_with_early_return_succeeds() { #[test] fn recursive_udt_early_return_fails_before_return_unify() { - // Recursive UDTs (e.g. `newtype Tree = (Int, Tree[])`) are definable - // in Q# but produce a compile error at the frontend before reaching - // return_unify. This documents that L7 (recursive-UDT defaultability) - // is covered by language-level rejection. + // A recursive UDT is rejected by the Q# type checker, so it never reaches this crate. + // That is what lets `return_unify` and the other transforms expand UDT definitions + // structurally without a visited set. The diagnostic itself is pinned by the + // `recursive_udt_*` tests in `qsc_frontend::typeck::tests`. let source = indoc! {r#" namespace Test { newtype Tree = (Data : Int, Children : Tree[]); @@ -630,23 +631,10 @@ fn recursive_udt_early_return_fails_before_return_unify() { } "#}; - let (_store, _pkg_id, result) = - compile_and_run_pipeline_to_with_errors(source, PipelineStage::ReturnUnify); - - // The program should either fail at the frontend (cyclic UDT) or - // succeed if the frontend resolves it. Either way, it should not - // panic in return_unify. - // If errors exist, they should not be return_unify panics. - for err in &result.errors { - if let crate::PipelineError::ReturnUnify(ru_err) = err { - // Any return_unify error is acceptable (diagnostic, not panic). - // We just verify it didn't panic. - assert!( - !format!("{ru_err:?}").contains("panic"), - "return_unify should not panic on recursive UDT: {ru_err:?}" - ); - } - } + assert_eq!( + frontend_error_codes(source), + vec!["Qdk.Qsc.TypeCk.RecursiveUdt".to_string()] + ); } #[test] diff --git a/source/compiler/qsc_fir_transforms/src/test_utils.rs b/source/compiler/qsc_fir_transforms/src/test_utils.rs index 16deaf5a716..47a6dbb4c90 100644 --- a/source/compiler/qsc_fir_transforms/src/test_utils.rs +++ b/source/compiler/qsc_fir_transforms/src/test_utils.rs @@ -261,6 +261,31 @@ pub fn compile_to_fir(source: &str) -> (fir::PackageStore, fir::PackageId) { compile_to_fir_with_capabilities(source, TargetCapabilityFlags::empty()) } +/// Compiles Q# source and returns the frontend diagnostic codes instead of +/// asserting that compilation succeeded. +/// +/// Use this to pin source that the frontend is expected to reject, which the +/// asserting `compile_to_fir*` helpers cannot express. +#[cfg(test)] +pub(crate) fn frontend_error_codes(source: &str) -> Vec { + use miette::Diagnostic; + + with_cached_stdlib_store(TargetCapabilityFlags::empty(), |store, std_id| { + let sources = SourceMap::new(vec![("test.qs".into(), source.into())], None); + let unit = frontend_compile::compile( + store, + &[(PackageId::CORE, None), (std_id, None)], + sources, + TargetCapabilityFlags::empty(), + LanguageFeatures::default(), + ); + unit.errors + .iter() + .filter_map(|error| error.code().map(|code| code.to_string())) + .collect() + }) +} + /// Compiles Q# source through core+std → HIR passes → FIR lowering using the /// given target capabilities. /// diff --git a/source/compiler/qsc_fir_transforms/src/udt_erase.rs b/source/compiler/qsc_fir_transforms/src/udt_erase.rs index b92cf80ef79..13492be6b5f 100644 --- a/source/compiler/qsc_fir_transforms/src/udt_erase.rs +++ b/source/compiler/qsc_fir_transforms/src/udt_erase.rs @@ -27,6 +27,17 @@ //! callable bodies in place; the pipeline driver unconditionally rebuilds the //! exec graph of every reachable spec in every reachable package afterwards, //! so this pass no longer tracks or returns which specs it mutated. +//! - **Relies on an acyclic UDT graph.** `resolve_ty` recurses through `Udt`, +//! `Array`, `Tuple`, and `Arrow` with no visited set, which is sound only +//! because a user-defined type cannot reference itself. The Q# type checker +//! enforces this in `qsc_frontend::typeck::check`, which rejects any cyclic +//! declaration with `Qdk.Qsc.TypeCk.RecursiveUdt` before HIR passes run. The +//! guarantee covers the package under compilation and extends to the whole +//! store only where dependency errors are also gated. Note that no form of +//! indirection exempts a cycle: `A[]` and `A -> Int` are erased structurally +//! just as a bare `A` is, so a cyclic type has no finite erased form at all — +//! a visited-set guard would terminate into a state that still violates +//! `PostUdtErase` rather than into a correct one. //! - Synthesized expressions use `EMPTY_EXEC_RANGE`; //! [`crate::exec_graph_rebuild`] rebuilds exec graphs later. diff --git a/source/compiler/qsc_frontend/src/incremental/tests.rs b/source/compiler/qsc_frontend/src/incremental/tests.rs index eae30cac6b3..195b7190ae2 100644 --- a/source/compiler/qsc_frontend/src/incremental/tests.rs +++ b/source/compiler/qsc_frontend/src/incremental/tests.rs @@ -348,6 +348,40 @@ fn errors_across_multiple_lines() { .assert_debug_eq(&labels); } +#[test] +fn recursive_udt_reported_once_across_fragments() { + let store = PackageStore::new(compile::core()); + let mut compiler = Compiler::new( + &store, + &Vec::new(), + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + ); + let mut unit = CompileUnit::new(store.peek_package_id()); + + let errors = compiler + .compile_fragments( + &mut unit, + "line_1", + "struct Foo { Bar : Foo }", + fail_on_error, + ) + .expect_err("should fail"); + assert_eq!( + errors + .iter() + .filter_map(|e| e.code().map(|c| c.to_string())) + .collect::>(), + vec!["Qdk.Qsc.TypeCk.RecursiveUdt".to_string()] + ); + + // The checker is long-lived across fragments, so the already-reported type must not be + // re-reported when a later fragment is compiled. + compiler + .compile_fragments(&mut unit, "line_2", "let x = 1;", fail_on_error) + .expect("should succeed"); +} + #[test] fn continue_after_parse_error() { let store = PackageStore::new(compile::core()); diff --git a/source/compiler/qsc_frontend/src/typeck.rs b/source/compiler/qsc_frontend/src/typeck.rs index e5c24ae842b..c766796664b 100644 --- a/source/compiler/qsc_frontend/src/typeck.rs +++ b/source/compiler/qsc_frontend/src/typeck.rs @@ -240,6 +240,16 @@ enum ErrorKind { span: Span, name: String, }, + #[error("user-defined type `{name}` is recursive")] + #[help( + "a user-defined type cannot contain itself, directly or through other types; arrays and callable types do not break the cycle, so `{name}[]` and `{name} -> _` are recursive as well" + )] + #[diagnostic(code("Qdk.Qsc.TypeCk.RecursiveUdt"))] + RecursiveUdt { + name: String, + #[label] + span: Span, + }, #[error("expected {expected} parameters for constraint, found {found}")] #[diagnostic(code("Qdk.Qsc.TypeCk.IncorrectNumberOfConstraintParameters"))] IncorrectNumberOfConstraintParameters { diff --git a/source/compiler/qsc_frontend/src/typeck/check.rs b/source/compiler/qsc_frontend/src/typeck/check.rs index 89baf34e47f..4ecfbcc1926 100644 --- a/source/compiler/qsc_frontend/src/typeck/check.rs +++ b/source/compiler/qsc_frontend/src/typeck/check.rs @@ -15,11 +15,12 @@ use qsc_ast::{ visit::{self, Visitor}, }; use qsc_data_structures::index_map::IndexMap; +use qsc_data_structures::span::Span; use qsc_hir::{ hir::{self, ItemId, PackageId}, - ty::{FunctorSetValue, Scheme, Ty, Udt}, + ty::{FunctorSetValue, Scheme, Ty, Udt, UdtDef, UdtDefKind}, }; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use std::vec; pub(crate) struct GlobalTable { @@ -136,7 +137,12 @@ impl Checker { } pub(crate) fn check_package(&mut self, names: &Names, package: &ast::Package) { - ItemCollector::new(self, names).visit_package(package); + let local_udts = { + let mut collector = ItemCollector::new(self, names); + collector.visit_package(package); + collector.local_udts + }; + self.check_recursive_udts(&local_udts); ItemChecker::new(self, names).visit_package(package); if let Some(entry) = &package.entry { @@ -161,6 +167,26 @@ impl Checker { } } + /// Rejects any user-defined type declared in this package that can reach itself through + /// its own field types. Q# has no indirection primitive, so a cycle through an array or a + /// callable type is just as unrepresentable as a direct one: downstream passes expand UDT + /// definitions structurally without a visited set, and a cyclic type has no finite expansion. + fn check_recursive_udts(&mut self, local_udts: &[ItemId]) { + let mut errors = Vec::new(); + for &id in local_udts { + let Some(udt) = self.table.udts.get(&id) else { + continue; + }; + if let Some(span) = cycle_span(&self.table.udts, id, &udt.definition) { + errors.push(Error(ErrorKind::RecursiveUdt { + name: udt.name.to_string(), + span, + })); + } + } + self.errors.append(&mut errors); + } + fn check_callable_decl(&mut self, names: &Names, decl: &ast::CallableDecl) { self.check_callable_signature(names, decl); let output = convert::ty_from_ast(names, &decl.output, &mut Default::default()).0; @@ -238,11 +264,19 @@ impl Checker { struct ItemCollector<'a> { checker: &'a mut Checker, names: &'a Names, + /// UDTs declared by the package being collected, in declaration order. Scoped to this + /// collector so it cannot accumulate across the incremental compiler's repeated + /// `check_package` calls, and ordered so diagnostics do not depend on `FxHashMap` order. + local_udts: Vec, } impl<'a> ItemCollector<'a> { fn new(checker: &'a mut Checker, names: &'a Names) -> Self { - Self { checker, names } + Self { + checker, + names, + local_udts: Vec::new(), + } } } @@ -282,6 +316,7 @@ impl Visitor<'_> for ItemCollector<'_> { definition: udt_def, }, ); + self.local_udts.push(item); self.checker.globals.insert(item, cons); } ast::ItemKind::Struct(decl) => { @@ -307,6 +342,7 @@ impl Visitor<'_> for ItemCollector<'_> { definition: udt_def, }, ); + self.local_udts.push(item); self.checker.globals.insert(item, cons); } _ => {} @@ -339,3 +375,63 @@ impl Visitor<'_> for ItemChecker<'_> { // We do not typecheck attributes, as they are verified during lowering. fn visit_attr(&mut self, _: &ast::Attr) {} } + +/// Finds the innermost field definition within `def` whose type can reach `target`, and returns +/// its span. Returns `None` when no field closes a cycle back to `target`. Since every outgoing +/// edge in the UDT graph originates at a leaf field, this is exactly the "is `target` recursive" +/// predicate as well as the label selector. +fn cycle_span(udts: &FxHashMap, target: ItemId, def: &UdtDef) -> Option { + match &def.kind { + UdtDefKind::Field(field) => { + ty_reaches(udts, target, &field.ty, &mut FxHashSet::default()).then_some(def.span) + } + UdtDefKind::Tuple(defs) => defs.iter().find_map(|def| cycle_span(udts, target, def)), + } +} + +/// Whether `target` is reachable from `ty` by descending through arrays, tuples, callable +/// signatures, and the definitions of the UDTs encountered along the way. `visited` bounds the +/// walk so it terminates on cyclic input. +fn ty_reaches( + udts: &FxHashMap, + target: ItemId, + ty: &Ty, + visited: &mut FxHashSet, +) -> bool { + match ty { + Ty::Udt(_, hir::Res::Item(id)) => { + if *id == target { + return true; + } + if !visited.insert(*id) { + return false; + } + // A missing entry means name resolution already failed; a second error would be noise. + udts.get(id) + .is_some_and(|udt| def_reaches(udts, target, &udt.definition, visited)) + } + Ty::Array(item) => ty_reaches(udts, target, item, visited), + Ty::Tuple(items) => items + .iter() + .any(|item| ty_reaches(udts, target, item, visited)), + Ty::Arrow(arrow) => { + ty_reaches(udts, target, &arrow.input.borrow(), visited) + || ty_reaches(udts, target, &arrow.output.borrow(), visited) + } + _ => false, + } +} + +fn def_reaches( + udts: &FxHashMap, + target: ItemId, + def: &UdtDef, + visited: &mut FxHashSet, +) -> bool { + match &def.kind { + UdtDefKind::Field(field) => ty_reaches(udts, target, &field.ty, visited), + UdtDefKind::Tuple(defs) => defs + .iter() + .any(|def| def_reaches(udts, target, def, visited)), + } +} diff --git a/source/compiler/qsc_frontend/src/typeck/tests.rs b/source/compiler/qsc_frontend/src/typeck/tests.rs index d70e878ef7a..35ef2e6642d 100644 --- a/source/compiler/qsc_frontend/src/typeck/tests.rs +++ b/source/compiler/qsc_frontend/src/typeck/tests.rs @@ -10,6 +10,7 @@ use crate::{ }; use expect_test::{Expect, expect}; use indoc::indoc; +use miette::Diagnostic; use qsc_ast::{ assigner::Assigner as AstAssigner, ast::{Block, Expr, Idents, NodeId, Package, Pat, Path, PathKind, QubitInit, TopLevelNode}, @@ -6205,3 +6206,305 @@ fn call_expr_non_tuple_expr() { "##]], ); } + +#[test] +fn recursive_struct_direct() { + check( + indoc! {" + namespace A { + struct Foo { Bar : Foo } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 40 } }))) + "#]], + ); +} + +#[test] +fn recursive_newtype_direct() { + check( + indoc! {" + namespace A { + newtype Foo = Foo; + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 32, hi: 35 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_through_array() { + check( + indoc! {" + namespace A { + newtype Tree = (Data : Int, Children : Tree[]); + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Tree", span: Span { lo: 46, hi: 63 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_through_nested_array() { + check( + indoc! {" + namespace A { + struct Foo { Children : Foo[][] } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 49 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_through_tuple_only() { + check( + indoc! {" + namespace A { + newtype Foo = (Int, Foo); + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 32, hi: 42 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_through_arrow() { + check( + indoc! {" + namespace A { + struct SelfApp { Run : SelfApp -> (Int -> Int) } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "SelfApp", span: Span { lo: 35, hi: 64 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_mutual() { + check( + indoc! {" + namespace A { + struct Foo { B : Bar } + struct Bar { F : Foo } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 38 } }))) + Error(Type(Error(RecursiveUdt { name: "Bar", span: Span { lo: 58, hi: 65 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_mutual_across_namespaces() { + // The cycle is closed by resolved item identity, not by shared namespace or bare name. + check( + indoc! {" + namespace A { + struct Foo { B : B.Bar } + } + namespace B { + struct Bar { F : A.Foo } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 40 } }))) + Error(Type(Error(RecursiveUdt { name: "Bar", span: Span { lo: 76, hi: 85 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_mutual_through_arrow_and_tuple() { + // Neither field names the other type directly; the edge only exists inside a callable + // output tuple. + check( + indoc! {" + namespace A { + struct Foo { F : Int -> (Bool, Bar) } + struct Bar { F : Int -> (Bool, Foo) } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 53 } }))) + Error(Type(Error(RecursiveUdt { name: "Bar", span: Span { lo: 73, hi: 95 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_chain() { + check( + indoc! {" + namespace A { + struct Leaf { M : Mid } + struct Mid { T : Top } + struct Top { L : Leaf } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Leaf", span: Span { lo: 32, hi: 39 } }))) + Error(Type(Error(RecursiveUdt { name: "Mid", span: Span { lo: 59, hi: 66 } }))) + Error(Type(Error(RecursiveUdt { name: "Top", span: Span { lo: 86, hi: 94 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_independent_cycles_all_reported() { + // Two disjoint cycles in one compilation: the check does not stop at the first component. + check( + indoc! {" + namespace A { + struct Foo { B : Bar } + struct Bar { F : Foo } + struct Baz { Q : Qux } + struct Qux { Z : Baz } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 38 } }))) + Error(Type(Error(RecursiveUdt { name: "Bar", span: Span { lo: 58, hi: 65 } }))) + Error(Type(Error(RecursiveUdt { name: "Baz", span: Span { lo: 85, hi: 92 } }))) + Error(Type(Error(RecursiveUdt { name: "Qux", span: Span { lo: 112, hi: 119 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_multiple_fields_reports_once() { + check( + indoc! {" + namespace A { + struct Foo { X : Foo, Y : Foo } + } + "}, + "", + &expect![[r#" + Error(Type(Error(RecursiveUdt { name: "Foo", span: Span { lo: 31, hi: 38 } }))) + "#]], + ); +} + +#[test] +fn recursive_udt_reported_at_use_site_program() { + // Despite `SelfApp` being constructed, field-accessed, and passed around, the cycle is + // reported exactly once and at the declaration, never at a use site. Inference still + // completes: every use site below gets a concrete type, with no cascading errors. + check( + indoc! {" + namespace A { + struct SelfApp { Run : SelfApp -> (Int -> Int) } + function Apply(s : SelfApp) : Int { (s.Run(s))(1) } + @EntryPoint() + operation Main() : Int { + let f = new SelfApp { Run = s -> x -> x }; + Apply(f) + } + } + "}, + "", + &expect![[r##" + #23 85-98 "(s : SelfApp)" : UDT<"SelfApp": Item 1 (Package 2)> + #24 86-97 "s : SelfApp" : UDT<"SelfApp": Item 1 (Package 2)> + #32 105-122 "{ (s.Run(s))(1) }" : Int + #34 107-120 "(s.Run(s))(1)" : Int + #35 107-117 "(s.Run(s))" : (Int -> Int) + #36 108-116 "s.Run(s)" : (Int -> Int) + #37 108-113 "s.Run" : (UDT<"SelfApp": Item 1 (Package 2)> -> (Int -> Int)) + #39 108-109 "s" : UDT<"SelfApp": Item 1 (Package 2)> + #40 110-113 "Run" : (UDT<"SelfApp": Item 1 (Package 2)> -> (Int -> Int)) + #41 113-116 "(s)" : UDT<"SelfApp": Item 1 (Package 2)> + #42 114-115 "s" : UDT<"SelfApp": Item 1 (Package 2)> + #45 117-120 "(1)" : Int + #46 118-119 "1" : Int + #50 138-140 "()" : ? + #53 159-161 "()" : Unit + #57 168-243 "{\n let f = new SelfApp { Run = s -> x -> x };\n Apply(f)\n }" : Int + #59 182-183 "f" : UDT<"SelfApp": Item 1 (Package 2)> + #61 186-219 "new SelfApp { Run = s -> x -> x }" : UDT<"SelfApp": Item 1 (Package 2)> + #66 206-217 "s -> x -> x" : (UDT<"SelfApp": Item 1 (Package 2)> -> (Int -> Int)) + #67 206-207 "s" : UDT<"SelfApp": Item 1 (Package 2)> + #69 211-217 "x -> x" : (Int -> Int) + #70 211-212 "x" : Int + #72 216-217 "x" : Int + #76 229-237 "Apply(f)" : Int + #77 229-234 "Apply" : (UDT<"SelfApp": Item 1 (Package 2)> -> Int) + #80 234-237 "(f)" : UDT<"SelfApp": Item 1 (Package 2)> + #81 235-236 "f" : UDT<"SelfApp": Item 1 (Package 2)> + Error(Type(Error(RecursiveUdt { name: "SelfApp", span: Span { lo: 35, hi: 64 } }))) + "##]], + ); +} + +#[test] +fn acyclic_udt_chain_is_allowed() { + check( + indoc! {" + namespace A { + struct Leaf { V : Int } + struct Mid { L : Leaf } + struct Top { M : Mid } + } + "}, + "", + &expect![[r#""#]], + ); +} + +#[test] +fn acyclic_udt_through_arrays_and_arrows_is_allowed() { + check( + indoc! {" + namespace A { + struct Leaf { V : Int } + struct Holder { Many : Leaf[], Make : Leaf -> Leaf, Pairs : (Leaf, Leaf[])[] } + } + "}, + "", + &expect![[r#""#]], + ); +} + +#[test] +fn recursive_udt_diagnostic_code() { + let (_, _, errors) = compile( + indoc! {" + namespace A { + struct Foo { Bar : Foo } + } + "}, + "", + false, + ); + + let codes: Vec<_> = errors + .iter() + .filter_map(|e| Diagnostic::code(e).map(|c| c.to_string())) + .collect(); + + assert_eq!(codes, vec!["Qdk.Qsc.TypeCk.RecursiveUdt".to_string()]); +}