Skip to content

refactor(hugrv2)!: combine TypeEnum with Term, no RV parametrization#2895

Open
acl-cqc wants to merge 85 commits intomainfrom
acl/type_wraps_term
Open

refactor(hugrv2)!: combine TypeEnum with Term, no RV parametrization#2895
acl-cqc wants to merge 85 commits intomainfrom
acl/type_wraps_term

Conversation

@acl-cqc
Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc commented Feb 25, 2026

  • Merge TypeEnum into Term: remove Term::Runtime(Type), add Term::RuntimeFunction(FuncValueType), Term::RuntimeSum(SumType) and Term::RuntimeExtension(CustomType) and remove TypeEnum. That is, runtime types are now just variants of Term.
  • Type now just wraps a Term, but guarantees by construction that the Term inside it represents a single runtime type. (There is no way to bypass this; there is TryFrom<Term> for Type; there are utility constructors on Type that take Types thus preserving the invariant - the same new_extension, new_tuple, new_sum, new_function as before).
  • Remove TypeRV. It never had a consistent meaning: it was sometimes a type, and sometimes a list. Also remove trait MaybeRV, enum NoRV and struct RowVariable.
  • TypeRowRV (much like Type) wraps a Term, but guarantees it represents a list of types (of perhaps unknown length). That is, it could be a Term::List (whose elements are single types), or a Term::Variable (a "row variable" i.e. ranging over lists of types, of unknown length), or a Term::ListConcat (whose elements are one of these three). Again there are utility constructors preserving the invariant, tho there is also a TypeRowRV::new_unchecked.
    • Note prior to 0ac2b53 (see diff LHS) TypeRowRV was merely an alias to Term (with GeneralSum and FuncValueType providing panicking-new + try_new + new_unchecked constructors, removed in that commit). However I think with a few nifty TypeRowRV conversions and methods (just_row_var and concat) the wrapper-struct is easier to use as well as giving more static checking.
  • TypeRow remains as a list of Types, i.e. whose length/number-of-types is known
  • A new trait TypeRowLike allows parametrizing FuncTypeBase so it can still do both Signature and FuncValueType. I have kept this trait crate-hidden, so we keep the substitute and validate methods hidden (as before), which does mean external code cannot parametrize in this way.

Thus, although the new system offers less Rust-compile-time checking, there is still a reasonable amount, and it's more principled now ;)

