Skip to content

Commit

Permalink
feat!: Allow "Row Variables" declared as List<Type> (#804)
Browse files Browse the repository at this point in the history
* Add a RowVariable variant of Type(Enum) that stands for potentially
multiple types, created via Type::new_row_var_use.
* This can appear in a TypeRow, i.e. the TypeRow is then of variable
length; and can be instantiated with a list of types (including row
vars) or a single row variable
* Validation enforces that RowVariables are not used directly as
wire/port types, but can appear *inside* other types
* OpDef's may be polymorphic over RowVariables (allowing "varargs"-like
operators equivalent to e.g. MakeTuple); these must be replaced by
non-rowvar types when instantiating the OpDef to an OpType
* FuncDefn's/FuncDecl's may also be polymorphic over RowVariables as
long as these are not directly argument/result types
* Also add TypeParam::new_sequence

closes #787

BREAKING CHANGE: Type::validate takes extra bool (allow_rowvars);
renamed {FunctionType, PolyFuncType}::(validate=>validate_var_len).

---------

Co-authored-by: doug-q <[email protected]>
  • Loading branch information
acl-cqc and doug-q authored May 22, 2024
1 parent dbaf601 commit 3ea4834
Show file tree
Hide file tree
Showing 19 changed files with 912 additions and 85 deletions.
19 changes: 18 additions & 1 deletion hugr-py/src/hugr/serialization/tys.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ class Variable(ConfiguredBaseModel):
b: "TypeBound"


class RowVar(ConfiguredBaseModel):
"""A variable standing for a row of some (unknown) number of types.
May occur only within a row; not a node input/output."""

t: Literal["R"] = "R"
i: int
b: "TypeBound"


class USize(ConfiguredBaseModel):
"""Unsigned integer size type."""

Expand Down Expand Up @@ -320,7 +329,15 @@ class Type(RootModel):
"""A HUGR type."""

root: Annotated[
Qubit | Variable | USize | FunctionType | Array | SumType | Opaque | Alias,
Qubit
| Variable
| RowVar
| USize
| FunctionType
| Array
| SumType
| Opaque
| Alias,
WrapValidator(_json_custom_error_validator),
Field(discriminator="t"),
]
Expand Down
11 changes: 10 additions & 1 deletion hugr/src/builder/build_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
types::EdgeKind,
};

use crate::extension::{ExtensionRegistry, ExtensionSet, PRELUDE_REGISTRY};
use crate::extension::{ExtensionRegistry, ExtensionSet, SignatureError, PRELUDE_REGISTRY};
use crate::types::{FunctionType, PolyFuncType, Type, TypeArg, TypeRow};

