Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add some validation for const nodes #1222

Merged
merged 7 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hugr-core/resources
50 changes: 50 additions & 0 deletions hugr-core/src/hugr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ pub enum HugrError {

#[cfg(test)]
mod test {
use std::{fs::File, io::BufReader};

use crate::{extension::PRELUDE_REGISTRY, test_file};

use super::{Hugr, HugrView};

#[test]
Expand All @@ -240,4 +244,50 @@ mod test {
let hugr = simple_dfg_hugr();
assert_matches!(hugr.get_io(hugr.root()), Some(_));
}

#[test]
fn hugr_validation_0() {
// https://github.com/CQCL/hugr/issues/1091 bad case
let mut hugr: Hugr = serde_json::from_reader(BufReader::new(
File::open(test_file!("hugr-0.json")).unwrap(),
))
.unwrap();
assert!(
hugr.update_validate(&PRELUDE_REGISTRY).is_err(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Needs to include float extensions here and below I think?. Maybe it's fine because CustomConst don't work through extensions.

"HUGR should not validate."
);
}

#[test]
fn hugr_validation_1() {
// https://github.com/CQCL/hugr/issues/1091 good case
let mut hugr: Hugr = serde_json::from_reader(BufReader::new(
File::open(test_file!("hugr-1.json")).unwrap(),
))
.unwrap();
assert!(hugr.update_validate(&PRELUDE_REGISTRY).is_ok());
}

#[test]
fn hugr_validation_2() {
// https://github.com/CQCL/hugr/issues/1185 bad case
let mut hugr: Hugr = serde_json::from_reader(BufReader::new(
File::open(test_file!("hugr-2.json")).unwrap(),
))
.unwrap();
assert!(
hugr.update_validate(&PRELUDE_REGISTRY).is_err(),
"HUGR should not validate."
);
}

#[test]
fn hugr_validation_3() {
// https://github.com/CQCL/hugr/issues/1185 good case
let mut hugr: Hugr = serde_json::from_reader(BufReader::new(
File::open(test_file!("hugr-3.json")).unwrap(),
))
.unwrap();
assert!(hugr.update_validate(&PRELUDE_REGISTRY).is_ok());
}
}
41 changes: 39 additions & 2 deletions hugr-core/src/hugr/validate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! HUGR invariant checks.

use std::any::TypeId;
use std::collections::HashMap;
use std::iter;

Expand All @@ -13,9 +14,10 @@ use crate::extension::{ExtensionRegistry, ExtensionSet, SignatureError};

use crate::ops::custom::{resolve_opaque_op, CustomOp, CustomOpError};
use crate::ops::validate::{ChildrenEdgeData, ChildrenValidationError, EdgeValidationError};
use crate::ops::{FuncDefn, OpParent, OpTag, OpTrait, OpType, ValidateOp};
use crate::ops::{FuncDefn, OpParent, OpTag, OpTrait, OpType, ValidateOp, Value};
use crate::std_extensions::arithmetic::float_types::ConstF64;
use crate::types::type_param::TypeParam;
use crate::types::{EdgeKind, FunctionType};
use crate::types::{EdgeKind, FunctionType, SumType, TypeEnum};
use crate::{Direction, Hugr, Node, Port};

use super::views::{HierarchyView, HugrView, SiblingGraph};
Expand Down Expand Up @@ -210,6 +212,35 @@ impl<'a, 'b> ValidationContext<'a, 'b> {
// Thirdly that the node has correct children
self.validate_children(node, op_type)?;

// OpType-specific checks.
// TODO Maybe we should delegate these checks to the OpTypes themselves.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe, but I can see why you don't. ValidateOp is not set up for op-specific logic. I think it's fine here for now, but I do think you should extract it into a function. i.e.

if let OpType::Const(c) = op_type {
            match c.validate()
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Perhaps this TODO can be removed now?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it should remain, it does feel like this should call into a trait.

if let OpType::Const(c) = op_type {
match c.get_type().as_type_enum() {
TypeEnum::Sum(SumType::Unit { size: _ }) => {
if let Value::Sum {
tag: _,
values,
sum_type: _,
} = c.value()
{
if !values.is_empty() {
return Err(ValidationError::UnitSumWithValues {});
}
}
}
TypeEnum::Extension(custom_type) => {
if custom_type.name() == "float64" {
if let Value::Extension { e } = c.value() {
if e.value().type_id() != TypeId::of::<ConstF64>() {
return Err(ValidationError::WrongExtensionValueType {});
}
}
}
}
_ => {}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is too specific to the bugs. We need to check all sum values have the right type, and we need logic that will work for all CustomConsts.

A start is: (the body of Value::validate):

Suggested change
match c.get_type().as_type_enum() {
TypeEnum::Sum(SumType::Unit { size: _ }) => {
if let Value::Sum {
tag: _,
values,
sum_type: _,
} = c.value()
{
if !values.is_empty() {
return Err(ValidationError::UnitSumWithValues {});
}
}
}
TypeEnum::Extension(custom_type) => {
if custom_type.name() == "float64" {
if let Value::Extension { e } = c.value() {
if e.value().type_id() != TypeId::of::<ConstF64>() {
return Err(ValidationError::WrongExtensionValueType {});
}
}
}
}
_ => {}
}
match self {
Self::Extension { e } => e.value().validate(),
Self::Sum { tag, typ, values } => typ.check_type(values),
Self::Function { .. } => Ok(()) // we don't check inside Function Vlaues
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! So, just to clarify, is your suggestion to:

  • create a new validate() method for Values, and call that (when appropriate) from validate_node();
  • in Value::validate(), for the case of Extension values, delegate to CustomConst::validate();
  • add overloads for CustomConst::validate() for specific types such as ConstF64?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes. ConstF64 does need a validate, which it doesn't have, but that is a different issue: It can't check serialisation, because ConstF64 only holds a f64, but it should check that it's not NAN or infinite.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes indeed, and adding a validate() to ConstF64 wouldn't catch the issue here, which is that we haven't got a ConstF64 at all...

}

Ok(())
}

Expand Down Expand Up @@ -752,6 +783,12 @@ pub enum ValidationError {
/// [Opaque]: crate::ops::CustomOp::Opaque
#[error(transparent)]
CustomOpError(#[from] CustomOpError),
/// Error in a [SumType::Unit]
#[error("A unit sum contained a non-empty list of values")]
UnitSumWithValues {},
/// Wrong type for an extension value
#[error("An extension value was of the wrong type")]
WrongExtensionValueType {},
}

/// Errors related to the inter-graph edge validations.
Expand Down
10 changes: 10 additions & 0 deletions hugr-core/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,13 @@ macro_rules! const_extension_ids {
}

pub use const_extension_ids;

#[cfg(test)]
/// Get the full path to a test file given its path relative to the
/// `resources/test` directory in this crate.
#[macro_export]
macro_rules! test_file {
($fname:expr) => {
concat!(env!("CARGO_MANIFEST_DIR"), "/resources/test/", $fname)
};
}
36 changes: 36 additions & 0 deletions resources/test/hugr-0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"version": "v1",
"nodes":
[
{
"parent": 0,
"input_extensions": null,
"op": "Const",
"v":
{
"v": "Extension",
"extensions":
[
"arithmetic.float.types"
],
"typ":
{
"t": "Opaque",
"extension": "arithmetic.float.types",
"id": "float64",
"args":
[],
"bound": "C"
},
"value":
{
"c": "ConstF64",
"v": 42
}
}
}
],
"edges":
[],
"encoder": null
}
39 changes: 39 additions & 0 deletions resources/test/hugr-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"version": "v1",
"nodes":
[
{
"parent": 0,
"input_extensions": null,
"op": "Const",
"v":
{
"v": "Extension",
"extensions":
[
"arithmetic.float.types"
],
"typ":
{
"t": "Opaque",
"extension": "arithmetic.float.types",
"id": "float64",
"args":
[],
"bound": "C"
},
"value":
{
"c": "ConstF64",
"v":
{
"value": 42.0
}
}
}
}
],
"edges":
[],
"encoder": null
}
33 changes: 33 additions & 0 deletions resources/test/hugr-2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"version": "v1",
"nodes":
[
{
"parent": 0,
"input_extensions": null,
"op": "Const",
"v":
{
"v": "Sum",
"tag": 1,
"typ":
{
"t": "Sum",
"s": "Unit",
"size": 2
},
"vs":
[
{
"v": "Tuple",
"vs":
[]
}
]
}
}
],
"edges":
[],
"encoder": null
}
27 changes: 27 additions & 0 deletions resources/test/hugr-3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"version": "v1",
"nodes":
[
{
"parent": 0,
"input_extensions": null,
"op": "Const",
"v":
{
"v": "Sum",
"tag": 1,
"typ":
{
"t": "Sum",
"s": "Unit",
"size": 2
},
"vs":
[]
}
}
],
"edges":
[],
"encoder": null
}
Loading