|
14 | 14 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 | use std::collections::BTreeSet; |
16 | 16 |
|
| 17 | +use rstest::rstest; |
17 | 18 | use stacks_common::types::StacksEpochId; |
18 | 19 |
|
19 | 20 | use crate::errors::ClarityTypeError; |
@@ -1059,3 +1060,165 @@ fn test_least_supertype() { |
1059 | 1060 | ); |
1060 | 1061 | } |
1061 | 1062 | } |
| 1063 | + |
| 1064 | +fn trait_id(contract: &str, name: &str) -> TraitIdentifier { |
| 1065 | + TraitIdentifier { |
| 1066 | + name: ClarityName::try_from(name.to_string()).unwrap(), |
| 1067 | + contract_identifier: QualifiedContractIdentifier::local(contract).unwrap(), |
| 1068 | + } |
| 1069 | +} |
| 1070 | + |
| 1071 | +fn callable_principal_subtype(contract: &str) -> CallableSubtype { |
| 1072 | + CallableSubtype::Principal(QualifiedContractIdentifier::local(contract).unwrap()) |
| 1073 | +} |
| 1074 | + |
| 1075 | +fn callable_trait_subtype(contract: &str, name: &str) -> CallableSubtype { |
| 1076 | + CallableSubtype::Trait(trait_id(contract, name)) |
| 1077 | +} |
| 1078 | + |
| 1079 | +fn list_union_type(members: Vec<CallableSubtype>) -> TypeSignature { |
| 1080 | + TypeSignature::ListUnionType(members.into_iter().collect()) |
| 1081 | +} |
| 1082 | + |
| 1083 | +fn buff_type(len: u32) -> TypeSignature { |
| 1084 | + TypeSignature::SequenceType(SequenceSubtype::BufferType( |
| 1085 | + BufferLength::try_from(len).unwrap(), |
| 1086 | + )) |
| 1087 | +} |
| 1088 | + |
| 1089 | +fn ascii_type(len: u32) -> TypeSignature { |
| 1090 | + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII( |
| 1091 | + BufferLength::try_from(len).unwrap(), |
| 1092 | + ))) |
| 1093 | +} |
| 1094 | + |
| 1095 | +fn utf8_type(len: u32) -> TypeSignature { |
| 1096 | + TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::UTF8( |
| 1097 | + StringUTF8Length::try_from(len).unwrap(), |
| 1098 | + ))) |
| 1099 | +} |
| 1100 | + |
| 1101 | +/// The equal-type fast path in `parent_list_type_from_iter` is only sound if |
| 1102 | +/// `least_supertype_v2_1(T, T) == Ok(T)` for every `T`. Each case seeds one |
| 1103 | +/// leaf type; its nested compounds (~64 shapes) are generated in the body. |
| 1104 | +#[rstest] |
| 1105 | +#[case::no_type(TypeSignature::NoType)] |
| 1106 | +#[case::int(TypeSignature::IntType)] |
| 1107 | +#[case::uint(TypeSignature::UIntType)] |
| 1108 | +#[case::bool(TypeSignature::BoolType)] |
| 1109 | +#[case::principal(TypeSignature::PrincipalType)] |
| 1110 | +#[case::buffer_zero_len(buff_type(0))] |
| 1111 | +#[case::buffer(buff_type(17))] |
| 1112 | +#[case::string_ascii(ascii_type(5))] |
| 1113 | +#[case::string_utf8(utf8_type(3))] |
| 1114 | +#[case::callable_principal(TypeSignature::CallableType(callable_principal_subtype("contract-a")))] |
| 1115 | +#[case::callable_trait(TypeSignature::CallableType(callable_trait_subtype( |
| 1116 | + "contract-a", |
| 1117 | + "trait-a" |
| 1118 | +)))] |
| 1119 | +#[case::trait_reference(TypeSignature::TraitReferenceType(trait_id("contract-a", "trait-a")))] |
| 1120 | +#[case::list_union_single(list_union_type(vec![callable_principal_subtype("contract-a")]))] |
| 1121 | +#[case::list_union_mixed(list_union_type(vec![ |
| 1122 | + callable_principal_subtype("contract-a"), |
| 1123 | + callable_principal_subtype("contract-b"), |
| 1124 | + callable_trait_subtype("contract-a", "trait-a"), |
| 1125 | + callable_trait_subtype("contract-b", "trait-b"), |
| 1126 | +]))] |
| 1127 | +#[case::empty_list(TypeSignature::from(TypeSignature::empty_list()))] |
| 1128 | +fn test_least_supertype_v2_1_idempotent(#[case] leaf: TypeSignature) { |
| 1129 | + use crate::types::TypeSignature::*; |
| 1130 | + |
| 1131 | + let mut corpus = vec![leaf]; |
| 1132 | + // two wrapping rounds cover nested compounds |
| 1133 | + for _ in 0..2 { |
| 1134 | + let snapshot = corpus.clone(); |
| 1135 | + for t in snapshot { |
| 1136 | + corpus.push(OptionalType(Box::new(t.clone()))); |
| 1137 | + corpus.push(ResponseType(Box::new((t.clone(), NoType)))); |
| 1138 | + corpus.push(ResponseType(Box::new((NoType, t.clone())))); |
| 1139 | + corpus.push(ResponseType(Box::new((t.clone(), t.clone())))); |
| 1140 | + corpus.push(TypeSignature::list_of(t.clone(), 0).unwrap()); |
| 1141 | + corpus.push(TypeSignature::list_of(t.clone(), 4).unwrap()); |
| 1142 | + corpus.push( |
| 1143 | + TupleTypeSignature::try_from(vec![( |
| 1144 | + ClarityName::try_from("field-a".to_string()).unwrap(), |
| 1145 | + t, |
| 1146 | + )]) |
| 1147 | + .unwrap() |
| 1148 | + .into(), |
| 1149 | + ); |
| 1150 | + } |
| 1151 | + } |
| 1152 | + |
| 1153 | + for t in &corpus { |
| 1154 | + assert_eq!( |
| 1155 | + TypeSignature::least_supertype_v2_1(t, t).as_ref(), |
| 1156 | + Ok(t), |
| 1157 | + "least_supertype_v2_1 must be idempotent for {t:?}" |
| 1158 | + ); |
| 1159 | + } |
| 1160 | +} |
| 1161 | + |
| 1162 | +fn buff_value(len: usize) -> Value { |
| 1163 | + Value::buff_from(vec![0; len]).unwrap() |
| 1164 | +} |
| 1165 | + |
| 1166 | +fn ascii_value(s: &str) -> Value { |
| 1167 | + Value::string_ascii_from_bytes(s.as_bytes().to_vec()).unwrap() |
| 1168 | +} |
| 1169 | + |
| 1170 | +fn callable_value(contract: &str) -> Value { |
| 1171 | + Value::CallableContract(CallableData { |
| 1172 | + contract_identifier: QualifiedContractIdentifier::local(contract).unwrap(), |
| 1173 | + trait_identifier: None, |
| 1174 | + }) |
| 1175 | +} |
| 1176 | + |
| 1177 | +fn tuple_value(v: Value) -> Value { |
| 1178 | + Value::from( |
| 1179 | + TupleData::from_data(vec![( |
| 1180 | + ClarityName::try_from("field-a".to_string()).unwrap(), |
| 1181 | + v, |
| 1182 | + )]) |
| 1183 | + .unwrap(), |
| 1184 | + ) |
| 1185 | +} |
| 1186 | + |
| 1187 | +/// `construct_parent_list_type` and `parent_list_type` must stay equivalent |
| 1188 | +/// for values satisfying constructor invariants, including identical errors. |
| 1189 | +#[rstest] |
| 1190 | +#[case::empty(vec![])] |
| 1191 | +#[case::homogeneous_bools(vec![Value::Bool(true), Value::Bool(false), Value::Bool(true)])] |
| 1192 | +#[case::homogeneous_ints(vec![Value::Int(1), Value::Int(2)])] |
| 1193 | +#[case::int_uint_mismatch(vec![Value::Int(1), Value::UInt(2)])] |
| 1194 | +#[case::bool_int_mismatch(vec![Value::Bool(true), Value::Int(1)])] |
| 1195 | +#[case::buffers_different_lengths(vec![buff_value(3), buff_value(5)])] |
| 1196 | +#[case::ascii_different_lengths(vec![ascii_value("abc"), ascii_value("defgh")])] |
| 1197 | +#[case::identical_callables(vec![callable_value("contract-a"), callable_value("contract-a")])] |
| 1198 | +#[case::distinct_callables(vec![callable_value("contract-a"), callable_value("contract-b")])] |
| 1199 | +#[case::callable_with_standard_principal(vec![ |
| 1200 | + callable_value("contract-a"), |
| 1201 | + Value::from(StandardPrincipalData::transient()), |
| 1202 | +])] |
| 1203 | +#[case::optional_none_and_some(vec![Value::none(), Value::some(Value::Int(5)).unwrap()])] |
| 1204 | +#[case::response_ok_and_err(vec![ |
| 1205 | + Value::okay(Value::Int(1)).unwrap(), |
| 1206 | + Value::error(Value::Bool(false)).unwrap(), |
| 1207 | +])] |
| 1208 | +#[case::empty_and_nonempty_lists(vec![ |
| 1209 | + Value::cons_list_unsanitized(vec![]).unwrap(), |
| 1210 | + Value::cons_list_unsanitized(vec![Value::Int(3)]).unwrap(), |
| 1211 | +])] |
| 1212 | +#[case::tuple_field_supertype(vec![tuple_value(buff_value(3)), tuple_value(buff_value(5))])] |
| 1213 | +fn test_construct_parent_list_type_matches_parent_list_type(#[case] values: Vec<Value>) { |
| 1214 | + let streamed = TypeSignature::construct_parent_list_type(&values); |
| 1215 | + let children: Vec<_> = values |
| 1216 | + .iter() |
| 1217 | + .map(|v| TypeSignature::type_of(v).unwrap()) |
| 1218 | + .collect(); |
| 1219 | + let collected = TypeSignature::parent_list_type(&children); |
| 1220 | + assert_eq!( |
| 1221 | + streamed, collected, |
| 1222 | + "construct_parent_list_type diverged from parent_list_type for {values:?}" |
| 1223 | + ); |
| 1224 | +} |
0 commit comments