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: FuncDefns don't require that their extensions match their children #688

Merged
merged 12 commits into from
Nov 16, 2023
8 changes: 7 additions & 1 deletion src/extension/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,13 @@ impl UnificationContext {
let m_input_node = self.make_or_get_meta(input, dir);
self.add_constraint(m_input_node, Constraint::Equal(m_input));
let m_output_node = self.make_or_get_meta(output, dir);
self.add_constraint(m_output_node, Constraint::Equal(m_output));
// If the parent node is a FuncDefn, it's input_resources
croyzor marked this conversation as resolved.
Show resolved Hide resolved
// should always be empty, meaning if the function adds
Copy link
Contributor

Choose a reason for hiding this comment

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

if "should_always be empty" is true, then you could add a constraint::equals to something with an empty solution (we might want one such variable per hugr). But I don't think we are insisting on "should always be empty" right now(?)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's the default, but we don't insist. OTOH I think it might be sane if we did insist, and I'd be happy to make that change in this PR

Copy link
Contributor

Choose a reason for hiding this comment

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

We might have to change EdgeKind::Static not to require equality of extensions at both ends. But we should probably do that anyway - ATM all calls will have to have the same input-extensions, as you can't lift the static input

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, if #693 doesn't fly, we need an alternative approach for dealing with functions. Maybe splitting EdgeKind::Static into EdgeKind::Function and EdgeKind::Const (as suggested here) would be a good way to go anyway....

What about resurrecting PR #693, and giving the LoadConstant node a nonzero delta built from the constant being loaded? No, that would leave the input-extensions for the LoadConstant undefined :-(.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe raise an issue "All calls to a function must have the same input_extensions" - I think that's a fair description of the problem (and it sounds like a problem to me)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Raised #695

// resources, they shouldn't be equal to the parent's output
// (which will also be empty.)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is that true? parent's output is equal to (its input, probably empty, maybe not necessarily) plus delta

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, yes, well, the "its input_extensions should always be empty" bit is as above, but the point to make is that the parent's output is equal to its input, because these do not include the delta of the function itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the stuff about empty input_extensions, since it isn't true

if node_type.tag() != OpTag::FuncDefn {
Copy link
Contributor

Choose a reason for hiding this comment

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

ok, that is neater than I realized, nice

Copy link
Contributor

Choose a reason for hiding this comment

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

Hang on. (Hang on!) Don't we need an additional constraint, then, that the (input-extensions to the) Output node are Plus of (the delta of the FuncDefn's declared FunctionType and the Input node)?

Could add a test of a FuncDefn that declares delta is {A} and then has a body that Lift's only by {B}, say - that should be rejected, but is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a test for this "funcdefn_signature_mismatch"

self.add_constraint(m_output_node, Constraint::Equal(m_output));
}
}
}

Expand Down
79 changes: 77 additions & 2 deletions src/extension/infer/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ use std::error::Error;

use super::*;
use crate::builder::test::closed_dfg_root_hugr;
use crate::builder::{DFGBuilder, Dataflow, DataflowHugr};
use crate::builder::{
Container, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder, ModuleBuilder,
};
use crate::extension::prelude::QB_T;
use crate::extension::ExtensionId;
use crate::extension::{prelude::PRELUDE_REGISTRY, ExtensionSet};
use crate::extension::{
prelude::{ConstUsize, PRELUDE_REGISTRY, USIZE_T},
ExtensionSet,
};
use crate::hugr::{validate::ValidationError, Hugr, HugrMut, HugrView, NodeType};
use crate::macros::const_extension_ids;
use crate::ops::custom::{ExternalOp, OpaqueOp};
Expand Down Expand Up @@ -962,3 +967,73 @@ fn sccs() {
Some(&ExtensionSet::from_iter([A, B, C, UNKNOWN_EXTENSION]))
);
}

#[test]
fn simple_funcdefn() -> Result<(), Box<dyn Error>> {
let mut builder = ModuleBuilder::new();
let mut func_builder = builder.define_function(
"F",
FunctionType::new(vec![NAT], vec![NAT])
.with_extension_delta(&ExtensionSet::singleton(&A))
.pure(),
)?;

let [w] = func_builder.input_wires_arr();
let lift = func_builder.add_dataflow_op(
ops::LeafOp::Lift {
type_row: type_row![NAT],
new_extension: A,
},
[w],
)?;
let [w] = lift.outputs_arr();
func_builder.finish_with_outputs([w])?;
builder.finish_hugr(&PRELUDE_REGISTRY)?;
Ok(())
}

#[test]
// Define a function "g" inside "f", which defines a function which adds to
croyzor marked this conversation as resolved.
Show resolved Hide resolved
// the graph. Tests that the resources of the graph of "g" aren't being
// constrained to match those of the FuncDefn node.
Copy link
Contributor

