Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
basic_data_flow [2026/07/21 22:11]
hermann
basic_data_flow [2026/07/26 14:17] (current)
hermann
Line 3: Line 3:
 ===== Description ===== ===== Description =====
  
-A Dinamica EGO model is, at its core, a graph: **functors** connected through their input and output ports. This is true no matter how the model was authored — built visually in the GUI, or written by hand in [[ego_script|EGO Script]]. ​Both are just different notations for describing ​the same underlying graph, and the execution engine schedules functors from that graph, not from the notation used to write it. This page describes how that scheduling works, independently of any particular notation.+A Dinamica EGO model is, at its core, a graph: **functors** connected through their input and output ports. This is true no matter how the model was authored — built visually in the GUI, saved as [[xml_script|EGOML Script]], or written by hand in [[ego_script|EGO Script]]. ​Either notation ​just describes ​the same underlying graph, and the execution engine schedules functors from that graph, not from the notation used to write it. This page describes how that scheduling works, independently of any particular notation.
  
 ===== Functors and connections ===== ===== Functors and connections =====
Line 10: Line 10:
  
 In [[ego_script|EGO Script]], a connection is expressed by giving an output a variable name and reusing that same name as an input elsewhere — see [[ego_script#​functors_variables_and_binding|Functors,​ variables, and binding]]. In the GUI, the same connection is drawn as a wire between two functor boxes. Either way, what the engine sees is a connection between two ports; the variable name or the wire is only how that connection happens to be written down. In [[ego_script|EGO Script]], a connection is expressed by giving an output a variable name and reusing that same name as an input elsewhere — see [[ego_script#​functors_variables_and_binding|Functors,​ variables, and binding]]. In the GUI, the same connection is drawn as a wire between two functor boxes. Either way, what the engine sees is a connection between two ports; the variable name or the wire is only how that connection happens to be written down.
 +
 +A connection doesn'​t require its two ports to share exactly the same type. When the types are compatible, the value is converted automatically as part of crossing the connection — a Real value connected to a port expecting a Tuple becomes a one-element Tuple, for instance. This conversion is a property of the connection itself, not of the value: the same value, written directly into that port as a literal constant instead of connected to it, is not converted at all — a constant is parsed by its target port's own type-specific parser, which recognizes only that one type's literal syntax and rejects every other form, however closely related the types might otherwise be. See [[ego_script#​constants|Constants]] for how this distinction is expressed in EGO Script specifically.
  
 ===== Data dependencies drive execution order ===== ===== Data dependencies drive execution order =====
Line 39: Line 41:
 This exemption is what makes loops possible at all. Without it, the functor that computes a loop's next state and the mux that carries it forward would depend on each other in a cycle, and neither could ever be scheduled. By treating the feedback connection as "last iteration'​s value" rather than "this iteration'​s value,"​ the engine breaks the cycle — the same trick a register or flip-flop uses in a clocked circuit to let a signal feed back into its own computation. This exemption is what makes loops possible at all. Without it, the functor that computes a loop's next state and the mux that carries it forward would depend on each other in a cycle, and neither could ever be scheduled. By treating the feedback connection as "last iteration'​s value" rather than "this iteration'​s value,"​ the engine breaks the cycle — the same trick a register or flip-flop uses in a clocked circuit to let a signal feed back into its own computation.
  
-The converse also holds. ​If a loop has no mux, and nothing produced ​by a functor ​inside ​it is consumed by anything outside it, then its iterations carry no state forward ​and depend on nothing but themselves — the same as any other pair of independent ​functors. ​In that case there is nothing ​forcing ​them to run one after another, and the engine is free to run several iterations ​at the same time.+The converse also holds. ​loop's iterations can run simultaneously as long as three conditions are met: no mux is used directly inside the loop, nothing produced inside ​the loop is consumed by anything outside it, and no member of the loop is used as a submodel'​s output port. Without a mux, an iteration carries ​no state forward ​to the next one — so, exactly ​as with any other pair of functors, iterations that share no connection have no dependency on each other, and the engine is free to run several of them at once. 
 + 
 +This also means a mux can be used deliberately to force sequential execution, even when a loop would otherwise qualify for parallel execution — any mux inside the loop disqualifies it, since it's the presence of a mux itself, not what it carries, ​that creates the dependency between iterations. ''​MuxValue 0 0'' ​is simply the simplest way to add one: a mux whose initial and feedback inputs are both just the constant ''​0'',​ carrying no real state, added purely to force the ordering. 
 + 
 +===== Destructive updates ===== 
 + 
 +Execution order also affects performance,​ separately from correctness. When a value needs to be destructively updated — modified in place, rather than copied — the engine schedules that update to run after every functor that only reads the value, whenever the dependency graph allows it; by the time the update runs, nothing ​that still needs the old version is left waiting on it, so no copy is needed. This isn't always possible: if two or more functors need to destructively update the same value, no ordering avoids a copy for all of them — only one destructive update can be the last one to run, so at least one of the others has to work from a copy instead. This matters most for large structured values like tables and maps, where copying is expensive — a scalar'​s copy cost is negligible either way. 
 + 
 +This has a particular consequence for mux-fed loops. A value that a functor inside the loop destructively updates on every iteration, and that something outside the loop also reads, can never benefit from the read-before-update scheduling above. The outside reference, per the container rule, can only run once the entire loop has finished — so it can never be scheduled ahead of any single iteration'​s update, on any pass. Since every iteration executes the same loop body, the engine ​cannot tell in advance which pass is the last one that outside reference actually needs; it copies the value on every iteration instead, just to keep it safe for that pending read. Feeding the outside reference from the value produced //after// each iteration'​s update, rather than the mux's own output, avoids creating this conflict ​at all: nothing destructively updates that value again once it's produced, so there is nothing for the outside reference to wait behind. 
 +