Shared machinery for lowering a NESTED op-expression into a flat cell list —
the recursion both host DSLs independently grew (cascade's Lower.resolve_legs,
the portal's Graph.build_node). Same algorithm: walk an op's legs, recurse
into each, a ref resolves to an existing cell-id (no new cell), a nested op
becomes an intermediate cell whose inputs are the recursed leg ids.
The generic walk is parameterized by three host callbacks so each DSL keeps its own id-naming and domain fields:
leg_id.(parent_id, index, leg)→ the id for a nested leg atindex. (portal:"#{parent}/#{i}"; cascade:"#{parent}$#{type}#{i}"or an explicit:as.)ref_id.(ref_leg)→ the cell-id arefpoints at (no new cell emitted).to_cell.(id, node, input_ids)→ build the host's cell for a node given its resolved input ids. Returns whatever cell struct the host uses — the walk never inspects it, only collects it — so a host that keeps its own Cell type (cascade) works as well as one usingReactiveDag.Cell. For a leaf,input_idsis[].
A leg is classified by classify.(leg) → :ref | :op | :leaf. :op legs
recurse; :ref/:leaf are terminal (a leaf still gets a cell; a ref does not).
Returns {root_id, cells} — the expression's root cell-id and every cell it
emitted (intermediates + the root, in dependency order). The host collects
these across all its named nodes and hands the union to Graph.build/1.
Summary
Functions
Lower one node rooted at id into {root_id, cells}, recursing through its
op-legs per the host cb callbacks. ref legs resolve to an existing id and
emit no cell; op legs recurse; leaf legs emit a terminal cell.