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

Re-spec and Implement OutlineCfg rewrite #225

Merged
merged 46 commits into from
Jul 5, 2023
Merged

Re-spec and Implement OutlineCfg rewrite #225

merged 46 commits into from
Jul 5, 2023

Conversation

acl-cqc
Copy link
Contributor

@acl-cqc acl-cqc commented Jun 27, 2023

  • I re-spec'd the rewrite as it was waay too complex, and the transformations I was doing to handle the more awkward cases can all be done via "replace" and "InsertIdentity" first. However, there's still a case where the region you want to nest includes the CFG Entry node (in which case there would be zero incoming edges, and the new basic block would be the entry node of the existing CFG). The spec rules this out but I don't think there is any workaround. I could implement it in the code but it would make it significantly more complicated (at least a couple of "if"s and a bunch more code). I'm wondering if we should add a special-case version of insertIdentity to add a new node before the entry node of a CFG; thoughts?
  • I also added a new method to the builder to allow me to create a CFGBuilder object on an existing CFG-node (so I can then use the CFGBuilder to construct a new Block and then a new CFG inside that).
  • Tieing this all together with the nest_cfgs.rs analysis will be the next PR. At the least, I'll need a way to apply rewrites to a HugrView/HugrMut/subregion-of-a-Hugr...

@@ -513,23 +513,14 @@ pub(crate) mod test {
// entry -> head -> split > merge -> tail -> exit
// | \-> right -/ |
// \---<---<---<---<---<---<---<---<---/
// split is unique successor of head
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a driveby, but I'm doing the same in the new code. (It seemed small enough just to duplicate.)

@cqc-alec
Copy link
Collaborator

cqc-alec commented Jul 4, 2023

However, there's still a case where the region you want to nest includes the CFG Entry node (in which case there would be zero incoming edges, and the new basic block would be the entry node of the existing CFG). The spec rules this out but I don't think there is any workaround. I could implement it in the code but it would make it significantly more complicated (at least a couple of "if"s and a bunch more code). I'm wondering if we should add a special-case version of insertIdentity to add a new node before the entry node of a CFG; thoughts?

Maybe this is the excuse we need to make the CFG's entry node special, exactly mirroring the exit node -- so it would not be a DFB, but just have outputs and no DFG inside it.

At the cost of introducing an extra node, if it makes the replacement API simpler I think it may be worth it. Thoughts?

specification/hugr.md Outdated Show resolved Hide resolved
specification/hugr.md Outdated Show resolved Hide resolved
@@ -95,6 +97,20 @@ impl<B: AsMut<Hugr> + AsRef<Hugr>> CFGBuilder<B> {
inputs: Some(input),
})
}
pub(crate) fn from_existing(base: B, cfg_node: Node) -> Result<Self, BuildError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should probably add a docstring for this?

I must be missing something here but I don't see how we get away with throwing away the entry node...

Is there a test for this 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.

It's not pub - but yes, docstring added.

I've clued about the entry node (the thing is that creating a CFGBuilder doesn't create an entry node - you have to do that explicitly later using e.g. simple_entry_builder and it will prepend that onto the list of children rather than append).

And added a test. (That creates a disconnected basic block, but it'll do for the test.)

src/hugr/rewrite/outline_cfg.rs Outdated Show resolved Hide resolved
}

impl Rewrite for OutlineCfg {
type Error = OutlineCfgError;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why this, is it used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's required for the Rewrite trait, as the type of errors that can be returned by the apply method


impl Rewrite for OutlineCfg {
type Error = OutlineCfgError;
const UNCHANGED_ON_FAILURE: bool = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

again, required for Rewrite trait. (If you put anything in the impl <Trait> for .... block that isn't part of the Trait, you'll get an error. Methods "private" to OutlineCfg would be in impl OutlineCfg i.e. not for any trait.)

src/hugr/rewrite/outline_cfg.rs Outdated Show resolved Hide resolved
@acl-cqc
Copy link
Contributor Author

acl-cqc commented Jul 4, 2023

However, there's still a case where the region you want to nest includes the CFG Entry node (in which case there would be zero incoming edges, and the new basic block would be the entry node of the existing CFG). The spec rules this out but I don't think there is any workaround. I could implement it in the code but it would make it significantly more complicated (at least a couple of "if"s and a bunch more code). I'm wondering if we should add a special-case version of insertIdentity to add a new node before the entry node of a CFG; thoughts?

Maybe this is the excuse we need to make the CFG's entry node special, exactly mirroring the exit node -- so it would not be a DFB, but just have outputs and no DFG inside it.

At the cost of introducing an extra node, if it makes the replacement API simpler I think it may be worth it. Thoughts?

Yes, I had been wondering something similar. (Actually I'd been wishing we'd done that thing whereby Entry/Exit, and Input/Output for DFGs, were edges to/from the parent/container node, but never mind ;-) ). There are some possibilities:

