-
Notifications
You must be signed in to change notification settings - Fork 7
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
Make Value::Function contain only the Node (index) of a FuncDefn #1856
Comments
I think it's a very bad idea to literally put the node of the function into the const node. This is another encoding of an edge. All graph operations will break if there are additional hidden edges. E.g. replace the function with a different one (different nodeid). |
So maybe we remove Value::Function altogether and only have |
I think we talked about turning hugr-model terms that referred to nodes, into static edges in the hugr. I mean, giving Const nodes an arbitrary number of static inputs would be...a bigger change than I had anticipated here (not sure how big, depends on all those other-port-kind methods etc.), but that may be where we head eventually |
Let my try to formulate the ideas in my head on where this could go. Functions as parameters, isolated regionsThere appears to be two different ways an operation can be passed a "subgraph" that both make sense but have different characteristics. The first way is to pass a (reference to a) function as a parameter to the operation. Parameters need to be static data, and so an operation that expects a function would have a parameter of type An isolated region in this context is a subgraph that does not have any non-local edges to the outside. The goal is that this subgraph may be represented as its own independent A particular point of this design is that the nested Regions with non-local edgesThe mechanism of passing a function to an operation is not entirely sufficient for all purposes. In particular, it can not be used for the builtin tail loop or conditional. The reason is that the tail loop and conditional allow non-local edges. The intended semantics for the non-local edges in the case of the loop is that the values of those edges are copied so that there is one of them for each iteration of the loop. In the case of the conditional, a non-local edge connected with multiple input ports doesn't need a copy as long as each input port is in a different branch. I don't currently have a good answer on how to capture this behaviour in types, so it would be - for now - restricted to the builtins. The builtins, in addition to parameters, take a list of non-isolated regions. These are represented in the same We might in the future find the need to extend the ability to pass non-isolated regions directly to custom ops, and we might find a way to encode the requirements on the non-local edges in the type system, and to deal with the concerns of #1546 in a satisfactory way. This would likely involve extending the type of operations with the additional parameter list that @acl-cqc mentioned above. But this is not needed at the moment, nor is it fully worked out. Static edgesI think that static edges and
I appreciate that there is value in going from a function declaration to all points where the function is referenced, and that this is something that the static edges do when the reference is from a node. However that precludes us from referring to function declarations/definitions from locations that aren't themselves nodes. This includes terms/ Referring to nodes from
|
@zrho that's super-useful, thank you :) :) Can I check - is it that "isolated" regions cannot have incoming edges from other parts of the Hugr, even static edges (I guess that would mean they cannot refer to terms that contain static references to other parts)? Or is it only that they can't capture runtime values from elsewhere? And, there is the issue of linking - for BRAT purposes, a "nested hugr"/isolated region that can't refer to outer FuncDefns is of little use, i.e. we're likely to have to define our own extension function type anyway. (As e.g. |
You say this, with which I strongly disagree(because it breaks rewriting the const node/func node), and then you say:
With which I agree. We should not refer to nodes from TypeArgs(because then our graph would have implicit edges). We should refer to static edges of the op to which the TypeArg is applied. This solves:
Which seems to be the only objection to static edges that you raise. |
I'm trying to understand how this is compatible with referring to functions from the middle of a This feels like a hack to try and squeeze the use-def chain between
Can you explain what you mean here? |
If you have a static edge pointing to a const node you can modify that const node, (without changing the type) and everything is fine. If you create a new const node replace an old one, you can move the edges out of the old const node to come out of the new const node. I understand your suggestion to be inlining the const value into the type arg. Now the const values have lost their identity. How do you rewrite that const value now? you walk every operation looking for type args that match I guess? How do you know which type args to mutate? Const values can't be checked for equality in general, so this seems hopeless. All of this also applies to FuncDefns, the other case where we use static edges.
Yes that is what I mean. Harder than what? TypeArgs to Ops are currently interpreted in the context of the Op in which they occur. The Op defines the TypeParams to which type var occurances (in ops) refer. I don't think one can sensibly encode |
I have to expand the scope a bit more to explain where I am coming from here. I do not propose to store At the moment I remember there was a guppy issue about using a nat that was passed as a type parameter. With a constant as a
I fear that if we do not address this (not the variables per se but the redundancy) first, whatever solution we come up with to refer to |
To be clear, I am only focused on hugr-core here and above. My primary concern is that we should not add non-edge links between nodes. My understanding of the issue is that it proposes to add a
Values are our constants. This doesn't seem to make sense?.
This is not clear. They are certainly closely related and tightly coupled, but this is not sufficient for them to be essentially the same thing.
RowVar variables occur in Yes it would be good for things to be simpler, but it's not clear that they can be. Even if these ideas were the same as you claim, it's not clear that we could unify them, or that doing so would not incur massive breakage. Implementing things non-generically is not the worst thing. I think it would be more productive to think about how we should enrich Const nodes and extension ops with static egdes. |
The issue is that in order to have references to functions nested in
They definitely can be simpler and we can unify them; I'm happy to go into details how this could be once the current pre-conference crunch is over. It is true that there'd be breakage which is why I haven't touched any of this. Part of the reason for a serialisation format that isn't tied to the internal representation is that we gain more freedom to rearrange internals. We would sooner or later have to do that rearrangement anyway since the current representation is really slow. At that point we can piggyback on that break. |
That's not the case. TypeArgs and Values are components of a Node, not first class nodes themselves.
There are no requirements I'm aware of specifying how fast it needs to be. If there were such requirements it's not clear that they could not be satisfied without large breakage. The majority (or maybe all?) of downstream dependencies use hugr-core to do their work. A serialisation format that isn't tied to the internal representation is of very comfort here. |
Indeed they aren't first class nodes or individually addressable things but they should be precisely in order to track dependencies like this. I am not saying to make Consider this illustration: Your proposal is on the left. The On the right is a variant where the Say that now I have a type variable somewhere in a |
I agree that your picture is an accurate representation of the two options. I think adding an additional layer of bookkeeping on top of the graph is worse than keeping it all in the graph. Type args requiring the context of a node (as they do now, to interpret variable occurences) doesn't seem too bad. |
Ok I'm really sorry for raising this issue just before I went away. As a halfway house / migration issue it was not thought through; (a) I had believed I needed it for static-evaluation of BRAT but I don't (likely I just need to define an extension type with appropriate constant-folding routines), (b) as an intermediate step it indeed breaks some things that we have come to expect from a Hugr. I think to @doug-q I would say - Hugr "graph" is already a mixture of two things: there is the runtime graph (edges down which values are passed) and the static graph (edges down which....nothing is passed: it's a reference from the target to the source). Then the TypeArgs also contain static information, but with massive duplication in order to maintain treelikeness, which is what we need so that a TypeArg can be interpreted in the context of a node (i.e. there always is a node). Lukas' proposal is to combine the latter two in a non-tree-like representation i.e. "terms". These can also be seen as a graph but not treelike. So, rather than "one graph" with multiple edge types, we might have two graphs - one for runtime and one for static - and then, do we use the same edge representation for both? For the static graph, we might not wish to maintain back-references all the time - they are expensive to maintain/update when they are not needed - instead we might store the "edges" in one direction only, and then compute backreferences when needed (potentially updating these under mutation). So, perhaps we have to change more things at a time in order not to lose functionality. I think that leaves open the question of whether there is any intermediate step that's useful. I would seriously consider just removing Value::Function, then, in order to dissuade people from using something whose semantics have never been entirely clear (e.g. linking, e.g. can the inner Hugr be a FuncDefn/Module or does it have to be a DFG? Or a CFG/other parent? etc.) - all the possible variants can then be introduced as extension types when/if desired. This would leave A side goal is devirtualization (detecting when the target of an indirect call is known to be a single FuncDefn). For that we could then add a separate |
A hugr is literally a single graph with labeled edges distinguishing categories. I'm not sure I agree on your characterisation of "runtime graph". (A linear unit type is a useful value edge that carries no information). But I don't think this detail is important.
I think this is a good idea, not because the semantics are unclear but because it can be implemented as a CustomConst.
It can be a monomorphic FuncDefn or a DFG this is defined in the code. IMO we should also allow CFG + TailLoop. I can escape this oppressive tyranny with my own CustomConst though!
This is not true. A CustomConst can have a FunctionType. Also sums-of-products including FunctionType.
A side goal of what? I agree we should do it with dataflow-analysis + constant folding,
Yes I understand the proposal. Maybe it would have been a better design. I can''t see how redesigning everything helps us further our goals. It's not like what we have now doesn't work well. A large redesign is likely to have unforseen downsides. Let's move forward with what we have, not backwards. |
(or FuncDecl, I guess). Then, remove
LoadFunction
- it becomes superceded byLoadConstant
.This allows many use cases of "constant Hugrs" via lambda-lifting (turning Lambdas into top-level functions), and "a function pointer" (no captures, not a closure) is the only runtime interpretation of a
Type::Function
that works without dynamic memory allocation.Ops taking Type::Function thus should generally also take a row of extra arguments. Note that there is still a difference between a
EdgeKind::Value(Type::Function(....))
andEdgeKind:Static(Type::Function(...))
(following #1594 enabling the latter) i.e. do we perform runtime computation to select the function pointer (this cannot also select any captured values - the functions must have exactly the same signature including any "captures"==extra arguments).The many use cases of closures requiring dynamic allocation (captures, runtime-generated Hugrs, etc.) should thus be captured by various extension types, with their own
apply
ops etc. There are just too many variations here, and it's not clear which we should favourI think this corresponds to the hugr-model idea of a value/constant referring to an "isolated" region of the Hugr, where "isolated" means - no incoming nonlocal edges, but static references would be allowed. @zrho right?
The text was updated successfully, but these errors were encountered: