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
Top comments (0)