Overview
A Divooka script file ("Divooka Document") is a data container of node graphs.
At its simplest form:
- A Divooka document contains multiple graphs.
- Each graph contains multiple nodes.
- Nodes contain a type, an optional ID, and attributes.
- Node attributes can connect to other nodes' attributes.
In a Dataflow Context, node connections are acyclic; in a Procedural Context, this is more flexible.
Interpretation
To demonstrate the simplicity of the language itself, we write an interpreter from scratch using Python.
The simplest possible interpreter works like this: it is a program that understands a basic acyclic graph of nodes containing Type
and ID
fields, key-value attributes (all strings), and connections between attributes of nodes. Connections are indicated directly as attribute values: if an attribute value starts with @
, then it refers to some input node/attribute, e.g. @Node1.Value
.
Without implementing file reading, and using just a few in-memory instances, a basic interpreter can understand nodes like DefineNumber
, which outputs a number in its Value
attribute, and an AddNumbers
node, which takes values from DefineNumber
nodes’ Value
outputs for its Value1
and Value2
attributes. Finally, a Print
node takes the Result
attribute from the AddNumbers
node as its input.
The interpreter maps node types to corresponding operators to perform actions and produce final results.
# minimal_graph_interpreter.py
# A tiny, in-memory, non-cyclic node graph + interpreter.
# Nodes have: Type, ID, attrs (all strings). Connections are '@NodeID.Attr'.
from typing import Dict, Any, List, Callable, Optional, Tuple
Node = Dict[str, Any] # {"ID": str, "Type": str, "attrs": {str:str}, "state": {str:Any}}
def is_ref(value: Any) -> bool:
return isinstance(value, str) and value.startswith("@") and "." in value[1:]
def parse_ref(ref: str) -> Tuple[str, str]:
# "@NodeID.Attr" -> ("NodeID", "Attr")
target = ref[1:]
node_id, attr = target.split(".", 1)
return node_id, attr
def to_number(s: Any) -> Optional[float]:
if isinstance(s, (int, float)):
return float(s)
if not isinstance(s, str):
return None
try:
return float(int(s))
except ValueError:
try:
return float(s)
except ValueError:
return None
class Interpreter:
def __init__(self, nodes: List[Node]):
# normalize nodes and build index
self.nodes: List[Node] = []
self.by_id: Dict[str, Node] = {}
for n in nodes:
node = {"ID": n["ID"], "Type": n["Type"], "attrs": dict(n.get("attrs", {})), "state": {}}
self.nodes.append(node)
self.by_id[node["ID"]] = node
# map Type -> evaluator
self.ops: Dict[str, Callable[[Node], bool]] = {
"DefineNumber": self.op_define_number,
"AddNumbers": self.op_add_numbers,
"Print": self.op_print,
}
# ---- helpers ----
def get_attr_value(self, node_id: str, attr: str) -> Any:
"""Return the most 'evaluated' value for an attribute (state overrides attrs)."""
node = self.by_id.get(node_id)
if not node:
return None
if attr in node["state"]:
return node["state"][attr]
return node["attrs"].get(attr)
def resolve(self, raw: Any) -> Any:
"""Dereference '@Node.Attr' chains once (graph is acyclic so one hop is enough)."""
if is_ref(raw):
nid, a = parse_ref(raw)
return self.get_attr_value(nid, a)
return raw
def all_resolved(self, values: List[Any]) -> bool:
return all(not is_ref(v) and v is not None for v in values)
# ---- operators ----
def op_define_number(self, node: Node) -> bool:
# Input: attrs["Value"] (string number). Output: state["Value"] (numeric)
if "Value" in node["state"]:
return False # already done
raw = node["attrs"].get("Value")
val = self.resolve(raw)
num = to_number(val)
if num is None:
return False # can't parse yet
node["state"]["Value"] = num
return True
def op_add_numbers(self, node: Node) -> bool:
# Inputs: attrs["Value1"], attrs["Value2"] (can be @ refs). Output: state["Result"]
if "Result" in node["state"]:
return False
v1 = to_number(self.resolve(node["attrs"].get("Value1")))
v2 = to_number(self.resolve(node["attrs"].get("Value2")))
if v1 is None or v2 is None:
return False
node["state"]["Result"] = v1 + v2
return True
def op_print(self, node: Node) -> bool:
# Input: attrs["Result"] (@ ref). Side effect: print once. Also store state["Printed"]=True
if node["state"].get("Printed"):
return False
r = self.resolve(node["attrs"].get("Result"))
# Allow printing numbers or strings once the reference resolves
if r is None or is_ref(r):
return False
print(r)
node["state"]["Printed"] = True
return True
# ---- execution ----
def step(self) -> bool:
"""Try to make progress by evaluating any node whose inputs are ready."""
progressed = False
for node in self.nodes:
op = self.ops.get(node["Type"])
if not op:
# Unknown node type: ignore
continue
progressed = op(node) or progressed
return progressed
def run(self, max_iters: int = 100):
"""Iteratively evaluate until no changes (DAG assumed, so this stabilizes quickly)."""
for _ in range(max_iters):
if not self.step():
return
raise RuntimeError("Exceeded max iterations (graph might be cyclic or ill-formed).")
if __name__ == "__main__":
# --- Example in-memory graph ---
graph = [
{"ID": "Node1", "Type": "DefineNumber", "attrs": {"Value": "3"}},
{"ID": "Node2", "Type": "DefineNumber", "attrs": {"Value": "5"}},
{
"ID": "Adder",
"Type": "AddNumbers",
"attrs": {"Value1": "@Node1.Value", "Value2": "@Node2.Value"},
},
{"ID": "Printer", "Type": "Print", "attrs": {"Result": "@Adder.Result"}},
]
interp = Interpreter(graph)
interp.run() # Should print: 8.0
Summary
The Divooka language demonstrates how a minimalist graph-based specification can serve as a foundation for both computation and orchestration.
Key takeaways:
- Node-Centric Abstraction: Everything is reduced to nodes with types, IDs, and attributes - uniform, extensible, and easy to interpret.
-
Simple Reference Mechanism: The
@NodeID.Attr
convention provides a straightforward but powerful way to connect attributes. - Separation of Concerns: Distinguishing between dataflow (acyclic, deterministic) and procedural (control flow, cyclic) contexts allows Divooka to cover both declarative and imperative styles.
-
Composable Operators: Even with just three operators (
DefineNumber
,AddNumbers
,Print
), meaningful behaviors emerge. - Compact Interpreter Footprint: The entire interpreter is under 200 lines of Python, demonstrating the specification’s simplicity and rapid prototyping potential.
One might ask why not use traditional graph connections. The answer is simplicity: defining connections as local attribute references reduces structure while keeping graphs clean. In dataflow, inputs typically come from a single source, while in procedural contexts, outputs are unique but inputs may be shared - making this lightweight approach intuitive and efficient.
Reference
- Wiki (WIP): https://wiki.methodox.io/en/Standardization/DiLS