use itertools::Itertools;
Expand Down Expand Up @@ -645,6 +645,15 @@ fn add_node_with_wires<T: Dataflow + ?Sized>(
inputs: impl IntoIterator<Item = Wire>,
) -> Result<(Node, usize), BuildError> {
let nodetype: NodeType = nodetype.into();
// Check there are no row variables, as that would prevent us
// from indexing into the node's ports in order to wire up
nodetype
.op_signature()
.as_ref()
.and_then(FunctionType::find_rowvar)
.map_or(Ok(()), |(idx, _)| {
Err(SignatureError::RowVarWhereTypeExpected { idx })
})?;
let num_outputs = nodetype.op().value_output_count();
let op_node = data_builder.add_child_node(nodetype.clone());

Expand Down
38 changes: 35 additions & 3 deletions hugr/src/builder/dataflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,14 @@ pub(crate) mod test {

use crate::builder::build_traits::DataflowHugr;
use crate::builder::{BuilderWiringError, DataflowSubContainer, ModuleBuilder};
use crate::extension::prelude::BOOL_T;
use crate::extension::{ExtensionId, EMPTY_REG};
use crate::extension::prelude::{BOOL_T, USIZE_T};
use crate::extension::{ExtensionId, SignatureError, EMPTY_REG, PRELUDE_REGISTRY};
use crate::hugr::validate::InterGraphEdgeError;
use crate::ops::{handle::NodeHandle, Lift, Noop, OpTag};

use crate::std_extensions::logic::test::and_op;
use crate::types::Type;
use crate::types::type_param::TypeParam;
use crate::types::{Type, TypeBound};
use crate::utils::test_quantum_extension::h_gate;
use crate::{
builder::test::{n_identity, BIT, NAT, QB},
Expand Down Expand Up @@ -550,4 +551,35 @@ pub(crate) mod test {
);
Ok(())
}

#[test]
fn no_outer_row_variables() -> Result<(), BuildError> {
let e = crate::hugr::validate::test::extension_with_eval_parallel();
let tv = Type::new_row_var_use(0, TypeBound::Copyable);
let mut fb = FunctionBuilder::new(
"bad_eval",
PolyFuncType::new(
[TypeParam::new_list(TypeBound::Copyable)],
FunctionType::new(
Type::new_function(FunctionType::new(USIZE_T, tv.clone())),
vec![],
),
),
)?;

let [func_arg] = fb.input_wires_arr();
let i = fb.add_load_value(crate::extension::prelude::ConstUsize::new(5));
let ev = e.instantiate_extension_op(
"eval",
[vec![USIZE_T.into()].into(), vec![tv.into()].into()],
&PRELUDE_REGISTRY,
)?;
let r = fb.add_dataflow_op(ev, [func_arg, i]);
// This error would be caught in validation, but the builder detects it much earlier
assert_eq!(
r.unwrap_err(),
BuildError::SignatureError(SignatureError::RowVarWhereTypeExpected { idx: 0 })
);
Ok(())
}
}
3 changes: 3 additions & 0 deletions hugr/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ pub enum SignatureError {
/// A type variable that was used has not been declared
#[error("Type variable {idx} was not declared ({num_decls} in scope)")]
FreeTypeVar { idx: usize, num_decls: usize },
/// A row variable was found outside of a variable-length row
#[error("Expected a single type, but found row variable {idx}")]
RowVarWhereTypeExpected { idx: usize },
/// The result of the type application stored in a [Call]
/// is not what we get by applying the type-args to the polymorphic function
///
Expand Down
17 changes: 16 additions & 1 deletion hugr/src/extension/op_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,10 @@ impl OpDef {
// TODO https://github.com/CQCL/hugr/issues/624 validate declared TypeParams
// for both type scheme and custom binary
if let SignatureFunc::TypeScheme(ts) = &self.signature_func {
ts.poly_func.validate(exts)?;
// The type scheme may contain row variables so be of variable length;
// these will have to be substituted to fixed-length concrete types when
// the OpDef is instantiated into an actual OpType.
ts.poly_func.validate_var_len(exts)?;
}
Ok(())
}
Expand Down Expand Up @@ -482,6 +485,7 @@ mod test {
use crate::extension::{SignatureError, EMPTY_REG, PRELUDE_REGISTRY};
use crate::ops::{CustomOp, OpName};
use crate::std_extensions::collections::{EXTENSION, LIST_TYPENAME};
use crate::types::type_param::TypeArgError;
use crate::types::Type;
use crate::types::{type_param::TypeParam, FunctionType, PolyFuncType, TypeArg, TypeBound};
use crate::{const_extension_ids, Extension};
Expand Down Expand Up @@ -639,6 +643,17 @@ mod test {
def.compute_signature(&args, &EMPTY_REG),
Ok(FunctionType::new_endo(vec![tv]))
);
// But not with an external row variable
let arg: TypeArg = Type::new_row_var_use(0, TypeBound::Eq).into();
assert_eq!(
def.compute_signature(&[arg.clone()], &EMPTY_REG),
Err(SignatureError::TypeArgMismatch(
TypeArgError::TypeMismatch {
param: TypeBound::Any.into(),
arg
}
))
);
Ok(())
}

Expand Down
21 changes: 20 additions & 1 deletion hugr/src/hugr/serialize/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,32 @@ fn polyfunctype1() -> PolyFuncType {
PolyFuncType::new([TypeParam::max_nat(), TypeParam::Extensions], function_type)
}

fn polyfunctype2() -> PolyFuncType {
let tv0 = Type::new_row_var_use(0, TypeBound::Any);
let tv1 = Type::new_row_var_use(1, TypeBound::Eq);
let params = [TypeBound::Any, TypeBound::Eq].map(TypeParam::new_list);
let inputs = vec![
Type::new_function(FunctionType::new(tv0.clone(), tv1.clone())),
tv0,
];
let res = PolyFuncType::new(params, FunctionType::new(inputs, tv1));
// Just check we've got the arguments the right way round
// (not that it really matters for the serialization schema we have)
res.validate_var_len(&EMPTY_REG).unwrap();
res
}

#[rstest]
#[case(FunctionType::new_endo(type_row![]).into())]
#[case(polyfunctype1())]
#[case(PolyFuncType::new([TypeParam::Opaque { ty: int_custom_type(TypeArg::BoundedNat { n: 1 }) }], FunctionType::new_endo(type_row![Type::new_var_use(0, TypeBound::Copyable)])))]
#[case(PolyFuncType::new([TypeBound::Eq.into()], FunctionType::new_endo(type_row![Type::new_var_use(0, TypeBound::Eq)])))]
#[case(PolyFuncType::new([TypeParam::List { param: Box::new(TypeBound::Any.into()) }], FunctionType::new_endo(type_row![])))]
#[case(PolyFuncType::new([TypeParam::new_list(TypeBound::Any)], FunctionType::new_endo(type_row![])))]
#[case(PolyFuncType::new([TypeParam::Tuple { params: [TypeBound::Any.into(), TypeParam::bounded_nat(2.try_into().unwrap())].into() }], FunctionType::new_endo(type_row![])))]
#[case(PolyFuncType::new(
[TypeParam::new_list(TypeBound::Any)],
FunctionType::new_endo(Type::new_tuple(Type::new_row_var_use(0, TypeBound::Any)))))]
#[case(polyfunctype2())]
fn roundtrip_polyfunctype(#[case] poly_func_type: PolyFuncType) {
check_testing_roundtrip(poly_func_type)
}
Expand Down
30 changes: 23 additions & 7 deletions hugr/src/hugr/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::ops::custom::{resolve_opaque_op, CustomOp};
use crate::ops::validate::{ChildrenEdgeData, ChildrenValidationError, EdgeValidationError};
use crate::ops::{FuncDefn, OpTag, OpTrait, OpType, ValidateOp};
use crate::types::type_param::TypeParam;
use crate::types::EdgeKind;
use crate::types::{EdgeKind, FunctionType};
use crate::{Direction, Hugr, Node, Port};

