From ef5485af3fc4c92303e94c696e8d171f8bb01c5d Mon Sep 17 00:00:00 2001 From: doug-q <141026920+doug-q@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:50:48 +0100 Subject: [PATCH 1/2] test: ignore serialisation tests when using miri (#975) Fixes #969. This solution is an over approximation because many serialisation tests(in which I include comprehensive coverage which does not yet exist) would not trigger the problem. This would be easy to remove if miri grows support. The most recent activity on the ticket is positive. --- hugr/src/hugr/serialize.rs | 6 +++--- hugr/src/hugr/validate.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hugr/src/hugr/serialize.rs b/hugr/src/hugr/serialize.rs index 8634f3d479..ccfa5b6f3c 100644 --- a/hugr/src/hugr/serialize.rs +++ b/hugr/src/hugr/serialize.rs @@ -261,6 +261,9 @@ impl TryFrom for Hugr { } #[cfg(test)] +#[cfg_attr(miri, ignore = "miri does not support 'life before main'")] +// Miri doesn't run the extension registration required by `typetag` for +// registering `CustomConst`s. https://github.com/rust-lang/miri/issues/450 pub mod test { use super::*; @@ -547,9 +550,6 @@ pub mod test { } #[test] - #[cfg_attr(miri, ignore = "Extension ops cannot be used with miri.")] - // Miri doesn't run the extension registration required by `typetag` for registering `CustomConst`s. - // https://github.com/rust-lang/miri/issues/450 fn constants_roundtrip() -> Result<(), Box> { let mut builder = DFGBuilder::new(FunctionType::new(vec![], vec![FLOAT64_TYPE])).unwrap(); let w = builder.add_load_value(ConstF64::new(0.5)); diff --git a/hugr/src/hugr/validate.rs b/hugr/src/hugr/validate.rs index 4343d2765f..2ee170596d 100644 --- a/hugr/src/hugr/validate.rs +++ b/hugr/src/hugr/validate.rs @@ -105,6 +105,7 @@ impl<'a, 'b> ValidationContext<'a, 'b> { // in-tree schema. For now, our serialized hugr does not match the // schema. When this is fixed we should pass true below. #[cfg(test)] + #[cfg_attr(miri, ignore = "miri does not support 'life before main'")] crate::hugr::serialize::test::check_hugr_roundtrip(self.hugr, false); Ok(()) From d24166d736cfb17d16f74434dd7376730fa7f824 Mon Sep 17 00:00:00 2001 From: Douglas Wilson Date: Fri, 26 Apr 2024 09:25:30 +0100 Subject: [PATCH 2/2] refactor: outline hugr::serialize::test --- hugr/src/hugr/serialize.rs | 300 +------------------------------- hugr/src/hugr/serialize/test.rs | 296 +++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+), 299 deletions(-) create mode 100644 hugr/src/hugr/serialize/test.rs diff --git a/hugr/src/hugr/serialize.rs b/hugr/src/hugr/serialize.rs index ccfa5b6f3c..343914fdfb 100644 --- a/hugr/src/hugr/serialize.rs +++ b/hugr/src/hugr/serialize.rs @@ -264,302 +264,4 @@ impl TryFrom for Hugr { #[cfg_attr(miri, ignore = "miri does not support 'life before main'")] // Miri doesn't run the extension registration required by `typetag` for // registering `CustomConst`s. https://github.com/rust-lang/miri/issues/450 -pub mod test { - - use super::*; - use crate::builder::{ - test::closed_dfg_root_hugr, Container, DFGBuilder, Dataflow, DataflowHugr, - DataflowSubContainer, HugrBuilder, ModuleBuilder, - }; - use crate::extension::prelude::BOOL_T; - use crate::extension::simple_op::MakeRegisteredOp; - use crate::extension::{EMPTY_REG, PRELUDE_REGISTRY}; - use crate::hugr::hugrmut::sealed::HugrMutInternals; - use crate::hugr::NodeType; - use crate::ops::custom::{ExtensionOp, OpaqueOp}; - use crate::ops::{dataflow::IOTrait, Input, Module, Noop, Output, DFG}; - use crate::std_extensions::arithmetic::float_ops::FLOAT_OPS_REGISTRY; - use crate::std_extensions::arithmetic::float_types::{ConstF64, FLOAT64_TYPE}; - use crate::std_extensions::logic::NotOp; - use crate::types::{FunctionType, Type}; - use crate::{type_row, OutgoingPort}; - use itertools::Itertools; - use jsonschema::{Draft, JSONSchema}; - use lazy_static::lazy_static; - use portgraph::LinkView; - use portgraph::{ - multiportgraph::MultiPortGraph, Hierarchy, LinkMut, PortMut, PortView, UnmanagedDenseMap, - }; - - const NAT: Type = crate::extension::prelude::USIZE_T; - const QB: Type = crate::extension::prelude::QB_T; - - lazy_static! { - static ref SCHEMA: JSONSchema = { - let schema_val: serde_json::Value = serde_json::from_str(include_str!( - "../../../specification/schema/hugr_schema_v1.json" - )) - .unwrap(); - JSONSchema::options() - .with_draft(Draft::Draft7) - .compile(&schema_val) - .expect("Schema is invalid.") - }; - } - - #[test] - fn empty_hugr_serialize() { - let hg = Hugr::default(); - assert_eq!(ser_roundtrip(&hg), hg); - } - - /// Serialize and deserialize a value. - pub fn ser_roundtrip(g: &T) -> T { - ser_roundtrip_validate(g, None) - } - - /// Serialize and deserialize a value, optionally validating against a schema. - pub fn ser_roundtrip_validate( - g: &T, - schema: Option<&JSONSchema>, - ) -> T { - let s = serde_json::to_string(g).unwrap(); - let val: serde_json::Value = serde_json::from_str(&s).unwrap(); - - if let Some(schema) = schema { - let validate = schema.validate(&val); - - if let Err(errors) = validate { - // errors don't necessarily implement Debug - for error in errors { - println!("Validation error: {}", error); - println!("Instance path: {}", error.instance_path); - } - panic!("Serialization test failed."); - } - } - serde_json::from_str(&s).unwrap() - } - - /// Serialize and deserialize a HUGR, and check that the result is the same as the original. - /// Checks the serialized json against the in-tree schema. - /// - /// Returns the deserialized HUGR. - pub fn check_hugr_schema_roundtrip(hugr: &Hugr) -> Hugr { - check_hugr_roundtrip(hugr, true) - } - - /// Serialize and deserialize a HUGR, and check that the result is the same as the original. - /// - /// If `check_schema` is true, checks the serialized json against the in-tree schema. - /// - /// Returns the deserialized HUGR. - pub fn check_hugr_roundtrip(hugr: &Hugr, check_schema: bool) -> Hugr { - let new_hugr: Hugr = ser_roundtrip_validate(hugr, check_schema.then_some(&SCHEMA)); - - // Original HUGR, with canonicalized node indices - // - // The internal port indices may still be different. - let mut h_canon = hugr.clone(); - h_canon.canonicalize_nodes(|_, _| {}); - - assert_eq!(new_hugr.root, h_canon.root); - assert_eq!(new_hugr.hierarchy, h_canon.hierarchy); - assert_eq!(new_hugr.metadata, h_canon.metadata); - - // Extension operations may have been downgraded to opaque operations. - for node in new_hugr.nodes() { - let new_op = new_hugr.get_optype(node); - let old_op = h_canon.get_optype(node); - assert_eq!(new_op, old_op); - } - - // Check that the graphs are equivalent up to port renumbering. - let new_graph = &new_hugr.graph; - let old_graph = &h_canon.graph; - assert_eq!(new_graph.node_count(), old_graph.node_count()); - assert_eq!(new_graph.port_count(), old_graph.port_count()); - assert_eq!(new_graph.link_count(), old_graph.link_count()); - for n in old_graph.nodes_iter() { - assert_eq!(new_graph.num_inputs(n), old_graph.num_inputs(n)); - assert_eq!(new_graph.num_outputs(n), old_graph.num_outputs(n)); - assert_eq!( - new_graph.output_neighbours(n).collect_vec(), - old_graph.output_neighbours(n).collect_vec() - ); - } - - new_hugr - } - - /// Generate an optype for a node with a matching amount of inputs and outputs. - fn gen_optype(g: &MultiPortGraph, node: portgraph::NodeIndex) -> OpType { - let inputs = g.num_inputs(node); - let outputs = g.num_outputs(node); - match (inputs == 0, outputs == 0) { - (false, false) => DFG { - signature: FunctionType::new(vec![NAT; inputs - 1], vec![NAT; outputs - 1]), - } - .into(), - (true, false) => Input::new(vec![NAT; outputs - 1]).into(), - (false, true) => Output::new(vec![NAT; inputs - 1]).into(), - (true, true) => Module.into(), - } - } - - #[test] - fn simpleser() { - let mut g = MultiPortGraph::new(); - - let root = g.add_node(0, 0); - let a = g.add_node(1, 1); - let b = g.add_node(3, 2); - let c = g.add_node(1, 1); - - g.link_nodes(a, 0, b, 0).unwrap(); - g.link_nodes(a, 0, b, 0).unwrap(); - g.link_nodes(b, 0, b, 1).unwrap(); - g.link_nodes(b, 1, c, 0).unwrap(); - g.link_nodes(b, 1, a, 0).unwrap(); - g.link_nodes(c, 0, a, 0).unwrap(); - - let mut h = Hierarchy::new(); - let mut op_types = UnmanagedDenseMap::new(); - - op_types[root] = NodeType::new_open(gen_optype(&g, root)); - - for n in [a, b, c] { - h.push_child(n, root).unwrap(); - op_types[n] = NodeType::new_pure(gen_optype(&g, n)); - } - - let hugr = Hugr { - graph: g, - hierarchy: h, - root, - op_types, - metadata: Default::default(), - }; - - check_hugr_schema_roundtrip(&hugr); - } - - #[test] - fn weighted_hugr_ser() { - let hugr = { - let mut module_builder = ModuleBuilder::new(); - module_builder.set_metadata("name", "test"); - - let t_row = vec![Type::new_sum([type_row![NAT], type_row![QB]])]; - let mut f_build = module_builder - .define_function("main", FunctionType::new(t_row.clone(), t_row).into()) - .unwrap(); - - let outputs = f_build - .input_wires() - .map(|in_wire| { - f_build - .add_dataflow_op( - Noop { - ty: f_build.get_wire_type(in_wire).unwrap(), - }, - [in_wire], - ) - .unwrap() - .out_wire(0) - }) - .collect_vec(); - f_build.set_metadata("val", 42); - f_build.finish_with_outputs(outputs).unwrap(); - - module_builder.finish_prelude_hugr().unwrap() - }; - - check_hugr_schema_roundtrip(&hugr); - } - - #[test] - fn dfg_roundtrip() -> Result<(), Box> { - let tp: Vec = vec![BOOL_T; 2]; - let mut dfg = DFGBuilder::new(FunctionType::new(tp.clone(), tp))?; - let mut params: [_; 2] = dfg.input_wires_arr(); - for p in params.iter_mut() { - *p = dfg - .add_dataflow_op(Noop { ty: BOOL_T }, [*p]) - .unwrap() - .out_wire(0); - } - let hugr = dfg.finish_hugr_with_outputs(params, &EMPTY_REG)?; - - check_hugr_schema_roundtrip(&hugr); - Ok(()) - } - - #[test] - fn opaque_ops() -> Result<(), Box> { - let tp: Vec = vec![BOOL_T; 1]; - let mut dfg = DFGBuilder::new(FunctionType::new_endo(tp))?; - let [wire] = dfg.input_wires_arr(); - - // Add an extension operation - let extension_op: ExtensionOp = NotOp.to_extension_op().unwrap(); - let wire = dfg - .add_dataflow_op(extension_op.clone(), [wire]) - .unwrap() - .out_wire(0); - - // Add an unresolved opaque operation - let opaque_op: OpaqueOp = extension_op.into(); - let wire = dfg.add_dataflow_op(opaque_op, [wire]).unwrap().out_wire(0); - - let hugr = dfg.finish_hugr_with_outputs([wire], &PRELUDE_REGISTRY)?; - - check_hugr_schema_roundtrip(&hugr); - Ok(()) - } - - #[test] - fn function_type() -> Result<(), Box> { - let fn_ty = Type::new_function(FunctionType::new_endo(type_row![BOOL_T])); - let mut bldr = DFGBuilder::new(FunctionType::new_endo(vec![fn_ty.clone()]))?; - let op = bldr.add_dataflow_op(Noop { ty: fn_ty }, bldr.input_wires())?; - let h = bldr.finish_prelude_hugr_with_outputs(op.outputs())?; - - check_hugr_schema_roundtrip(&h); - Ok(()) - } - - #[test] - fn hierarchy_order() -> Result<(), Box> { - let mut hugr = closed_dfg_root_hugr(FunctionType::new(vec![QB], vec![QB])); - let [old_in, out] = hugr.get_io(hugr.root()).unwrap(); - hugr.connect(old_in, 0, out, 0); - - // Now add a new input - let new_in = hugr.add_node(Input::new([QB].to_vec()).into()); - hugr.disconnect(old_in, OutgoingPort::from(0)); - hugr.connect(new_in, 0, out, 0); - hugr.move_before_sibling(new_in, old_in); - hugr.remove_node(old_in); - hugr.update_validate(&PRELUDE_REGISTRY)?; - - let new_hugr: Hugr = check_hugr_schema_roundtrip(&hugr); - new_hugr.validate(&EMPTY_REG).unwrap_err(); - new_hugr.validate(&PRELUDE_REGISTRY)?; - Ok(()) - } - - #[test] - fn constants_roundtrip() -> Result<(), Box> { - let mut builder = DFGBuilder::new(FunctionType::new(vec![], vec![FLOAT64_TYPE])).unwrap(); - let w = builder.add_load_value(ConstF64::new(0.5)); - let hugr = builder.finish_hugr_with_outputs([w], &FLOAT_OPS_REGISTRY)?; - - let ser = serde_json::to_string(&hugr)?; - let deser = serde_json::from_str(&ser)?; - - assert_eq!(hugr, deser); - - Ok(()) - } -} +pub mod test; diff --git a/hugr/src/hugr/serialize/test.rs b/hugr/src/hugr/serialize/test.rs new file mode 100644 index 0000000000..c3c9c59079 --- /dev/null +++ b/hugr/src/hugr/serialize/test.rs @@ -0,0 +1,296 @@ +use super::*; +use crate::builder::{ + test::closed_dfg_root_hugr, Container, DFGBuilder, Dataflow, DataflowHugr, + DataflowSubContainer, HugrBuilder, ModuleBuilder, +}; +use crate::extension::prelude::BOOL_T; +use crate::extension::simple_op::MakeRegisteredOp; +use crate::extension::{EMPTY_REG, PRELUDE_REGISTRY}; +use crate::hugr::hugrmut::sealed::HugrMutInternals; +use crate::hugr::NodeType; +use crate::ops::custom::{ExtensionOp, OpaqueOp}; +use crate::ops::{dataflow::IOTrait, Input, Module, Noop, Output, DFG}; +use crate::std_extensions::arithmetic::float_ops::FLOAT_OPS_REGISTRY; +use crate::std_extensions::arithmetic::float_types::{ConstF64, FLOAT64_TYPE}; +use crate::std_extensions::logic::NotOp; +use crate::types::{FunctionType, Type}; +use crate::{type_row, OutgoingPort}; +use itertools::Itertools; +use jsonschema::{Draft, JSONSchema}; +use lazy_static::lazy_static; +use portgraph::LinkView; +use portgraph::{ + multiportgraph::MultiPortGraph, Hierarchy, LinkMut, PortMut, PortView, UnmanagedDenseMap, +}; + +const NAT: Type = crate::extension::prelude::USIZE_T; +const QB: Type = crate::extension::prelude::QB_T; + +lazy_static! { + static ref SCHEMA: JSONSchema = { + let schema_val: serde_json::Value = serde_json::from_str(include_str!( + "../../../../specification/schema/hugr_schema_v1.json" + )) + .unwrap(); + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&schema_val) + .expect("Schema is invalid.") + }; +} + +#[test] +fn empty_hugr_serialize() { + let hg = Hugr::default(); + assert_eq!(ser_roundtrip(&hg), hg); +} + +/// Serialize and deserialize a value. +pub fn ser_roundtrip(g: &T) -> T { + ser_roundtrip_validate(g, None) +} + +/// Serialize and deserialize a value, optionally validating against a schema. +pub fn ser_roundtrip_validate( + g: &T, + schema: Option<&JSONSchema>, +) -> T { + let s = serde_json::to_string(g).unwrap(); + let val: serde_json::Value = serde_json::from_str(&s).unwrap(); + + if let Some(schema) = schema { + let validate = schema.validate(&val); + + if let Err(errors) = validate { + // errors don't necessarily implement Debug + for error in errors { + println!("Validation error: {}", error); + println!("Instance path: {}", error.instance_path); + } + panic!("Serialization test failed."); + } + } + serde_json::from_str(&s).unwrap() +} + +/// Serialize and deserialize a HUGR, and check that the result is the same as the original. +/// Checks the serialized json against the in-tree schema. +/// +/// Returns the deserialized HUGR. +pub fn check_hugr_schema_roundtrip(hugr: &Hugr) -> Hugr { + check_hugr_roundtrip(hugr, true) +} + +/// Serialize and deserialize a HUGR, and check that the result is the same as the original. +/// +/// If `check_schema` is true, checks the serialized json against the in-tree schema. +/// +/// Returns the deserialized HUGR. +pub fn check_hugr_roundtrip(hugr: &Hugr, check_schema: bool) -> Hugr { + let new_hugr: Hugr = ser_roundtrip_validate(hugr, check_schema.then_some(&SCHEMA)); + + // Original HUGR, with canonicalized node indices + // + // The internal port indices may still be different. + let mut h_canon = hugr.clone(); + h_canon.canonicalize_nodes(|_, _| {}); + + assert_eq!(new_hugr.root, h_canon.root); + assert_eq!(new_hugr.hierarchy, h_canon.hierarchy); + assert_eq!(new_hugr.metadata, h_canon.metadata); + + // Extension operations may have been downgraded to opaque operations. + for node in new_hugr.nodes() { + let new_op = new_hugr.get_optype(node); + let old_op = h_canon.get_optype(node); + assert_eq!(new_op, old_op); + } + + // Check that the graphs are equivalent up to port renumbering. + let new_graph = &new_hugr.graph; + let old_graph = &h_canon.graph; + assert_eq!(new_graph.node_count(), old_graph.node_count()); + assert_eq!(new_graph.port_count(), old_graph.port_count()); + assert_eq!(new_graph.link_count(), old_graph.link_count()); + for n in old_graph.nodes_iter() { + assert_eq!(new_graph.num_inputs(n), old_graph.num_inputs(n)); + assert_eq!(new_graph.num_outputs(n), old_graph.num_outputs(n)); + assert_eq!( + new_graph.output_neighbours(n).collect_vec(), + old_graph.output_neighbours(n).collect_vec() + ); + } + + new_hugr +} + +/// Generate an optype for a node with a matching amount of inputs and outputs. +fn gen_optype(g: &MultiPortGraph, node: portgraph::NodeIndex) -> OpType { + let inputs = g.num_inputs(node); + let outputs = g.num_outputs(node); + match (inputs == 0, outputs == 0) { + (false, false) => DFG { + signature: FunctionType::new(vec![NAT; inputs - 1], vec![NAT; outputs - 1]), + } + .into(), + (true, false) => Input::new(vec![NAT; outputs - 1]).into(), + (false, true) => Output::new(vec![NAT; inputs - 1]).into(), + (true, true) => Module.into(), + } +} + +#[test] +fn simpleser() { + let mut g = MultiPortGraph::new(); + + let root = g.add_node(0, 0); + let a = g.add_node(1, 1); + let b = g.add_node(3, 2); + let c = g.add_node(1, 1); + + g.link_nodes(a, 0, b, 0).unwrap(); + g.link_nodes(a, 0, b, 0).unwrap(); + g.link_nodes(b, 0, b, 1).unwrap(); + g.link_nodes(b, 1, c, 0).unwrap(); + g.link_nodes(b, 1, a, 0).unwrap(); + g.link_nodes(c, 0, a, 0).unwrap(); + + let mut h = Hierarchy::new(); + let mut op_types = UnmanagedDenseMap::new(); + + op_types[root] = NodeType::new_open(gen_optype(&g, root)); + + for n in [a, b, c] { + h.push_child(n, root).unwrap(); + op_types[n] = NodeType::new_pure(gen_optype(&g, n)); + } + + let hugr = Hugr { + graph: g, + hierarchy: h, + root, + op_types, + metadata: Default::default(), + }; + + check_hugr_schema_roundtrip(&hugr); +} + +#[test] +fn weighted_hugr_ser() { + let hugr = { + let mut module_builder = ModuleBuilder::new(); + module_builder.set_metadata("name", "test"); + + let t_row = vec![Type::new_sum([type_row![NAT], type_row![QB]])]; + let mut f_build = module_builder + .define_function("main", FunctionType::new(t_row.clone(), t_row).into()) + .unwrap(); + + let outputs = f_build + .input_wires() + .map(|in_wire| { + f_build + .add_dataflow_op( + Noop { + ty: f_build.get_wire_type(in_wire).unwrap(), + }, + [in_wire], + ) + .unwrap() + .out_wire(0) + }) + .collect_vec(); + f_build.set_metadata("val", 42); + f_build.finish_with_outputs(outputs).unwrap(); + + module_builder.finish_prelude_hugr().unwrap() + }; + + check_hugr_schema_roundtrip(&hugr); +} + +#[test] +fn dfg_roundtrip() -> Result<(), Box> { + let tp: Vec = vec![BOOL_T; 2]; + let mut dfg = DFGBuilder::new(FunctionType::new(tp.clone(), tp))?; + let mut params: [_; 2] = dfg.input_wires_arr(); + for p in params.iter_mut() { + *p = dfg + .add_dataflow_op(Noop { ty: BOOL_T }, [*p]) + .unwrap() + .out_wire(0); + } + let hugr = dfg.finish_hugr_with_outputs(params, &EMPTY_REG)?; + + check_hugr_schema_roundtrip(&hugr); + Ok(()) +} + +#[test] +fn opaque_ops() -> Result<(), Box> { + let tp: Vec = vec![BOOL_T; 1]; + let mut dfg = DFGBuilder::new(FunctionType::new_endo(tp))?; + let [wire] = dfg.input_wires_arr(); + + // Add an extension operation + let extension_op: ExtensionOp = NotOp.to_extension_op().unwrap(); + let wire = dfg + .add_dataflow_op(extension_op.clone(), [wire]) + .unwrap() + .out_wire(0); + + // Add an unresolved opaque operation + let opaque_op: OpaqueOp = extension_op.into(); + let wire = dfg.add_dataflow_op(opaque_op, [wire]).unwrap().out_wire(0); + + let hugr = dfg.finish_hugr_with_outputs([wire], &PRELUDE_REGISTRY)?; + + check_hugr_schema_roundtrip(&hugr); + Ok(()) +} + +#[test] +fn function_type() -> Result<(), Box> { + let fn_ty = Type::new_function(FunctionType::new_endo(type_row![BOOL_T])); + let mut bldr = DFGBuilder::new(FunctionType::new_endo(vec![fn_ty.clone()]))?; + let op = bldr.add_dataflow_op(Noop { ty: fn_ty }, bldr.input_wires())?; + let h = bldr.finish_prelude_hugr_with_outputs(op.outputs())?; + + check_hugr_schema_roundtrip(&h); + Ok(()) +} + +#[test] +fn hierarchy_order() -> Result<(), Box> { + let mut hugr = closed_dfg_root_hugr(FunctionType::new(vec![QB], vec![QB])); + let [old_in, out] = hugr.get_io(hugr.root()).unwrap(); + hugr.connect(old_in, 0, out, 0); + + // Now add a new input + let new_in = hugr.add_node(Input::new([QB].to_vec()).into()); + hugr.disconnect(old_in, OutgoingPort::from(0)); + hugr.connect(new_in, 0, out, 0); + hugr.move_before_sibling(new_in, old_in); + hugr.remove_node(old_in); + hugr.update_validate(&PRELUDE_REGISTRY)?; + + let new_hugr: Hugr = check_hugr_schema_roundtrip(&hugr); + new_hugr.validate(&EMPTY_REG).unwrap_err(); + new_hugr.validate(&PRELUDE_REGISTRY)?; + Ok(()) +} + +#[test] +fn constants_roundtrip() -> Result<(), Box> { + let mut builder = DFGBuilder::new(FunctionType::new(vec![], vec![FLOAT64_TYPE])).unwrap(); + let w = builder.add_load_value(ConstF64::new(0.5)); + let hugr = builder.finish_hugr_with_outputs([w], &FLOAT_OPS_REGISTRY)?; + + let ser = serde_json::to_string(&hugr)?; + let deser = serde_json::from_str(&ser)?; + + assert_eq!(hugr, deser); + + Ok(()) +}