Skip to content

Commit 292af80

Browse files
authored
feat: Open lists and tuples in Term (#2360)
This PR introduces open lists and tuples in `core` terms. The `Term` type is extended with `Term::ListConcat` and `Term::TupleConcat` variants and helper methods are added to deal with them. Closes #2294.
1 parent 909a794 commit 292af80

File tree

12 files changed

+592
-114
lines changed

12 files changed

+592
-114
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hugr-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ semver = { workspace = true, features = ["serde"] }
6363
zstd = { workspace = true, optional = true }
6464
ordered-float = { workspace = true, features = ["serde"] }
6565
base64.workspace = true
66+
relrc = { workspace = true, features = ["petgraph", "serde"] }
67+
smallvec = "1.15.0"
68+
tracing = "0.1.41"
6669

6770
[dev-dependencies]
6871
rstest = { workspace = true }

hugr-core/src/export.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -945,22 +945,17 @@ impl<'a> Context<'a> {
945945

946946
self.make_term_apply(model::CORE_TYPE, &[])
947947
}
948-
Term::BoundedNatType { .. } => self.make_term_apply(model::CORE_NAT_TYPE, &[]),
948+
Term::BoundedNatType(_) => self.make_term_apply(model::CORE_NAT_TYPE, &[]),
949949
Term::StringType => self.make_term_apply(model::CORE_STR_TYPE, &[]),
950950
Term::BytesType => self.make_term_apply(model::CORE_BYTES_TYPE, &[]),
951951
Term::FloatType => self.make_term_apply(model::CORE_FLOAT_TYPE, &[]),
952952
Term::ListType(item_type) => {
953953
let item_type = self.export_term(item_type, None);
954954
self.make_term_apply(model::CORE_LIST_TYPE, &[item_type])
955955
}
956-
Term::TupleType(params) => {
957-
let item_types = self.bump.alloc_slice_fill_iter(
958-
params
959-
.iter()
960-
.map(|param| table::SeqPart::Item(self.export_term(param, None))),
961-
);
962-
let types = self.make_term(table::Term::List(item_types));
963-
self.make_term_apply(model::CORE_TUPLE_TYPE, &[types])
956+
Term::TupleType(item_types) => {
957+
let item_types = self.export_term(item_types, None);
958+
self.make_term_apply(model::CORE_TUPLE_TYPE, &[item_types])
964959
}
965960
Term::Runtime(ty) => self.export_type(ty),
966961
Term::BoundedNat(value) => self.make_term(model::Literal::Nat(*value).into()),
@@ -975,6 +970,14 @@ impl<'a> Context<'a> {
975970
);
976971
self.make_term(table::Term::List(parts))
977972
}
973+
Term::ListConcat(lists) => {
974+
let parts = self.bump.alloc_slice_fill_iter(
975+
lists
976+
.iter()
977+
.map(|elem| table::SeqPart::Splice(self.export_term(elem, None))),
978+
);
979+
self.make_term(table::Term::List(parts))
980+
}
978981
Term::Tuple(elems) => {
979982
let parts = self.bump.alloc_slice_fill_iter(
980983
elems
@@ -983,6 +986,14 @@ impl<'a> Context<'a> {
983986
);
984987
self.make_term(table::Term::Tuple(parts))
985988
}
989+
Term::TupleConcat(tuples) => {
990+
let parts = self.bump.alloc_slice_fill_iter(
991+
tuples
992+
.iter()
993+
.map(|elem| table::SeqPart::Splice(self.export_term(elem, None))),
994+
);
995+
self.make_term(table::Term::Tuple(parts))
996+
}
986997
Term::Variable(v) => self.export_type_arg_var(v),
987998
Term::StaticType => self.make_term_apply(model::CORE_STATIC, &[]),
988999
}

hugr-core/src/extension/resolution/types.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,16 @@ pub(super) fn collect_term_exts(
231231
collect_term_exts(item_type, used_extensions, missing_extensions)
232232
}
233233
Term::TupleType(item_types) => {
234-
for item_type in item_types {
235-
collect_term_exts(item_type, used_extensions, missing_extensions);
234+
collect_term_exts(item_types, used_extensions, missing_extensions)
235+
}
236+
Term::ListConcat(lists) => {
237+
for list in lists {
238+
collect_term_exts(list, used_extensions, missing_extensions);
239+
}
240+
}
241+
Term::TupleConcat(tuples) => {
242+
for tuple in tuples {
243+
collect_term_exts(tuple, used_extensions, missing_extensions);
236244
}
237245
}
238246
Term::Variable(_)

hugr-core/src/extension/resolution/types_mut.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -222,23 +222,19 @@ pub(super) fn resolve_term_exts(
222222
) -> Result<(), ExtensionResolutionError> {
223223
match term {
224224
Term::Runtime(ty) => resolve_type_exts(node, ty, extensions, used_extensions)?,
225-
Term::List(elems) => {
226-
for elem in elems.iter_mut() {
227-
resolve_term_exts(node, elem, extensions, used_extensions)?;
228-
}
229-
}
230-
Term::Tuple(elems) => {
231-
for elem in elems.iter_mut() {
232-
resolve_term_exts(node, elem, extensions, used_extensions)?;
225+
Term::List(children)
226+
| Term::ListConcat(children)
227+
| Term::Tuple(children)
228+
| Term::TupleConcat(children) => {
229+
for child in children.iter_mut() {
230+
resolve_term_exts(node, child, extensions, used_extensions)?;
233231
}
234232
}
235233
Term::ListType(item_type) => {
236-
resolve_term_exts(node, item_type, extensions, used_extensions)?;
234+
resolve_term_exts(node, item_type.as_mut(), extensions, used_extensions)?;
237235
}
238236
Term::TupleType(item_types) => {
239-
for item_type in item_types.iter_mut() {
240-
resolve_term_exts(node, item_type, extensions, used_extensions)?;
241-
}
237+
resolve_term_exts(node, item_types.as_mut(), extensions, used_extensions)?;
242238
}
243239
Term::Variable(_)
244240
| Term::RuntimeType(_)

hugr-core/src/hugr/serialize/test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ fn polyfunctype2() -> PolyFuncTypeRV {
482482
#[case(PolyFuncType::new([TypeParam::StringType], Signature::new_endo(vec![Type::new_var_use(0, TypeBound::Copyable)])))]
483483
#[case(PolyFuncType::new([TypeBound::Copyable.into()], Signature::new_endo(vec![Type::new_var_use(0, TypeBound::Copyable)])))]
484484
#[case(PolyFuncType::new([TypeParam::new_list_type(TypeBound::Any)], Signature::new_endo(type_row![])))]
485-
#[case(PolyFuncType::new([TypeParam::TupleType([TypeBound::Any.into(), TypeParam::bounded_nat_type(2.try_into().unwrap())].into())], Signature::new_endo(type_row![])))]
485+
#[case(PolyFuncType::new([TypeParam::new_tuple_type([TypeBound::Any.into(), TypeParam::bounded_nat_type(2.try_into().unwrap())])], Signature::new_endo(type_row![])))]
486486
#[case(PolyFuncType::new(
487487
[TypeParam::new_list_type(TypeBound::Any)],
488488
Signature::new_endo(Type::new_tuple(TypeRV::new_row_var_use(0, TypeBound::Any)))))]
@@ -495,7 +495,7 @@ fn roundtrip_polyfunctype_fixedlen(#[case] poly_func_type: PolyFuncType) {
495495
#[case(PolyFuncTypeRV::new([TypeParam::StringType], FuncValueType::new_endo(vec![Type::new_var_use(0, TypeBound::Copyable)])))]
496496
#[case(PolyFuncTypeRV::new([TypeBound::Copyable.into()], FuncValueType::new_endo(vec![Type::new_var_use(0, TypeBound::Copyable)])))]
497497
#[case(PolyFuncTypeRV::new([TypeParam::new_list_type(TypeBound::Any)], FuncValueType::new_endo(type_row![])))]
498-
#[case(PolyFuncTypeRV::new([TypeParam::TupleType([TypeBound::Any.into(), TypeParam::bounded_nat_type(2.try_into().unwrap())].into())], FuncValueType::new_endo(type_row![])))]
498+
#[case(PolyFuncTypeRV::new([TypeParam::new_tuple_type([TypeBound::Any.into(), TypeParam::bounded_nat_type(2.try_into().unwrap())])], FuncValueType::new_endo(type_row![])))]
499499
#[case(PolyFuncTypeRV::new(
500500
[TypeParam::new_list_type(TypeBound::Any)],
501501
FuncValueType::new_endo(TypeRV::new_row_var_use(0, TypeBound::Any))))]

hugr-core/src/import.rs

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ use crate::{
2525
types::{
2626
CustomType, FuncTypeBase, MaybeRV, PolyFuncType, PolyFuncTypeBase, RowVariable, Signature,
2727
Term, Type, TypeArg, TypeBase, TypeBound, TypeEnum, TypeName, TypeRow,
28-
type_param::TypeParam, type_row::TypeRowBase,
28+
type_param::{SeqPart, TypeParam},
29+
type_row::TypeRowBase,
2930
},
3031
};
3132
use fxhash::FxHashMap;
3233
use hugr_model::v0 as model;
3334
use hugr_model::v0::table;
34-
use itertools::Either;
35+
use itertools::{Either, Itertools};
3536
use smol_str::{SmolStr, ToSmolStr};
3637
use thiserror::Error;
3738

@@ -1255,14 +1256,10 @@ impl<'a> Context<'a> {
12551256
if let Some([item_types]) = self.match_symbol(term_id, model::CORE_TUPLE_TYPE)? {
12561257
// At present `hugr-model` has no way to express that the item
12571258
// types of a tuple must be copyable. Therefore we import it as `Any`.
1258-
let item_types = (|| {
1259-
self.import_closed_list(item_types)?
1260-
.into_iter()
1261-
.map(|param| self.import_term(param))
1262-
.collect::<Result<_, _>>()
1263-
})()
1264-
.map_err(|err| error_context!(err, "item types of tuple type"))?;
1265-
return Ok(TypeParam::TupleType(item_types));
1259+
let item_types = self
1260+
.import_term(item_types)
1261+
.map_err(|err| error_context!(err, "item types of tuple type"))?;
1262+
return Ok(TypeParam::new_tuple_type(item_types));
12661263
}
12671264

12681265
match self.get_term(term_id)? {
@@ -1277,28 +1274,24 @@ impl<'a> Context<'a> {
12771274
Ok(Term::new_var_use(var.1 as _, decl))
12781275
}
12791276

1280-
table::Term::List { .. } => {
1281-
let elems = (|| {
1282-
self.import_closed_list(term_id)?
1283-
.iter()
1284-
.map(|item| self.import_term(*item))
1285-
.collect::<Result<_, _>>()
1286-
})()
1287-
.map_err(|err| error_context!(err, "list items"))?;
1288-
1289-
Ok(Term::List(elems))
1277+
table::Term::List(parts) => {
1278+
// PERFORMANCE: Can we do this without the additional allocation?
1279+
let parts: Vec<_> = parts
1280+
.iter()
1281+
.map(|part| self.import_seq_part(part))
1282+
.collect::<Result<_, _>>()
1283+
.map_err(|err| error_context!(err, "list parts"))?;
1284+
Ok(TypeArg::new_list_from_parts(parts))
12901285
}
12911286

1292-
table::Term::Tuple { .. } => {
1293-
let elems = (|| {
1294-
self.import_closed_list(term_id)?
1295-
.iter()
1296-
.map(|item| self.import_term(*item))
1297-
.collect::<Result<_, _>>()
1298-
})()
1299-
.map_err(|err| error_context!(err, "tuple items"))?;
1300-
1301-
Ok(Term::Tuple(elems))
1287+
table::Term::Tuple(parts) => {
1288+
// PERFORMANCE: Can we do this without the additional allocation?
1289+
let parts: Vec<_> = parts
1290+
.iter()
1291+
.map(|part| self.import_seq_part(part))
1292+
.try_collect()
1293+
.map_err(|err| error_context!(err, "tuple parts"))?;
1294+
Ok(TypeArg::new_tuple_from_parts(parts))
13021295
}
13031296

13041297
table::Term::Literal(model::Literal::Str(value)) => {
@@ -1322,6 +1315,16 @@ impl<'a> Context<'a> {
13221315
.map_err(|err| error_context!(err, "term {}", term_id))
13231316
}
13241317

1318+
fn import_seq_part(
1319+
&mut self,
1320+
seq_part: &'a table::SeqPart,
1321+
) -> Result<SeqPart<TypeArg>, ImportError> {
1322+
Ok(match seq_part {
1323+
table::SeqPart::Item(term_id) => SeqPart::Item(self.import_term(*term_id)?),
1324+
table::SeqPart::Splice(term_id) => SeqPart::Splice(self.import_term(*term_id)?),
1325+
})
1326+
}
1327+
13251328
/// Import a `Type` from a term that represents a runtime type.
13261329
fn import_type<RV: MaybeRV>(
13271330
&mut self,

hugr-core/src/types/poly_func.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ pub(crate) mod test {
271271
for decl in [
272272
Term::new_list_type(Term::max_nat_type()),
273273
Term::StringType,
274-
Term::TupleType(vec![TypeBound::Any.into(), Term::max_nat_type()]),
274+
Term::new_tuple_type([TypeBound::Any.into(), Term::max_nat_type()]),
275275
] {
276276
let invalid_ts = PolyFuncTypeBase::new_validated([decl.clone()], body_type.clone());
277277
assert_eq!(

hugr-core/src/types/serialize.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ pub(super) enum TypeParamSer {
7878
Float,
7979
StaticType,
8080
List { param: Box<Term> },
81-
Tuple { params: Vec<Term> },
81+
Tuple { params: ArrayOrTermSer },
8282
}
8383

8484
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
@@ -104,9 +104,15 @@ pub(super) enum TypeArgSer {
104104
List {
105105
elems: Vec<Term>,
106106
},
107+
ListConcat {
108+
lists: Vec<Term>,
109+
},
107110
Tuple {
108111
elems: Vec<Term>,
109112
},
113+
TupleConcat {
114+
tuples: Vec<Term>,
115+
},
110116
Variable {
111117
#[serde(flatten)]
112118
v: TermVar,
@@ -130,15 +136,19 @@ impl From<Term> for TermSer {
130136
Term::BytesType => TermSer::TypeParam(TypeParamSer::Bytes),
131137
Term::FloatType => TermSer::TypeParam(TypeParamSer::Float),
132138
Term::ListType(param) => TermSer::TypeParam(TypeParamSer::List { param }),
133-
Term::TupleType(params) => TermSer::TypeParam(TypeParamSer::Tuple { params }),
134139
Term::Runtime(ty) => TermSer::TypeArg(TypeArgSer::Type { ty }),
140+
Term::TupleType(params) => TermSer::TypeParam(TypeParamSer::Tuple {
141+
params: (*params).into(),
142+
}),
135143
Term::BoundedNat(n) => TermSer::TypeArg(TypeArgSer::BoundedNat { n }),
136144
Term::String(arg) => TermSer::TypeArg(TypeArgSer::String { arg }),
137145
Term::Bytes(value) => TermSer::TypeArg(TypeArgSer::Bytes { value }),
138146
Term::Float(value) => TermSer::TypeArg(TypeArgSer::Float { value }),
139147
Term::List(elems) => TermSer::TypeArg(TypeArgSer::List { elems }),
140148
Term::Tuple(elems) => TermSer::TypeArg(TypeArgSer::Tuple { elems }),
141149
Term::Variable(v) => TermSer::TypeArg(TypeArgSer::Variable { v }),
150+
Term::ListConcat(lists) => TermSer::TypeArg(TypeArgSer::ListConcat { lists }),
151+
Term::TupleConcat(tuples) => TermSer::TypeArg(TypeArgSer::TupleConcat { tuples }),
142152
}
143153
}
144154
}
@@ -154,7 +164,7 @@ impl From<TermSer> for Term {
154164
TypeParamSer::Bytes => Term::BytesType,
155165
TypeParamSer::Float => Term::FloatType,
156166
TypeParamSer::List { param } => Term::ListType(param),
157-
TypeParamSer::Tuple { params } => Term::TupleType(params),
167+
TypeParamSer::Tuple { params } => Term::TupleType(Box::new(params.into())),
158168
},
159169
TermSer::TypeArg(arg) => match arg {
160170
TypeArgSer::Type { ty } => Term::Runtime(ty),
@@ -165,11 +175,39 @@ impl From<TermSer> for Term {
165175
TypeArgSer::List { elems } => Term::List(elems),
166176
TypeArgSer::Tuple { elems } => Term::Tuple(elems),
167177
TypeArgSer::Variable { v } => Term::Variable(v),
178+
TypeArgSer::ListConcat { lists } => Term::ListConcat(lists),
179+
TypeArgSer::TupleConcat { tuples } => Term::TupleConcat(tuples),
168180
},
169181
}
170182
}
171183
}
172184

185+
/// Helper type that serialises lists as JSON arrays for compatibility.
186+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
187+
#[serde(untagged)]
188+
pub(super) enum ArrayOrTermSer {
189+
Array(Vec<Term>),
190+
Term(Box<Term>),
191+
}
192+
193+
impl From<ArrayOrTermSer> for Term {
194+
fn from(value: ArrayOrTermSer) -> Self {
195+
match value {
196+
ArrayOrTermSer::Array(terms) => Term::new_list(terms),
197+
ArrayOrTermSer::Term(term) => *term,
198+
}
199+
}
200+
}
201+
202+
impl From<Term> for ArrayOrTermSer {
203+
fn from(term: Term) -> Self {
204+
match term {
205+
Term::List(terms) => ArrayOrTermSer::Array(terms),
206+
term => ArrayOrTermSer::Term(Box::new(term)),
207+
}
208+
}
209+
}
210+
173211
/// Helper for to serialize and deserialize the byte string in [`TypeArg::Bytes`] via base64.
174212
mod base64 {
175213
use std::sync::Arc;

0 commit comments

Comments
 (0)