  • Require the entry node to have no predecessors. In which case there's no need to have any children, either (they could go outside the CFG and their outputs wired as inputs to the CFG). So...
  • Add a new "Entry block" subtype (BasicBlock::Entry - we have BasicBlock::DFB and BasicBlock::Exit currently). I guess this has no inputs (implicitly these are the same as the CFG's inputs, just as the outputs of a BasicBlock::Exit are the same as the CFG's outputs), but the same predicate_variants and other_outputs as a normal DFB. [Note the Entry node could include a branch. If it doesn't branch, then really its single successor should have other predecessors, or else we could normalize by merging basic blocks.]
  • Combine the proposed BasicBlock::Entry with BasicBlock::Exit into a BasicBlock::Identity and use that for both entry and exit. I guess this would have predicate_variants and other_outputs, with the idea that
    • for the entry block, the CFG inputs are equal to the Sum of the predicate_variants, plus the other outputs
    • for the exit block, the predicate_variants are the empty list (this is consistent with it having no successors) - that is, a 0-ary Sum, a type of which there are no instances. The inputs and other_outputs are then identical with the outputs of the CFG.

The latter seems both the most sensible and also a bit weird. Hmmm, I'll keep thinking.

I was also wondering if maybe we should just allow the set of blocks to include the entry block. Since the entry block may have other predecessors too, it may still share most of the code with the standard case (where entry block is not in the set of blocks) - I think I will code this up first and see how it looks.

@cqc-alec
Copy link
Collaborator

cqc-alec commented Jul 4, 2023

However, there's still a case where the region you want to nest includes the CFG Entry node (in which case there would be zero incoming edges, and the new basic block would be the entry node of the existing CFG). The spec rules this out but I don't think there is any workaround. I could implement it in the code but it would make it significantly more complicated (at least a couple of "if"s and a bunch more code). I'm wondering if we should add a special-case version of insertIdentity to add a new node before the entry node of a CFG; thoughts?

Maybe this is the excuse we need to make the CFG's entry node special, exactly mirroring the exit node -- so it would not be a DFB, but just have outputs and no DFG inside it.
At the cost of introducing an extra node, if it makes the replacement API simpler I think it may be worth it. Thoughts?

Yes, I had been wondering something similar. (Actually I'd been wishing we'd done that thing whereby Entry/Exit, and Input/Output for DFGs, were edges to/from the parent/container node, but never mind ;-) ). There are some possibilities:

* Require the entry node to have no predecessors. In which case there's no need to have any children, either (they could go outside the CFG and their outputs wired as inputs to the CFG). So...

* Add a new "Entry block" subtype (BasicBlock::Entry - we have BasicBlock::DFB and BasicBlock::Exit currently). I guess this has no inputs (implicitly these are the same as the CFG's inputs, just as the outputs of a BasicBlock::Exit are the same as the CFG's outputs), but the same `predicate_variants` and `other_outputs` as a normal DFB. [Note the Entry node could include a branch. If it doesn't branch, then really its single successor should have other predecessors, or else we could normalize by merging basic blocks.]

* Combine the proposed `BasicBlock::Entry` with `BasicBlock::Exit` into a `BasicBlock::Identity` and use that for both entry and exit. I guess this would have `predicate_variants` and `other_outputs`, with the idea that
  
  * for the entry block, the CFG inputs are equal to the Sum of the predicate_variants, plus the other outputs
  * for the exit block, the predicate_variants are the empty list (this is consistent with it having no successors) - that is, a 0-ary Sum, a type of which there are no instances. The inputs and other_outputs are then identical with the outputs of the CFG.

The latter seems both the most sensible and also a bit weird. Hmmm, I'll keep thinking.

I was also wondering if maybe we should just allow the set of blocks to include the entry block. Since the entry block may have other predecessors too, it may still share most of the code with the standard case (where entry block is not in the set of blocks) - I think I will code this up first and see how it looks.

I'm thinking some sort of hybrid of these ideas:

  • Have distinct node types BasicBlock::{Entry,Exit,DFB}. I think using the same node type for both entry and exit is a little confusing since the conditions on them are different, although operationally they are both the identity.
  • Neither Entry nor Exit has children.
  • Require the Entry node to have no predecessors, no inputs and a single successor; the inputs to the DFG in that successor are just the inputs to the CFG. (We have a choice between doing the logic for the first branch in a DFB node or in a node preceding the CFG; here I choose the first option since it seems a bit less weird.)

@acl-cqc
Copy link
Contributor Author

acl-cqc commented Jul 4, 2023

Ok, I've gone with allowing the set to include the entry node (because it is, after all, just another DFB). You can see the changes necessary in this commit: ddf8fb6 - not that hard at all. Beyond that I've added a test of moving the entry node, refactoring some other test code in nest_cfgs.rs.

(Contrastingly allowing the exit block is much harder, somewhat because it's a separate block type, also the second node in children not the first, and if we move the exit block into the sub-cfg, we need a new exit block because the sub-cfg cannot be the exit block - whereas it can be the entry block.)

I wonder if we need a testutils file/class, maybe even a test_cfgs library - nest_cfgs.rs (in src/algorithm) is a long way away from src/hugr/rewrite/outline_cfg.rs...

@acl-cqc
Copy link
Contributor Author

acl-cqc commented Jul 4, 2023

If we want to add another BasicBlock type, etc., I think that's a wider change, and best in another PR - we could do that after this, but I don't think this is so bad that we have to do that first (and quite possibly not at all)

@acl-cqc acl-cqc requested a review from cqc-alec July 4, 2023 18:25
Copy link
Collaborator

@cqc-alec cqc-alec left a comment

Choose a reason for hiding this comment

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

Looks good, just a few textual suggestions.

specification/hugr.md Outdated Show resolved Hide resolved
specification/hugr.md Outdated Show resolved Hide resolved
specification/hugr.md Outdated Show resolved Hide resolved
specification/hugr.md Outdated Show resolved Hide resolved
specification/hugr.md Outdated Show resolved Hide resolved
@acl-cqc acl-cqc merged commit 0e6fcc7 into main Jul 5, 2023
@acl-cqc acl-cqc deleted the new/outline_cfg branch July 5, 2023 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants