Skip to content

Commit 8e36cdc

Browse files
aborgna-qss2165
andauthored
feat!: Add Hugr entrypoints (#2147)
Adds the concept of "entrypoint" to the HUGRs, detaching the "main node of interest" from the hierarchy root. Now hugrs will **always** have a `Module` at their root (appropriately called `HugrView::module_root`), and any manipulation is mostly focused around the entrypoint instead. Closes #2029 Closes #2010 before: ```mermaid graph LR subgraph 0 ["(0) DFG"] direction LR 1["(1) Input"] 2["(2) Output"] 3["(3) test.quantum.CX"] 4["(4) test.quantum.CX"] 1--"0:0<br>qubit"-->3 1--"1:1<br>qubit"-->3 3--"0:1<br>qubit"-->4 3--"1:0<br>qubit"-->4 3-."2:2".->4 4--"0:0<br>qubit"-->2 4--"1:1<br>qubit"-->2 end ``` after: ```mermaid graph LR subgraph 0 ["(0) Module"] direction LR subgraph 1 ["(1) FuncDefn: #quot;main#quot;"] direction LR 2["(2) Input"] 3["(3) Output"] subgraph 4 ["(4) [**DFG**]"] direction LR style 4 stroke:#832561,stroke-width:3px 5["(5) Input"] 6["(6) Output"] 7["(7) test.quantum.CX"] 8["(8) test.quantum.CX"] 5--"0:0<br>qubit"-->7 5--"1:1<br>qubit"-->7 7--"0:1<br>qubit"-->8 7--"1:0<br>qubit"-->8 7-."2:2".->8 8--"0:0<br>qubit"-->6 8--"1:1<br>qubit"-->6 end 2--"0:0<br>qubit"-->4 2--"1:1<br>qubit"-->4 4--"0:0<br>qubit"-->3 4--"1:1<br>qubit"-->3 end end ``` # How does this affect... ### ...HugrView? `::root` has been split into `module_root` and `entrypoint`. In general, you'll want to use the latter (as described in the docs). `nodes()` is still defined, but now there's also `descendants(node)` and `entry_descendants()`, which should be preferred. ### ...builders? The change should be transparent for hugr building operations. When you start an specific builder or call `Hugr::new(op)` we add additional nodes as necessary so that the new entrypoint is correctly defined inside a module; - If `op` is a module, we just leave it at the top. - If `op` can be defined in a module, we put it below the root. - If `op` is a dataflow operation, we define a "main" function with the same signature and put it there. - More exotic operations are not allowed, you'll need to build the container yourself and change the entrypoint afterwards. ### ...SiblingGraph, SiblingMut, and DescendantsGraph? The structs are no more. Instead, `HugrView` has two new methods: - `with_entrypoint(&self, Node) -> Rerooted<&Self>` - `with_entrypoint_mut(&mut self, Node) -> Rerooted<&mut Self>` The new wrapper implements `HugrMut`, so it can be used transparently. The main benefit of this is that `Rerooted` still contains all the nodes that are not descendants from the entrypoint. So external edges are still well-defined, and rewrite operations may still put look at the root module and define things there if needed. ### ...serialization? The hugr json now has an `entrypoint` field. If missing, we assume it's the root. For backwards compatibility, if the serialized root is not a module we will transparently wrap it as described before so older jsons will continue working without modifications. hugr-module encoding is left as a TODO ### ...packages and envelopes? Now every HUGR can be put in a package without modifications! Ideally we'll now be able to remove the hugr <-> json methods and only use envelopes. (That's a TODO). ### ...mermaid/dot renders? The entrypoint node is now highlighted in purple, and its title shown in bold between brackets. See the example above. ### ...passes? Things should work as normal. New passes should look at the entrypoint and its descendants when looking for things to rewrite, instead of the whole hugr. # TODOs not in this PR: - [ ] Python support. #2148 - [ ] `hugr-module` support #2156 - [ ] Deprecate/remove hugr json methods, always use envelopes instead. #2159 - [ ] Update the spec. BREAKING CHANGE: `Hugr`s now have an entrypoint node. BREAKING CHANGE: Removed `SiblingGraph` / `SiblingMut` / `DescendantsGraph` --------- Co-authored-by: Seyon Sivarajah <[email protected]>
1 parent 63c317b commit 8e36cdc

78 files changed

Lines changed: 1693 additions & 2021 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hugr-cli/tests/validate.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fn test_package(#[default(bool_t())] id_type: Type) -> Package {
4848
df.finish_with_outputs([i]).unwrap();
4949
let hugr = module.hugr().clone(); // unvalidated
5050

51-
Package::new(vec![hugr]).unwrap()
51+
Package::new(vec![hugr])
5252
}
5353

5454
#[fixture]
@@ -121,7 +121,7 @@ fn test_mermaid_invalid(bad_hugr_string: String, mut cmd: Command) {
121121
cmd.write_stdin(bad_hugr_string);
122122
cmd.assert()
123123
.failure()
124-
.stderr(contains("has an unconnected port"));
124+
.stderr(contains("Error loading hugr"));
125125
}
126126

127127
#[rstest]
@@ -133,7 +133,7 @@ fn test_bad_hugr(bad_hugr_string: String, mut val_cmd: Command) {
133133
val_cmd
134134
.assert()
135135
.failure()
136-
.stderr(contains("Node(1)").and(contains("unconnected port")));
136+
.stderr(contains("Error loading hugr"));
137137
}
138138

139139
#[rstest]

hugr-core/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ hugr-model = { version = "0.19.1", path = "../hugr-model" }
3131

3232
cgmath = { workspace = true, features = ["serde"] }
3333
delegate = { workspace = true }
34-
derive_more = { workspace = true, features = ["display", "error", "from"] }
34+
derive_more = { workspace = true, features = [
35+
"display",
36+
"error",
37+
"from",
38+
"into",
39+
] }
3540
downcast-rs = { workspace = true }
3641
enum_dispatch = { workspace = true }
3742
fxhash.workspace = true

hugr-core/src/builder.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ pub enum BuildError {
151151
/// CFG can only have one entry.
152152
#[error("CFG entry node already built for CFG node: {0}.")]
153153
EntryBuiltError(Node),
154+
/// We don't allow creating `BasicBlockBuilder<Hugr>`s when the sum-rows
155+
/// are not homogeneous. Use a CFGBuilder and create a valid graph instead.
156+
#[error("Cannot initialize hugr for a BasicBlockBuilder with complex sum-rows. Use a CFGBuilder instead.")]
157+
BasicBlockTooComplex,
154158
/// Node was expected to have a certain type but was found to not.
155159
#[error("Node with index {node} does not have type {op_desc} as expected.")]
156160
#[allow(missing_docs)]
@@ -164,6 +168,13 @@ pub enum BuildError {
164168
#[error("Error building Conditional node: {0}.")]
165169
ConditionalError(#[from] conditional::ConditionalBuildError),
166170

171+
/// Node not found in Hugr
172+
#[error("{node} not found in the Hugr")]
173+
NodeNotFound {
174+
/// Missing node
175+
node: Node,
176+
},
177+
167178
/// Wire not found in Hugr
168179
#[error("Wire not found in Hugr: {0}.")]
169180
WireNotFound(Wire),
@@ -303,31 +314,32 @@ pub(crate) mod test {
303314
#[fixture]
304315
pub(crate) fn simple_package() -> Package {
305316
let hugr = simple_module_hugr();
306-
Package::new([hugr]).unwrap()
317+
Package::new([hugr])
307318
}
308319

309320
#[fixture]
310321
pub(crate) fn multi_module_package() -> Package {
311322
let hugr0 = simple_module_hugr();
312323
let hugr1 = simple_module_hugr();
313-
Package::new([hugr0, hugr1]).unwrap()
324+
Package::new([hugr0, hugr1])
314325
}
315326

316327
/// A helper method which creates a DFG rooted hugr with Input and Output node
317328
/// only (no wires), given a function type with extension delta.
318329
// TODO consider taking two type rows and using TO_BE_INFERRED
319330
pub(crate) fn closed_dfg_root_hugr(signature: Signature) -> Hugr {
320-
let mut hugr = Hugr::new(ops::DFG {
331+
let mut hugr = Hugr::new_with_entrypoint(ops::DFG {
321332
signature: signature.clone(),
322-
});
333+
})
334+
.unwrap();
323335
hugr.add_node_with_parent(
324-
hugr.root(),
336+
hugr.entrypoint(),
325337
ops::Input {
326338
types: signature.input,
327339
},
328340
);
329341
hugr.add_node_with_parent(
330-
hugr.root(),
342+
hugr.entrypoint(),
331343
ops::Output {
332344
types: signature.output,
333345
},

hugr-core/src/builder/build_traits.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub trait Container {
9494
name: impl Into<String>,
9595
signature: impl Into<PolyFuncType>,
9696
) -> Result<FunctionBuilder<&mut Hugr>, BuildError> {
97-
let signature = signature.into();
97+
let signature: PolyFuncType = signature.into();
9898
let body = signature.body().clone();
9999
let f_node = self.add_child_node(ops::FuncDefn {
100100
name: name.into(),
@@ -228,9 +228,9 @@ pub trait Dataflow: Container {
228228
hugr: Hugr,
229229
input_wires: impl IntoIterator<Item = Wire>,
230230
) -> Result<BuildHandle<DataflowOpID>, BuildError> {
231-
let optype = hugr.get_optype(hugr.root()).clone();
231+
let optype = hugr.get_optype(hugr.entrypoint()).clone();
232232
let num_outputs = optype.value_output_count();
233-
let node = self.add_hugr(hugr).new_root;
233+
let node = self.add_hugr(hugr).inserted_entrypoint;
234234

235235
wire_up_inputs(input_wires, node, self)
236236
.map_err(|error| BuildError::OperationWiring { op: optype, error })?;
@@ -250,8 +250,8 @@ pub trait Dataflow: Container {
250250
hugr: &impl HugrView,
251251
input_wires: impl IntoIterator<Item = Wire>,
252252
) -> Result<BuildHandle<DataflowOpID>, BuildError> {
253-
let node = self.add_hugr_view(hugr).new_root;
254-
let optype = hugr.get_optype(hugr.root()).clone();
253+
let node = self.add_hugr_view(hugr).inserted_entrypoint;
254+
let optype = hugr.get_optype(hugr.entrypoint()).clone();
255255
let num_outputs = optype.value_output_count();
256256

257257
wire_up_inputs(input_wires, node, self)
@@ -284,6 +284,7 @@ pub trait Dataflow: Container {
284284
/// # Panics
285285
///
286286
/// Panics if the number of input Wires does not match the size of the array.
287+
#[track_caller]
287288
fn input_wires_arr<const N: usize>(&self) -> [Wire; N] {
288289
collect_array(self.input_wires())
289290
}
@@ -676,7 +677,7 @@ fn add_node_with_wires<T: Dataflow + ?Sized>(
676677
nodetype: impl Into<OpType>,
677678
inputs: impl IntoIterator<Item = Wire>,
678679
) -> Result<(Node, usize), BuildError> {
679-
let op = nodetype.into();
680+
let op: OpType = nodetype.into();
680681
let num_outputs = op.value_output_count();
681682
let op_node = data_builder.add_child_node(op.clone());
682683

hugr-core/src/builder/cfg.rs

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ impl CFGBuilder<Hugr> {
148148
signature: signature.clone(),
149149
};
150150

151-
let base = Hugr::new(cfg_op);
152-
let cfg_node = base.root();
151+
let base = Hugr::new_with_entrypoint(cfg_op).expect("CFG entrypoints be valid");
152+
let cfg_node = base.entrypoint();
153153
CFGBuilder::create(base, cfg_node, signature.input, signature.output)
154154
}
155155
}
@@ -224,7 +224,7 @@ impl<B: AsMut<Hugr> + AsRef<Hugr>> CFGBuilder<B> {
224224
self.hugr_mut().add_node_with_parent(parent, op)
225225
};
226226

227-
BlockBuilder::create(self.hugr_mut(), block_n)
227+
BlockBuilder::create_with_io(self.hugr_mut(), block_n)
228228
}
229229

230230
/// Return a builder for a non-entry [`DataflowBlock`] child graph with
@@ -314,7 +314,29 @@ impl<B: AsMut<Hugr> + AsRef<Hugr>> BlockBuilder<B> {
314314
) -> Result<(), BuildError> {
315315
Dataflow::set_outputs(self, [branch_wire].into_iter().chain(outputs))
316316
}
317+
318+
/// Create a new BlockBuilder.
319+
///
320+
/// See [`BlockBuilder::create_with_io`] if you need to initialize the input
321+
/// and output nodes.
322+
///
323+
/// # Parameters
324+
/// - `base`: The base HUGR to build on.
325+
/// - `block_n`: The block we are building.
317326
fn create(base: B, block_n: Node) -> Result<Self, BuildError> {
327+
let db = DFGBuilder::create(base, block_n)?;
328+
Ok(BlockBuilder::from_dfg_builder(db))
329+
}
330+
331+
/// Create a new BlockBuilder, initializing the input and output nodes.
332+
///
333+
/// See [`BlockBuilder::create`] if you don't need to initialize the input
334+
/// and output nodes.
335+
///
336+
/// # Parameters
337+
/// - `base`: The base HUGR to build on.
338+
/// - `block_n`: The block we are building.
339+
fn create_with_io(base: B, block_n: Node) -> Result<Self, BuildError> {
318340
let block_op = base
319341
.as_ref()
320342
.get_optype(block_n)
@@ -348,15 +370,28 @@ impl BlockBuilder<Hugr> {
348370
) -> Result<Self, BuildError> {
349371
let inputs = inputs.into();
350372
let sum_rows: Vec<_> = sum_rows.into_iter().collect();
351-
let other_outputs = other_outputs.into();
352-
let op = DataflowBlock {
353-
inputs: inputs.clone(),
354-
other_outputs: other_outputs.clone(),
355-
sum_rows,
356-
};
357-
358-
let base = Hugr::new(op);
359-
let root = base.root();
373+
let other_outputs: TypeRow = other_outputs.into();
374+
let num_out_branches = sum_rows.len();
375+
376+
// We only support blocks where all the possible `sum_rows` branches have the same types,
377+
// as that lets us branch it directly to an exit node.
378+
if let Some(row) = sum_rows.first() {
379+
if sum_rows.iter().skip(1).any(|r2| row != r2) {
380+
return Err(BuildError::BasicBlockTooComplex);
381+
}
382+
}
383+
let cfg_outputs = sum_rows.first().cloned().unwrap_or_default();
384+
let cfg_outputs = cfg_outputs.extend(other_outputs.as_slice());
385+
386+
let mut cfg = CFGBuilder::new(Signature::new(inputs, cfg_outputs))?;
387+
let block = cfg.entry_builder(sum_rows, other_outputs)?;
388+
let block = block.finish_sub_container()?;
389+
for i in 0..num_out_branches {
390+
cfg.branch(&block, i, &cfg.exit_block())?;
391+
}
392+
let mut base = std::mem::take(cfg.hugr_mut());
393+
let root = block.node();
394+
base.set_entrypoint(root);
360395
Self::create(base, root)
361396
}
362397

@@ -375,7 +410,7 @@ impl BlockBuilder<Hugr> {
375410
pub(crate) mod test {
376411
use crate::builder::{DataflowSubContainer, ModuleBuilder};
377412

378-
use crate::extension::prelude::usize_t;
413+
use crate::extension::prelude::{bool_t, usize_t};
379414
use crate::hugr::validate::InterGraphEdgeError;
380415
use crate::hugr::ValidationError;
381416
use crate::type_row;
@@ -417,6 +452,29 @@ pub(crate) mod test {
417452
Ok(())
418453
}
419454

455+
#[test]
456+
fn basic_cfg_block() -> Result<(), BuildError> {
457+
assert_eq!(
458+
BlockBuilder::new(
459+
vec![],
460+
[vec![usize_t()].into(), vec![bool_t()].into()],
461+
vec![]
462+
),
463+
Err(BuildError::BasicBlockTooComplex)
464+
);
465+
466+
let sum_rows: Vec<TypeRow> = vec![vec![usize_t()].into(), vec![usize_t()].into()];
467+
let mut block_builder =
468+
BlockBuilder::new(vec![usize_t()], sum_rows.clone(), vec![usize_t()])?;
469+
let [inp] = block_builder.input_wires_arr();
470+
let branch = block_builder.make_sum(0, sum_rows, [inp])?;
471+
let hugr = block_builder.finish_hugr_with_outputs(branch, [inp])?;
472+
473+
hugr.validate().unwrap();
474+
475+
Ok(())
476+
}
477+
420478
pub(crate) fn build_basic_cfg<T: AsMut<Hugr> + AsRef<Hugr>>(
421479
cfg_builder: &mut CFGBuilder<T>,
422480
) -> Result<(), BuildError> {

hugr-core/src/builder/conditional.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::hugr::views::HugrView;
22
use crate::types::{Signature, TypeRow};
33

4-
use crate::ops;
5-
use crate::ops::handle::CaseID;
4+
use crate::ops::handle::{CaseID, NodeHandle};
5+
use crate::ops::{self};
66

77
use super::build_traits::SubContainer;
88
use super::handle::BuildHandle;
@@ -153,7 +153,7 @@ impl ConditionalBuilder<Hugr> {
153153
) -> Result<Self, BuildError> {
154154
let sum_rows: Vec<_> = sum_rows.into_iter().collect();
155155
let other_inputs = other_inputs.into();
156-
let outputs = outputs.into();
156+
let outputs: TypeRow = outputs.into();
157157

158158
let n_out_wires = outputs.len();
159159
let n_cases = sum_rows.len();
@@ -163,8 +163,8 @@ impl ConditionalBuilder<Hugr> {
163163
other_inputs,
164164
outputs,
165165
};
166-
let base = Hugr::new(op);
167-
let conditional_node = base.root();
166+
let base = Hugr::new_with_entrypoint(op).expect("Conditional entrypoint should be valid");
167+
let conditional_node = base.entrypoint();
168168

169169
Ok(ConditionalBuilder {
170170
base,
@@ -178,13 +178,15 @@ impl ConditionalBuilder<Hugr> {
178178
impl CaseBuilder<Hugr> {
179179
/// Initialize a Case rooted HUGR
180180
pub fn new(signature: Signature) -> Result<Self, BuildError> {
181-
let op = ops::Case {
182-
signature: signature.clone(),
183-
};
184-
let base = Hugr::new(op);
185-
let root = base.root();
186-
let dfg_builder = DFGBuilder::create_with_io(base, root, signature)?;
187-
181+
// Start by building a conditional with a single case
182+
let mut conditional =
183+
ConditionalBuilder::new([signature.input.clone()], vec![], signature.output.clone())?;
184+
let case = conditional.case_builder(0)?.finish_sub_container()?.node();
185+
186+
// Extract the half-finished hugr, and wrap it in an owned case builder
187+
let mut base = std::mem::take(conditional.hugr_mut());
188+
base.set_entrypoint(case);
189+
let dfg_builder = DFGBuilder::create(base, case)?;
188190
Ok(CaseBuilder::from_dfg_builder(dfg_builder))
189191
}
190192
}
@@ -203,6 +205,14 @@ mod test {
203205

204206
use super::*;
205207

208+
#[test]
209+
fn basic_conditional_case() -> Result<(), BuildError> {
210+
let case_b = CaseBuilder::new(Signature::new_endo(vec![usize_t(), usize_t()]))?;
211+
let [in0, in1] = case_b.input_wires_arr();
212+
case_b.finish_with_outputs([in0, in1])?;
213+
Ok(())
214+
}
215+
206216
#[test]
207217
fn basic_conditional() -> Result<(), BuildError> {
208218
let mut conditional_b =

0 commit comments

Comments
 (0)