Choose a reason for hiding this comment

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

struggling to see how the test does what you say here - sorry! - can you put a comment or two inside the method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed it but it doesn't pass! It's really requires us to be ignoring "Static" edges for functions, so I'll re-add it when I open the PR fixing #695

fn funcdefn() -> Result<(), Box<dyn Error>> {
croyzor marked this conversation as resolved.
Show resolved Hide resolved
use crate::builder::{Container, Dataflow};
use crate::values::Value;

let mut builder = ModuleBuilder::new();
let mut func_builder = builder.define_function(
"F",
FunctionType::new(vec![NAT], vec![NAT])
.with_extension_delta(&ExtensionSet::singleton(&A))
.pure(),
)?;

let mut nested_func_builder = func_builder.define_function(
"G",
FunctionType::new(vec![NAT], vec![NAT])
.with_extension_delta(&ExtensionSet::singleton(&A))
croyzor marked this conversation as resolved.
Show resolved Hide resolved
.pure(),
)?;

let [w] = nested_func_builder.input_wires_arr();
let lift = nested_func_builder.add_dataflow_op(
ops::LeafOp::Lift {
type_row: type_row![NAT],
new_extension: A,
},
[w],
)?;
let [w] = lift.outputs_arr();
let g_id = nested_func_builder.finish_with_outputs([w])?;

let int_value: Value = ConstUsize::new(42).into();
let k_node = func_builder.add_constant(ops::Const::new(int_value, USIZE_T).unwrap(), None)?;
let k = func_builder.load_const(&k_node)?;
let call = func_builder.call(g_id.handle(), [k])?;
let [w] = call.outputs_arr();
func_builder.finish_with_outputs([w])?;

let hugr = builder.finish_hugr(&PRELUDE_REGISTRY)?;
croyzor marked this conversation as resolved.
Show resolved Hide resolved
infer_extensions(&hugr)?;
Ok(())
}
14 changes: 9 additions & 5 deletions src/hugr/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,15 @@ impl<'a, 'b> ValidationContext<'a, 'b> {
// Secondly that the node has correct children
self.validate_children(node, node_type)?;

// If this is a container with I/O nodes, check that the extension they
// define match the extensions of the container.
if let Some([input, output]) = self.hugr.get_io(node) {
self.extension_validator
.validate_io_extensions(node, input, output)?;
// FuncDefns have no resources since they're static nodes, but the
// functions they define can have any extension delta.
if node_type.tag() != OpTag::FuncDefn {
croyzor marked this conversation as resolved.
Show resolved Hide resolved
// If this is a container with I/O nodes, check that the extension they
// define match the extensions of the container.
if let Some([input, output]) = self.hugr.get_io(node) {
self.extension_validator
.validate_io_extensions(node, input, output)?;
}
}

Ok(())
Expand Down
50 changes: 49 additions & 1 deletion src/hugr/validate/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ fn extensions_mismatch() -> Result<(), BuildError> {
assert_matches!(
handle,
Err(ValidationError::ExtensionError(
ExtensionError::ParentIOExtensionMismatch { .. }
ExtensionError::TgtExceedsSrcExtensionsAtPort { .. }
))
);
Ok(())
Expand Down Expand Up @@ -750,3 +750,51 @@ fn invalid_types() {
SignatureError::TypeArgMismatch(TypeArgError::WrongNumberArgs(2, 1))
);
}

#[test]
fn parent_io_mismatch() {
let mut hugr = Hugr::new(NodeType::new_pure(ops::DFG {
signature: FunctionType::new(type_row![USIZE_T], type_row![USIZE_T]),
Copy link
Contributor

Choose a reason for hiding this comment

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

The problem here is that the FunctionType has zero delta? Whereas if you did .with_extension_delta(...XB_T...) it'd pass? (Might be worth doing both ways via a loop / helper fn just to show the difference between working and not working)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a comment explaining this

}));

let input = hugr
.add_node_with_parent(
hugr.root(),
NodeType::new_pure(ops::Input {
types: type_row![USIZE_T],
}),
)
.unwrap();
let output = hugr
.add_node_with_parent(
hugr.root(),
NodeType::new(
ops::Output {
types: type_row![USIZE_T],
},
ExtensionSet::singleton(&XB),
),
)
.unwrap();

let lift = hugr
.add_node_with_parent(
hugr.root(),
NodeType::new_pure(ops::LeafOp::Lift {
type_row: type_row![USIZE_T],
new_extension: XB,
}),
)
.unwrap();

hugr.connect(input, 0, lift, 0).unwrap();
hugr.connect(lift, 0, output, 0).unwrap();

let result = hugr.validate(&PRELUDE_REGISTRY);
assert_matches!(
result,
Err(ValidationError::ExtensionError(
ExtensionError::ParentIOExtensionMismatch { .. }
))
);
}