jonathanhogg / flitter

A functional programming language and declarative system for describing 2D and 3D visuals
https://flitter.readthedocs.io
BSD 2-Clause "Simplified" License
40 stars 1 forks source link

The graph model could be a proper DAG #2

Open jonathanhogg opened 1 year ago

jonathanhogg commented 1 year ago

The hokey reference stuff currently allows one to link one piece of the scene tree into another point. It'd be neater if the system just supported a proper DAG (directed acyclic graph). What I'd really like to do is this:

!shader fragment="screen.frag"
    !canvas#main
        ...
    !shader fragment="blur.frag" horizontal=false
        !shader fragment="blur.frag" horizontal=true
            {#main}

For this to work, a bunch of changes would need to be made:

Evaluation changes

At the moment, the evaluation is strictly functional tree reduction with the exception of Top which evaluates each sub-expression (representing a top-level statement) one at a time and appends the results into the context graph. This would need to change to something more like an imperative model, as otherwise there'd be nothing in the graph to query in the example above.

My first thought is to add a new execute() generator method to the statement expressions. This would yield individual Vectors. So Sequence would yield from the execute() method of each sub-expression, IfElse would evaluate() its condition and then yield from one branch or the other, and so on. The default execute() method on most expressions would just yield the result of evaluate(). Simple expressions would still call evaluate() on their sub-expressions for speed (e.g., binary operators). If necessary, an evaluate() method on statement-like expressions would iterate over execute(), collecting the results together into one Vector.

A statement-like expression here would be: Sequence, For, IfElse, Node, Tag, Attributes, Append, Prepend. I guess Let and Function as well, logically, but they both yield nothing. I think partial-evaluation remains unchanged: this really only impacts Search and this cannot be partially evaluated.

Language changes

At the moment, adding a Query below a Node reparents the matches. With a DAG there needs to be a new way to detach a node from one place and attach it in another so one can still do grafting. I'd propose a new append operator that overwrites the list of children instead of appending to it (perhaps !), and a new operator for discarding the results of a statement so that they don't get re-appended to the context graph root (I'm gonna use ? while I think about this):

? {window} !
    !shader fragment=read("feedback.frag")
        {window>*}

This needs some careful thinking about: if ! immediately drops the children of {window}, then the later {window>*} won't match anything and the previous contents of the window get garbage collected. Worse still, if !shader is appended immediately, then {window>*} creates a cycle.

jonathanhogg commented 1 year ago

OK. New plan for dealing with grafting.

Each Node has a weak-set of parents instead of a single parent pointer as now. Nodes that pop out of the top-level statement without any parent nodes are appended to the graph root, otherwise they are ignored – this way top-level queries work as before and there's no need for a new "discard" operator.

Instead of having a smash-append operator, have a stealing version of Query: so maybe {#foo} searches for tagged nodes, but {{#foo}} searches-for and removes the nodes from the place they are found. Note that this latter part is key if the node actually has multiple parents: {{#foo > #bar}} should only steal the #bar node from below the #foo node – it may still be parented somewhere else.

So our standard window shader graft could be:

{window}
    let contents={{window>*}}
    !shader fragment=read("feedback.frag")
        contents

This works, but is kinda ugly. A possible alternative is a delayed-append operator that builds the complete node before appending it to the parent, i.e., waits for the execute() generator to complete before appending the yielded results. Something like:

{window} ?
    !shader fragment=read("feedback.frag")
        {{window>*}}
jonathanhogg commented 11 months ago

Note that if I ditch queries, a la #17, then most of this is moot. In that case, references would need to stay so I might have to think a bit harder about whether they need to be improved / made more orthogonal.