Along the way,

  • Replace Type::as_type_enum with impl Deref<Target=Term> for Type, similarly for TypeRowRV
  • AliasDecl/Defn and Type::Alias dropped for now, pending Overhaul or scrap aliases #2558
  • Since TermTypeError is now used more (it's a subclass of SignatureError but captures all the errors that might happen from e.g. turning a Term into a Type), I've added impl From<TermTypeError> for OpLoadError (also for ImportErrorInner) - because Rust won't let me impl <T: From<SignatureError>> From<TermTypeError> for T :-(.
  • Generalize Term::new_list to take items that are impl Into<Term> rather than just Term

Proposed follow-ups....

  • Either remove some redundant SignatureError variants (that also appear in TermTypeError) or perhaps combine the two errors altogether. (Less urgent?)
  • Could add a utility method TypeRowRV::new_spliced(IntoIterator<Item=Term>) that checks each Term is either a type or a list of types (and panics if not....ok could also have try_new_spliced), and then assembles the appropriate lists/concats. commit 68fba45 shows this done for FuncValueType whereas here/now it would be for TypeRowRV, and I'm not sure that it's much better than the TypeRowRV::just_row_var, concat and from([Type]) that this PR has now (in particular, just_row_var/concat have better Rust-compile-time checking than this "splicing" approach). So, probably less urgent / only if TypeRowRV is too awkward in practice.

BREAKING CHANGE: TypeEnum and TypeRV are no more - use Term. RowVariable, MaybeRV etc. are gone - use appropriate term types and variables. Type and TypeRowRV merely wrap Term with invariants.

@acl-cqc acl-cqc changed the title refactor!: combine TypeEnum with Term, remove RV stuff refactor(hugrv2)!: combine TypeEnum with Term, remove RV stuff Feb 25, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Feb 25, 2026

Merging this PR will degrade performance by 41.37%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 28 untouched benchmarks
⏩ 6 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
construction 12 µs 20.4 µs -41.37%

Comparing acl/type_wraps_term (80f1934) with main (7881c99)

Open in CodSpeed

Footnotes

  1. 6 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 25, 2026

Codecov Report

❌ Patch coverage is 88.43159% with 104 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.33%. Comparing base (8fbaac0) to head (80f1934).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
hugr-core/src/import.rs 61.90% 4 Missing and 20 partials ⚠️
hugr-core/src/types/check.rs 0.00% 8 Missing ⚠️
hugr-core/src/types/serialize.rs 88.40% 6 Missing and 2 partials ⚠️
hugr-core/src/types/type_param.rs 95.69% 4 Missing and 4 partials ⚠️
hugr-core/src/extension/resolution/types_mut.rs 82.50% 0 Missing and 7 partials ⚠️
hugr-core/src/types.rs 93.87% 5 Missing and 1 partial ⚠️
hugr-core/src/types/signature.rs 78.57% 6 Missing ⚠️
hugr-core/src/types/type_row.rs 95.16% 4 Missing and 2 partials ⚠️
hugr-core/src/extension/prelude.rs 86.20% 0 Missing and 4 partials ⚠️
hugr-core/src/extension/resolution.rs 33.33% 4 Missing ⚠️
... and 15 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2895      +/-   ##
==========================================
+ Coverage   81.30%   81.33%   +0.02%     
==========================================
  Files         240      239       -1     
  Lines       45400    45271     -129     
  Branches    39168    39039     -129     
==========================================
- Hits        36913    36821      -92     
+ Misses       6497     6449      -48     
- Partials     1990     2001      +11     
Flag Coverage Δ
python 88.89% <ø> (ø)
rust 80.12% <88.43%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@acl-cqc acl-cqc changed the title refactor(hugrv2)!: combine TypeEnum with Term, remove RV stuff refactor(hugrv2)!: combine TypeEnum with Term, no RV parametrization Feb 25, 2026
Comment thread hugr-core/src/extension/prelude/unwrap_builder.rs
/// - `used_extensions`: A The registry where to store the used extensions.
/// - `missing_extensions`: A set of `ExtensionId`s of which the
/// `Weak<Extension>` pointer has been invalidated.
pub(crate) fn collect_func_type_exts(
Copy link
Copy Markdown
Contributor Author

@acl-cqc acl-cqc Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could combine these by making trait TypeRowLike (crate-private) include gathering extensions, but that would move stuff out of extension/resolution that (has) lives(/lived) in there. Thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like keeping the logic here rather than in an unrelated trait, esp. if we end up making that trait public at some point.

But it's not a strong preference.

const EMPTY: Type = Type::new_unit_sum(0); // as no Type::default()
let mut tm = std::mem::replace(typ, EMPTY).into();
let r = resolve_term_exts(node, &mut tm, extensions, used_extensions);
*typ = tm.try_into().unwrap();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we don't have a Type::new_unchecked. I guess we could add one (e.g. crate-private), or indeed an as_mut (but assuming we keep that private, not an impl DerefMut). Footguns again...but potentially resolve_type_exts(t, ...) would become resolve_term_exts(t.as_mut_term_unchecked(), ...)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this one is fine as-is. Type is 96B, so all is done on the stack.
We don't need to fumble with pointers as with TypeRowRV.

/// Adds the extensions used in the type to the `used_extensions` registry.
pub(super) fn resolve_type_exts<RV: MaybeRV>(
/// Adds the extensions used in the row to the `used_extensions` registry.
fn resolve_typerow_rv_exts(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what we get for not having a TypeRowRV::as_mut(&mut self) -> &mut Term. I mean we could; it is a footgun, but so is new_unchecked. At least the latter rings an alarm bell; perhaps we could have as_mut_term_unchecked...and keep it crate-private, but at least resolve_typerow_rv_exts(t, ...) would be resolve_term_exts(t.as_mut_term_unchecked(), ...)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think TypeRowRV::as_mut_term_unchecked would really be unchecked since there's nothing to check on the call site, right?

But I agree we should try to avoid the footgun.
An option would be to mark the function as unsafe to ensure we are careful on the call point.

Comment thread hugr-core/src/hugr/patch/simple_replace.rs
Comment thread hugr-core/src/types/type_row.rs Outdated
Comment thread hugr-core/src/types/type_row.rs Outdated
}
}

/*impl FromIterator<Type> for TypeRowRV {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would allow fn foo(t: impl Iterator<Item=Type>) -> TypeRowRV { t.collect() } and could equally (perhaps primarily) be done for TypeRow as well as TypeRowRV but I wasn't sure it added much. (If you are in that situation, currently you would have to t.collect_vec().into())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should implement this instead of <T: IntoIterator<Item = Type>> From<T> for TypeRowRV ?

Having to collect_vec seems quite annoying.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For TypeRow and TypeRowRV, I think we want From<Vec<Type>> and similarly for From<[Type;N]>, it'll be too painful otherwise. ATM we write lots of parameters as impl Into<TypeRow> so that allows e.g. someTypeRowRV.concat([type1,type2]).

Copy link
Copy Markdown
Contributor Author

@acl-cqc acl-cqc Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However the place for FromIter might be Term...will continue in thread #2895 (comment) EDIT, no totally not

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so I've replaced the impl <T: IntoIterator<Item=Type>> From<T> for TypeRowRV with impl From<Vec<Type>> and impl <const N:usize> From<[Type; N]>, because that's consistent with what we do for TypeRow and also (from array/vec of Term) for Term.

However, could also make all three use <T: IntoIterator<Item....>> ??

(I think it's good to avoid changing a list of Types into a Term without Term::new_list)

So FromIterator could still be done as a separate thing. Should be done for both TypeRow and TypeRowRV if it is for either, so done in 3a41dda - looks like an improvement??

Comment thread hugr-core/src/types.rs Outdated
Comment thread hugr-core/src/types.rs
Comment thread hugr-core/src/types.rs
TypeEnum::RowVar(rv) => rv.validate(var_decls),
}
self.0.validate(var_decls)?;
// ALAN even this should be only a debug-assert really:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, how paranoid are we feeling about the benefit of sanity checks? :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move caching of TypeBound from Type into SumType::General (via a checked struct - prior to 0ac2b53 there was such, called GeneralSum, so we could reintroduce that and add the bound there). This because of the other Type variants, CustomType stores the bound itself already, and FunctionType is always Copyable. This can come soon after I think.

If we do the move of the bound then we wouldn't need the check here, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll move into either SumType::validate or, since that doesn't currently exist ATM, into Term::validate (which currently doesn't check the elements are types at all...so that would be a better place)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #3022

@acl-cqc acl-cqc marked this pull request as ready for review April 7, 2026 11:22
@acl-cqc acl-cqc requested a review from a team as a code owner April 7, 2026 11:22
@acl-cqc acl-cqc requested a review from aborgna-q April 7, 2026 11:22
) -> Result<()> {
let hugr_elem_ty = match args.node().args() {
[TypeArg::Runtime(ty)] => ty.clone(),
[ty] => ty.clone().try_into().expect("List elements not a type"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some new expects now in the lowering code that used to return an error instead (in stack_array.rs too).

Could we bail! instead of panicking?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, hadn't seen what bail did. Nice. Done (*2, think that's all of them?)

Comment thread hugr-core/src/types.rs Outdated
Comment thread hugr-core/src/types.rs
}
}
}
pub struct Type(Term, TypeBound);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This may be an opportunity to change the tuple struct to have named fields.

type.0 doesn't tell much; type.term and type.bound would be clearer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping to do the SumType change soon after I think, it's fairly straightforward structural stuff so hopefully can be done via some ahem magic (cf. Carl Sagan)...then we'll just have pub struct Type(Term). Happy to get that ready before I merge this...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, #3022

Comment thread hugr-core/src/types.rs
TypeEnum::RowVar(rv) => rv.validate(var_decls),
}
self.0.validate(var_decls)?;
// ALAN even this should be only a debug-assert really:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move caching of TypeBound from Type into SumType::General (via a checked struct - prior to 0ac2b53 there was such, called GeneralSum, so we could reintroduce that and add the bound there). This because of the other Type variants, CustomType stores the bound itself already, and FunctionType is always Copyable. This can come soon after I think.

If we do the move of the bound then we wouldn't need the check here, right?

Comment thread hugr-core/src/types.rs
&Weak::default(),
)),
Type::new_alias(AliasDecl::new("my_alias", TypeBound::Copyable)),
//Type::new_alias(AliasDecl::new("my_alias", TypeBound::Copyable)),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//Type::new_alias(AliasDecl::new("my_alias", TypeBound::Copyable)),

#[inline]
pub fn new_list_concat(lists: impl IntoIterator<Item = Self>) -> Self {
Self::ListConcat(lists.into_iter().collect())
pub fn concat_lists(lists: impl IntoIterator<Item = Self>) -> Self {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would mention that behaviour

/// Returns a [`Type`] if the [`Term`] is a runtime type.
#[must_use]
pub fn as_runtime(&self) -> Option<TypeBase<NoRV>> {
pub(crate) fn least_upper_bound(&self) -> Option<TypeBound> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't drop the others even if we make this public, they are just helpers with clear names.
Maybe add a comment in their docs referencing least_upper_bound (if public).

/// Fallibly convert a [Term] to a [TypeRow].
///
/// This will fail if `arg` is not a [Term::List] or any of the elements are not [Type]s
impl TryFrom<Term> for TypeRow {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the first conversion is also a TryFrom then calling the chain may get too boilerplate-y (since it requires explicit types).

Comment thread hugr-core/src/types/type_row.rs Outdated
Comment thread hugr-core/src/types/type_row.rs Outdated
}
}

/*impl FromIterator<Type> for TypeRowRV {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should implement this instead of <T: IntoIterator<Item = Type>> From<T> for TypeRowRV ?

Having to collect_vec seems quite annoying.

@acl-cqc acl-cqc force-pushed the acl/type_wraps_term branch from 25c7fdd to 3a41dda Compare April 10, 2026 14:48
@acl-cqc
Copy link
Copy Markdown
Contributor Author

acl-cqc commented Apr 10, 2026

@aborgna-q some improvements on the conversions, thanks, including generalization of Term::new_list which was a big win, some questions remaining at #2895 (comment).

Also unsure about aliases. Searching for commented-out code mentioning alias might be useful when we revamp....is it any good to include Term::RuntimeAlias(Void, TypeBound) i.e. for uninstantiable Void? (Maybe not, I don't think we've even figured out the details of that typebound yet, and will we want alias terms or only types?)

@acl-cqc
Copy link
Copy Markdown
Contributor Author

acl-cqc commented Apr 20, 2026

  • Tried FromIterator for both TypeRow and TypeRowRV, see if you think it's worth it? 3a41dda
  • Replaced impl <T: IntoIterator<Item=Type>> From<T> for TypeRow(and ...RV) with from-Vec and from-Array, because this is consistent with Term and so on. Could go for the generic from-T for all three?
  • Moving bound cache into (opaque struct inside) SumType::General in perf!: remove bound-caching from Type, do in SumType::General instead #3022 - needs a bit more work but you can see the idea and can have this ready to merge immediately

I think that's all the serious concerns (in particular, all those marked ALAN), plenty of smaller ones which I can have a look over but ATM it looks like they haven't raised any strong opinions ;)

@acl-cqc acl-cqc requested a review from aborgna-q April 20, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants