Skip to content

refactor: compile Hugr using abstract graph datatype#97

Merged
acl-cqc merged 44 commits intomainfrom
refactor/hugr_graph
Jan 12, 2026
Merged

refactor: compile Hugr using abstract graph datatype#97
acl-cqc merged 44 commits intomainfrom
refactor/hugr_graph

Conversation

@acl-cqc
Copy link
Collaborator

@acl-cqc acl-cqc commented Dec 17, 2025

  • Refactor parent outside HugrOp
  • Make abstract Hugr/Graph type.
    • Stores 2-way edges
    • separates parent from op (different map), and separates out the root.
    • parent-ness is now set when the name is created, not when the op is set (which may be later).
    • and may record for each op a list of which children to order first in the output.
  • Hence, also drop ordering of HugrOp, remove index from Case.

@acl-cqc acl-cqc marked this pull request as ready for review December 18, 2025 13:36
@acl-cqc acl-cqc force-pushed the refactor/hugr_graph branch from b242b34 to 0651887 Compare December 20, 2025 09:24
@acl-cqc acl-cqc requested a review from croyzor December 20, 2025 09:28
@acl-cqc acl-cqc force-pushed the refactor/hugr_graph branch from 0651887 to 365dddc Compare December 20, 2025 10:12
nameSupply :: Namespace
} deriving (Eq, Show) -- we probably want a better `show`

splitNamespace :: String -> State HugrGraph Namespace
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be nice to implement instance FreshMonad (State HugrGraph a) from Brat.Naming

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See other comment - have indeed moved namespace out, so the instance FreshMonad would be something else...

_ -> error "addEdge to/from node not present"
where
addToMap :: Ord k => k -> v -> M.Map k [v] -> M.Map k [v]
addToMap k v m = M.insert k (v:(fromMaybe [] $ M.lookup k m)) m
Copy link
Collaborator

Choose a reason for hiding this comment

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

Extra lookup could be avoided with M.alter (though still a bit awkward!)

  addToMap k v m = M.alter k (\maybeVs -> Just (v : fromMaybe [] maybeVs)) m

or

  addToMap k v m = M.alter k (maybe (Just [v]) (fmap (v:))) m

Copy link
Collaborator Author

@acl-cqc acl-cqc Jan 10, 2026

Choose a reason for hiding this comment

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

Got me going in the right direction....M.alter (Just . (v:) . fromMaybe []) k m is succinct and I think reasonably clear

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

tho it got a bit more complex with the single-incoming-edge check (fromMaybe [] -> maybe [] chk)

nodes :: M.Map NodeId HugrOp,
edges_out :: M.Map NodeId [(Int, PortId NodeId)],
edges_in :: M.Map NodeId [(PortId NodeId, Int)],
nameSupply :: Namespace
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if having the nameSupply here makes sense, it feels like it should be provided by the monad. The monadic functions here could be defined on e.g.:

freshNode :: (MonadState HugrGraph m, FreshMonad m) => NodeId -> String -> m Namespace

but I'm splitting hairs and that smells of premature abstraction

Copy link
Collaborator Author

@acl-cqc acl-cqc Jan 10, 2026

Choose a reason for hiding this comment

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

I agree it's better to move the nameSupply out into the Compile monad as that let's up parametrize everything that doesn't use Namespace/create names by the node type - which will be better for #98 among others. So I've done that.

This parametrized freshNode seems pretty complex, but maybe very neat. What instance would I want - instance FreshMonad Compile? instance FrsehMonad (State (HugrGraph NodeId, Namespace)) ?? (I am slightly nervous of this. FreshMonad requires the !- operator to run in a sub-namespace, but when we do that we also want to run on a completely separate hugr so I do kinda feel that running a separate/inner monad is better...)

And would that requirement be MonadState (HugrGraph NodeId, Namespace) m, FreshMonad? Ah - no need to grab the Namespace out of the state, FreshMonad does that work for us? (I have not seen MonadState before, nor the | m -> s syntax in class Monad m => MonadState s (m :: Type -> Type) | m -> s where ....)

If it is just instance (MonadState (HugrGraph NodeId)) Compile....then all the things in HugrGraph.hs that are ... -> State (HugrGraph n/N) Result could be MonadState (HugrGraph n/N) m => ... -> m Result and we do away with onHugr, is that it? (So onHugr was my poor man's way of doing MonadState, but in this case the poor man's way is simple....)

Copy link
Collaborator Author

@acl-cqc acl-cqc Jan 10, 2026

Choose a reason for hiding this comment

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

I did try instance (MonadState (HugrGraph NodeId)) Compile but got:

