From 4ab529803f759741d120f8394f89e33184bd04fc Mon Sep 17 00:00:00 2001 From: cake-monotone Date: Tue, 18 Mar 2025 21:59:14 +0900 Subject: [PATCH] [red-knot] Refactor `property_tests.rs` into `property_tests` module structure (#16827) ## Summary For now, `property_tests.rs` has grown larger and larger, making the file difficult to read and maintain. Although the code has been split, the test paths and full names remain unchanged. There are no changes affecting test execution. --- .../src/types/property_tests.rs | 326 +----------------- .../src/types/property_tests/setup.rs | 9 + .../types/property_tests/type_generation.rs | 313 +++++++++++++++++ 3 files changed, 327 insertions(+), 321 deletions(-) create mode 100644 crates/red_knot_python_semantic/src/types/property_tests/setup.rs create mode 100644 crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/red_knot_python_semantic/src/types/property_tests.rs index ecc64b24c0..55273f4e9f 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests.rs @@ -23,314 +23,10 @@ //! while cargo test --release -p red_knot_python_semantic -- \ //! --ignored types::property_tests::stable; do :; done //! ``` +mod setup; +mod type_generation; -use std::sync::{Arc, Mutex, OnceLock}; - -use crate::db::tests::{setup_db, TestDb}; -use crate::symbol::{builtins_symbol, known_module_symbol}; -use crate::types::{ - BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, - SubclassOfType, TupleType, Type, UnionType, -}; -use crate::{Db, KnownModule}; -use quickcheck::{Arbitrary, Gen}; - -/// A test representation of a type that can be transformed unambiguously into a real Type, -/// given a db. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum Ty { - Never, - Unknown, - None, - Any, - IntLiteral(i64), - BooleanLiteral(bool), - StringLiteral(&'static str), - LiteralString, - BytesLiteral(&'static str), - // BuiltinInstance("str") corresponds to an instance of the builtin `str` class - BuiltinInstance(&'static str), - /// Members of the `abc` stdlib module - AbcInstance(&'static str), - AbcClassLiteral(&'static str), - TypingLiteral, - // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself - BuiltinClassLiteral(&'static str), - KnownClassInstance(KnownClass), - Union(Vec), - Intersection { - pos: Vec, - neg: Vec, - }, - Tuple(Vec), - SubclassOfAny, - SubclassOfBuiltinClass(&'static str), - SubclassOfAbcClass(&'static str), - AlwaysTruthy, - AlwaysFalsy, - BuiltinsFunction(&'static str), - BuiltinsBoundMethod { - class: &'static str, - method: &'static str, - }, -} - -#[salsa::tracked] -fn create_bound_method<'db>( - db: &'db dyn Db, - function: Type<'db>, - builtins_class: Type<'db>, -) -> Type<'db> { - Type::Callable(CallableType::BoundMethod(BoundMethodType::new( - db, - function.expect_function_literal(), - builtins_class.to_instance(db).unwrap(), - ))) -} - -impl Ty { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { - match self { - Ty::Never => Type::Never, - Ty::Unknown => Type::unknown(), - Ty::None => Type::none(db), - Ty::Any => Type::any(), - Ty::IntLiteral(n) => Type::IntLiteral(n), - Ty::StringLiteral(s) => Type::string_literal(db, s), - Ty::BooleanLiteral(b) => Type::BooleanLiteral(b), - Ty::LiteralString => Type::LiteralString, - Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), - Ty::BuiltinInstance(s) => builtins_symbol(db, s) - .symbol - .expect_type() - .to_instance(db) - .unwrap(), - Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) - .symbol - .expect_type() - .to_instance(db) - .unwrap(), - Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) - .symbol - .expect_type(), - Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal), - Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).symbol.expect_type(), - Ty::KnownClassInstance(known_class) => known_class.to_instance(db), - Ty::Union(tys) => { - UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) - } - Ty::Intersection { pos, neg } => { - let mut builder = IntersectionBuilder::new(db); - for p in pos { - builder = builder.add_positive(p.into_type(db)); - } - for n in neg { - builder = builder.add_negative(n.into_type(db)); - } - builder.build() - } - Ty::Tuple(tys) => { - let elements = tys.into_iter().map(|ty| ty.into_type(db)); - TupleType::from_elements(db, elements) - } - Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), - Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( - db, - builtins_symbol(db, s) - .symbol - .expect_type() - .expect_class_literal() - .class, - ), - Ty::SubclassOfAbcClass(s) => SubclassOfType::from( - db, - known_module_symbol(db, KnownModule::Abc, s) - .symbol - .expect_type() - .expect_class_literal() - .class, - ), - Ty::AlwaysTruthy => Type::AlwaysTruthy, - Ty::AlwaysFalsy => Type::AlwaysFalsy, - Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(), - Ty::BuiltinsBoundMethod { class, method } => { - let builtins_class = builtins_symbol(db, class).symbol.expect_type(); - let function = builtins_class.member(db, method).symbol.expect_type(); - - create_bound_method(db, function, builtins_class) - } - } - } -} - -fn arbitrary_core_type(g: &mut Gen) -> Ty { - // We could select a random integer here, but this would make it much less - // likely to explore interesting edge cases: - let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap()); - let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g)); - g.choose(&[ - Ty::Never, - Ty::Unknown, - Ty::None, - Ty::Any, - int_lit, - bool_lit, - Ty::StringLiteral(""), - Ty::StringLiteral("a"), - Ty::LiteralString, - Ty::BytesLiteral(""), - Ty::BytesLiteral("\x00"), - Ty::KnownClassInstance(KnownClass::Object), - Ty::KnownClassInstance(KnownClass::Str), - Ty::KnownClassInstance(KnownClass::Int), - Ty::KnownClassInstance(KnownClass::Bool), - Ty::KnownClassInstance(KnownClass::List), - Ty::KnownClassInstance(KnownClass::Tuple), - Ty::KnownClassInstance(KnownClass::FunctionType), - Ty::KnownClassInstance(KnownClass::SpecialForm), - Ty::KnownClassInstance(KnownClass::TypeVar), - Ty::KnownClassInstance(KnownClass::TypeAliasType), - Ty::KnownClassInstance(KnownClass::NoDefaultType), - Ty::TypingLiteral, - Ty::BuiltinClassLiteral("str"), - Ty::BuiltinClassLiteral("int"), - Ty::BuiltinClassLiteral("bool"), - Ty::BuiltinClassLiteral("object"), - Ty::BuiltinInstance("type"), - Ty::AbcInstance("ABC"), - Ty::AbcInstance("ABCMeta"), - Ty::SubclassOfAny, - Ty::SubclassOfBuiltinClass("object"), - Ty::SubclassOfBuiltinClass("str"), - Ty::SubclassOfBuiltinClass("type"), - Ty::AbcClassLiteral("ABC"), - Ty::AbcClassLiteral("ABCMeta"), - Ty::SubclassOfAbcClass("ABC"), - Ty::SubclassOfAbcClass("ABCMeta"), - Ty::AlwaysTruthy, - Ty::AlwaysFalsy, - Ty::BuiltinsFunction("chr"), - Ty::BuiltinsFunction("ascii"), - Ty::BuiltinsBoundMethod { - class: "str", - method: "isascii", - }, - Ty::BuiltinsBoundMethod { - class: "int", - method: "bit_length", - }, - ]) - .unwrap() - .clone() -} - -/// Constructs an arbitrary type. -/// -/// The `size` parameter controls the depth of the type tree. For example, -/// a simple type like `int` has a size of 0, `Union[int, str]` has a size -/// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc. -fn arbitrary_type(g: &mut Gen, size: u32) -> Ty { - if size == 0 { - arbitrary_core_type(g) - } else { - match u32::arbitrary(g) % 4 { - 0 => arbitrary_core_type(g), - 1 => Ty::Union( - (0..*g.choose(&[2, 3]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - ), - 2 => Ty::Tuple( - (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - ), - 3 => Ty::Intersection { - pos: (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - neg: (0..*g.choose(&[0, 1, 2]).unwrap()) - .map(|_| arbitrary_type(g, size - 1)) - .collect(), - }, - _ => unreachable!(), - } - } -} - -impl Arbitrary for Ty { - fn arbitrary(g: &mut Gen) -> Ty { - const MAX_SIZE: u32 = 2; - arbitrary_type(g, MAX_SIZE) - } - - fn shrink(&self) -> Box> { - match self.clone() { - Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { - 0 => None, - 1 => Some(elts.into_iter().next().unwrap()), - _ => Some(Ty::Union(elts)), - })), - Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { - 0 => None, - 1 => Some(elts.into_iter().next().unwrap()), - _ => Some(Ty::Tuple(elts)), - })), - Ty::Intersection { pos, neg } => { - // Shrinking on intersections is not exhaustive! - // - // We try to shrink the positive side or the negative side, - // but we aren't shrinking both at the same time. - // - // This should remove positive or negative constraints but - // won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink - // iteration. - // - // Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails - // so that shrinking can happen there. - let pos_orig = pos.clone(); - let neg_orig = neg.clone(); - Box::new( - // we shrink negative constraints first, as - // intersections with only negative constraints are - // more confusing - neg.shrink() - .map(move |shrunk_neg| Ty::Intersection { - pos: pos_orig.clone(), - neg: shrunk_neg, - }) - .chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection { - pos: shrunk_pos, - neg: neg_orig.clone(), - })) - .filter_map(|ty| { - if let Ty::Intersection { pos, neg } = &ty { - match (pos.len(), neg.len()) { - // an empty intersection does not mean - // anything - (0, 0) => None, - // a single positive element should be - // unwrapped - (1, 0) => Some(pos[0].clone()), - _ => Some(ty), - } - } else { - unreachable!() - } - }), - ) - } - _ => Box::new(std::iter::empty()), - } - } -} - -static CACHED_DB: OnceLock>> = OnceLock::new(); - -fn get_cached_db() -> TestDb { - let db = CACHED_DB.get_or_init(|| Arc::new(Mutex::new(setup_db()))); - db.lock().unwrap().clone() -} +use type_generation::{intersection, union}; /// A macro to define a property test for types. /// @@ -347,8 +43,8 @@ macro_rules! type_property_test { ($test_name:ident, $db:ident, forall types $($types:ident),+ . $property:expr) => { #[quickcheck_macros::quickcheck] #[ignore] - fn $test_name($($types: super::Ty),+) -> bool { - let $db = &super::get_cached_db(); + fn $test_name($($types: crate::types::property_tests::type_generation::Ty),+) -> bool { + let $db = &crate::types::property_tests::setup::get_cached_db(); $(let $types = $types.into_type($db);)+ $property @@ -360,18 +56,6 @@ macro_rules! type_property_test { }; } -fn intersection<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); - for ty in tys { - builder = builder.add_positive(ty); - } - builder.build() -} - -fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { - UnionType::from_elements(db, tys) -} - mod stable { use super::union; use crate::types::Type; diff --git a/crates/red_knot_python_semantic/src/types/property_tests/setup.rs b/crates/red_knot_python_semantic/src/types/property_tests/setup.rs new file mode 100644 index 0000000000..a64ad2e46e --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/property_tests/setup.rs @@ -0,0 +1,9 @@ +use crate::db::tests::{setup_db, TestDb}; +use std::sync::{Arc, Mutex, OnceLock}; + +static CACHED_DB: OnceLock>> = OnceLock::new(); + +pub(crate) fn get_cached_db() -> TestDb { + let db = CACHED_DB.get_or_init(|| Arc::new(Mutex::new(setup_db()))); + db.lock().unwrap().clone() +} diff --git a/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs new file mode 100644 index 0000000000..aa5b65967f --- /dev/null +++ b/crates/red_knot_python_semantic/src/types/property_tests/type_generation.rs @@ -0,0 +1,313 @@ +use crate::db::tests::TestDb; +use crate::symbol::{builtins_symbol, known_module_symbol}; +use crate::types::{ + BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, + SubclassOfType, TupleType, Type, UnionType, +}; +use crate::{Db, KnownModule}; +use quickcheck::{Arbitrary, Gen}; + +/// A test representation of a type that can be transformed unambiguously into a real Type, +/// given a db. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Ty { + Never, + Unknown, + None, + Any, + IntLiteral(i64), + BooleanLiteral(bool), + StringLiteral(&'static str), + LiteralString, + BytesLiteral(&'static str), + // BuiltinInstance("str") corresponds to an instance of the builtin `str` class + BuiltinInstance(&'static str), + /// Members of the `abc` stdlib module + AbcInstance(&'static str), + AbcClassLiteral(&'static str), + TypingLiteral, + // BuiltinClassLiteral("str") corresponds to the builtin `str` class object itself + BuiltinClassLiteral(&'static str), + KnownClassInstance(KnownClass), + Union(Vec), + Intersection { + pos: Vec, + neg: Vec, + }, + Tuple(Vec), + SubclassOfAny, + SubclassOfBuiltinClass(&'static str), + SubclassOfAbcClass(&'static str), + AlwaysTruthy, + AlwaysFalsy, + BuiltinsFunction(&'static str), + BuiltinsBoundMethod { + class: &'static str, + method: &'static str, + }, +} + +#[salsa::tracked] +fn create_bound_method<'db>( + db: &'db dyn Db, + function: Type<'db>, + builtins_class: Type<'db>, +) -> Type<'db> { + Type::Callable(CallableType::BoundMethod(BoundMethodType::new( + db, + function.expect_function_literal(), + builtins_class.to_instance(db).unwrap(), + ))) +} + +impl Ty { + pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + match self { + Ty::Never => Type::Never, + Ty::Unknown => Type::unknown(), + Ty::None => Type::none(db), + Ty::Any => Type::any(), + Ty::IntLiteral(n) => Type::IntLiteral(n), + Ty::StringLiteral(s) => Type::string_literal(db, s), + Ty::BooleanLiteral(b) => Type::BooleanLiteral(b), + Ty::LiteralString => Type::LiteralString, + Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), + Ty::BuiltinInstance(s) => builtins_symbol(db, s) + .symbol + .expect_type() + .to_instance(db) + .unwrap(), + Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) + .symbol + .expect_type() + .to_instance(db) + .unwrap(), + Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) + .symbol + .expect_type(), + Ty::TypingLiteral => Type::KnownInstance(KnownInstanceType::Literal), + Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).symbol.expect_type(), + Ty::KnownClassInstance(known_class) => known_class.to_instance(db), + Ty::Union(tys) => { + UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) + } + Ty::Intersection { pos, neg } => { + let mut builder = IntersectionBuilder::new(db); + for p in pos { + builder = builder.add_positive(p.into_type(db)); + } + for n in neg { + builder = builder.add_negative(n.into_type(db)); + } + builder.build() + } + Ty::Tuple(tys) => { + let elements = tys.into_iter().map(|ty| ty.into_type(db)); + TupleType::from_elements(db, elements) + } + Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), + Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( + db, + builtins_symbol(db, s) + .symbol + .expect_type() + .expect_class_literal() + .class, + ), + Ty::SubclassOfAbcClass(s) => SubclassOfType::from( + db, + known_module_symbol(db, KnownModule::Abc, s) + .symbol + .expect_type() + .expect_class_literal() + .class, + ), + Ty::AlwaysTruthy => Type::AlwaysTruthy, + Ty::AlwaysFalsy => Type::AlwaysFalsy, + Ty::BuiltinsFunction(name) => builtins_symbol(db, name).symbol.expect_type(), + Ty::BuiltinsBoundMethod { class, method } => { + let builtins_class = builtins_symbol(db, class).symbol.expect_type(); + let function = builtins_class.member(db, method).symbol.expect_type(); + + create_bound_method(db, function, builtins_class) + } + } + } +} + +fn arbitrary_core_type(g: &mut Gen) -> Ty { + // We could select a random integer here, but this would make it much less + // likely to explore interesting edge cases: + let int_lit = Ty::IntLiteral(*g.choose(&[-2, -1, 0, 1, 2]).unwrap()); + let bool_lit = Ty::BooleanLiteral(bool::arbitrary(g)); + g.choose(&[ + Ty::Never, + Ty::Unknown, + Ty::None, + Ty::Any, + int_lit, + bool_lit, + Ty::StringLiteral(""), + Ty::StringLiteral("a"), + Ty::LiteralString, + Ty::BytesLiteral(""), + Ty::BytesLiteral("\x00"), + Ty::KnownClassInstance(KnownClass::Object), + Ty::KnownClassInstance(KnownClass::Str), + Ty::KnownClassInstance(KnownClass::Int), + Ty::KnownClassInstance(KnownClass::Bool), + Ty::KnownClassInstance(KnownClass::List), + Ty::KnownClassInstance(KnownClass::Tuple), + Ty::KnownClassInstance(KnownClass::FunctionType), + Ty::KnownClassInstance(KnownClass::SpecialForm), + Ty::KnownClassInstance(KnownClass::TypeVar), + Ty::KnownClassInstance(KnownClass::TypeAliasType), + Ty::KnownClassInstance(KnownClass::NoDefaultType), + Ty::TypingLiteral, + Ty::BuiltinClassLiteral("str"), + Ty::BuiltinClassLiteral("int"), + Ty::BuiltinClassLiteral("bool"), + Ty::BuiltinClassLiteral("object"), + Ty::BuiltinInstance("type"), + Ty::AbcInstance("ABC"), + Ty::AbcInstance("ABCMeta"), + Ty::SubclassOfAny, + Ty::SubclassOfBuiltinClass("object"), + Ty::SubclassOfBuiltinClass("str"), + Ty::SubclassOfBuiltinClass("type"), + Ty::AbcClassLiteral("ABC"), + Ty::AbcClassLiteral("ABCMeta"), + Ty::SubclassOfAbcClass("ABC"), + Ty::SubclassOfAbcClass("ABCMeta"), + Ty::AlwaysTruthy, + Ty::AlwaysFalsy, + Ty::BuiltinsFunction("chr"), + Ty::BuiltinsFunction("ascii"), + Ty::BuiltinsBoundMethod { + class: "str", + method: "isascii", + }, + Ty::BuiltinsBoundMethod { + class: "int", + method: "bit_length", + }, + ]) + .unwrap() + .clone() +} + +/// Constructs an arbitrary type. +/// +/// The `size` parameter controls the depth of the type tree. For example, +/// a simple type like `int` has a size of 0, `Union[int, str]` has a size +/// of 1, `tuple[int, Union[str, bytes]]` has a size of 2, etc. +fn arbitrary_type(g: &mut Gen, size: u32) -> Ty { + if size == 0 { + arbitrary_core_type(g) + } else { + match u32::arbitrary(g) % 4 { + 0 => arbitrary_core_type(g), + 1 => Ty::Union( + (0..*g.choose(&[2, 3]).unwrap()) + .map(|_| arbitrary_type(g, size - 1)) + .collect(), + ), + 2 => Ty::Tuple( + (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1)) + .collect(), + ), + 3 => Ty::Intersection { + pos: (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1)) + .collect(), + neg: (0..*g.choose(&[0, 1, 2]).unwrap()) + .map(|_| arbitrary_type(g, size - 1)) + .collect(), + }, + _ => unreachable!(), + } + } +} + +impl Arbitrary for Ty { + fn arbitrary(g: &mut Gen) -> Ty { + const MAX_SIZE: u32 = 2; + arbitrary_type(g, MAX_SIZE) + } + + fn shrink(&self) -> Box> { + match self.clone() { + Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { + 0 => None, + 1 => Some(elts.into_iter().next().unwrap()), + _ => Some(Ty::Union(elts)), + })), + Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() { + 0 => None, + 1 => Some(elts.into_iter().next().unwrap()), + _ => Some(Ty::Tuple(elts)), + })), + Ty::Intersection { pos, neg } => { + // Shrinking on intersections is not exhaustive! + // + // We try to shrink the positive side or the negative side, + // but we aren't shrinking both at the same time. + // + // This should remove positive or negative constraints but + // won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink + // iteration. + // + // Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails + // so that shrinking can happen there. + let pos_orig = pos.clone(); + let neg_orig = neg.clone(); + Box::new( + // we shrink negative constraints first, as + // intersections with only negative constraints are + // more confusing + neg.shrink() + .map(move |shrunk_neg| Ty::Intersection { + pos: pos_orig.clone(), + neg: shrunk_neg, + }) + .chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection { + pos: shrunk_pos, + neg: neg_orig.clone(), + })) + .filter_map(|ty| { + if let Ty::Intersection { pos, neg } = &ty { + match (pos.len(), neg.len()) { + // an empty intersection does not mean + // anything + (0, 0) => None, + // a single positive element should be + // unwrapped + (1, 0) => Some(pos[0].clone()), + _ => Some(ty), + } + } else { + unreachable!() + } + }), + ) + } + _ => Box::new(std::iter::empty()), + } + } +} + +pub(crate) fn intersection<'db>( + db: &'db TestDb, + tys: impl IntoIterator>, +) -> Type<'db> { + let mut builder = IntersectionBuilder::new(db); + for ty in tys { + builder = builder.add_positive(ty); + } + builder.build() +} + +pub(crate) fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { + UnionType::from_elements(db, tys) +}