use super::views::{HierarchyView, HugrView, SiblingGraph};
Expand Down Expand Up @@ -211,7 +211,20 @@ impl<'a, 'b> ValidationContext<'a, 'b> {
}
}

// Secondly that the node has correct children
// Secondly, check that the node signature does not contain any row variables.
// (We do this here so it's before we try indexing into the ports of any nodes).
op_type
.dataflow_signature()
.as_ref()
.and_then(FunctionType::find_rowvar)
.map_or(Ok(()), |(idx, _)| {
Err(ValidationError::SignatureError {
node,
cause: SignatureError::RowVarWhereTypeExpected { idx },
})
})?;

// Thirdly that the node has correct children
self.validate_children(node, node_type)?;

Ok(())
Expand Down Expand Up @@ -301,11 +314,14 @@ impl<'a, 'b> ValidationContext<'a, 'b> {
var_decls: &[TypeParam],
) -> Result<(), SignatureError> {
match &port_kind {
EdgeKind::Value(ty) => ty.validate(self.extension_registry, var_decls),
EdgeKind::Value(ty) => ty.validate(false, self.extension_registry, var_decls),
// Static edges must *not* refer to type variables declared by enclosing FuncDefns
// as these are only types at runtime.
EdgeKind::Const(ty) => ty.validate(self.extension_registry, &[]),
EdgeKind::Function(pf) => pf.validate(self.extension_registry),
// as these are only types at runtime. (Note the choice of `allow_row_vars` as `false` is arbitrary here.)
EdgeKind::Const(ty) => ty.validate(false, self.extension_registry, &[]),
// Allow function "value" to have unknown arity. A Call node will have to provide
// TypeArgs that produce a known arity, but a LoadFunction might pass the function
// value ("function pointer") around without knowing how to call it.
EdgeKind::Function(pf) => pf.validate_var_len(self.extension_registry),
_ => Ok(()),
}
}
Expand Down Expand Up @@ -794,4 +810,4 @@ pub enum InterGraphEdgeError {
}

#[cfg(test)]
mod test;
pub(crate) mod test;
Loading

0 comments on commit 3ea4834

Please sign in to comment.