error: [GHC-46208]
    Functional dependencies conflict between instance declarations:
      instance MonadState (HugrGraph NodeId) Compile
        -- Defined at Brat/Compile/Hugr.hs:75:10
      instance [safe] Monad m => MonadState s (StateT s m)
        -- Defined in ‘Control.Monad.State.Class’

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 haskell is confused because Compile, being a state monad, already implements MonadState CompilationState and it doesn't know which one to choose

addEdge (src@(Port s o), tgt@(Port t i)) = state $ \h@HugrGraph {..} ->
((), ) $ case (M.lookup s nodes, M.lookup t nodes) of
(Just _, Just _) -> h {
edges_out = addToMap s (o, tgt) edges_out,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We're not doing anything to avoid doubly-wiring a port, maybe there should be a check here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can check the inport yes good plan, checking the output would require understanding the type which feels a bit out of scope for this HugrGraph

})

setFirstChildren :: NodeId -> [NodeId] -> State HugrGraph ()
setFirstChildren p cs = state $ \h -> let nch = M.alter (\Nothing -> Just cs) p (first_children h)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this state could be a modify :: (s -> s) -> State s ()

setOp :: NodeId -> HugrOp -> State HugrGraph ()
-- Insist the parent exists
setOp name op = state $ \h@HugrGraph {parents, nodes} -> case M.lookup name parents of
Nothing -> error "name has no parent"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Nothing -> error "name has no parent"
Nothing -> error ("Node " ++ show name ++ " has no parent")

parents = M.alter (\Nothing -> Just parent) (NodeId freshName) parents
})

setFirstChildren :: NodeId -> [NodeId] -> State HugrGraph ()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
setFirstChildren :: NodeId -> [NodeId] -> State HugrGraph ()
-- INVARIANT: first children for this node must not already be set
setFirstChildren :: NodeId -> [NodeId] -> State HugrGraph ()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Invariants are true afterwards too ;-). "ERRORS if already set"....in Rust this would be "PANICS if alreday set"

setFirstChildren p cs = state $ \h -> let nch = M.alter (\Nothing -> Just cs) p (first_children h)
in ((), h {first_children = nch})

setOp :: NodeId -> HugrOp -> State HugrGraph ()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
setOp :: NodeId -> HugrOp -> State HugrGraph ()
-- INVARIANT: The node op must not already be set
setOp :: NodeId -> HugrOp -> State HugrGraph ()

defNode <- addNode (show fnName ++ "_def") (OpDefn $ FuncDefn moduleNode (show fnName) funTy [])
registerFuncDef idNode (defNode, extra_call)
pure (body defNode)
ctr@Ctr {parent} <- freshNodeWithIO (show fnName ++ "_def") moduleNode
Copy link
Collaborator

Choose a reason for hiding this comment

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

retain calling the parent "defNode" for clarity pls

nodeStackAndIndices :: StackAndIndices
nodeStackAndIndices = let just_root = (B0 :< (root, nodes M.! root), M.singleton root 0)
init = foldl addNode just_root (first_children root)
in foldl addNode init (M.keys parents)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Haskell folklore usually goes "Use foldr instead of foldl. If you really want foldl, use foldl1." Though I looked into it and couldn't convince myself that this use of foldl is problematic

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean foldl' not foldl1 but done, also combined the two uses - copying first_children root is harmless, it'll be at most two elements!

@acl-cqc acl-cqc requested a review from croyzor January 10, 2026 10:18
@croyzor
Copy link
Collaborator

croyzor commented Jan 12, 2026

LGTM

@acl-cqc acl-cqc force-pushed the refactor/hugr_graph branch from 93d9302 to 00b4b16 Compare January 12, 2026 19:59
@acl-cqc acl-cqc merged commit 70ff5bf into main Jan 12, 2026
1 check passed
@acl-cqc acl-cqc deleted the refactor/hugr_graph branch January 12, 2026 20:06
acl-cqc added a commit that referenced this pull request Feb 25, 2026
Uses the HugrGraph ADT added in #97.

`splice` replaces a HoleOp (ignoring the index) with a DFG-rooted Hugr
of matching signature, i.e. inserts the DFG.
`inlineDFG` flattens the result, if desired.

I wasn't sure what the best approach was for dealing with new/old keys,
but the `splice` method is general over both key types by taking a
translation function, which gives some guarantee that we *are*
translating the keys. `splice_prepend` (both NodeID Hugr's) and
`splice_new` (arbitrary-keyed into NodeID) offer two
possibilities.

My hope ATM is that we don't need to deal with order edges since these
are *only* added for nonlocal edges, and so we can do splicing/inlining
*before* adding order edges.

Tests are pretty basic here, i.e. about the simplest possible case, with/without inline.
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