CU2 — Executing a graph of filters
g.node("add", Add(1))g.node("double", Double())g.edge("add", "double")g.forward(41) # → 84.0Status: closed. 29 tests in Rust, 25 in Python.
The starting decision
Section titled “The starting decision”The engine goes in Rust, Python is a wrapper. It is your decision and it has
a consequence worth keeping in mind: it forces Value to exist already. If the
core executes, the data has to have a shape Rust understands.
The four roles, which are easy to confuse:
| piece | role | where |
|---|---|---|
Graph | the structure | soma-core/src/graph.rs |
Catalog | the store of implementations | soma-core/src/filter.rs |
Filter | the contract of an executable unit | soma-core/src/filter.rs |
Graph::run | the engine | soma-core/src/execution.rs |
Decisions taken
Section titled “Decisions taken”Valuewith four variants:Null,Text,Bytes,Tensor. There is noJsonbecause it would requireserde_jsonand the core depends on nothing; there is no opaqueObjectbecause it is only good for sending something down a wire and there is no wire. The conversion error says what is missing rather than inventing a representation.Filterhas one method:forward(&Value) -> Result<Value, FilterError>. Nofit(training is another use case), noconfig_hash(caching), nometa(compiler), nocomposite_fit(autograd).- No
stateparameter. The original passesstateon everyforward, even to stateless filters. State arrives withfit. Send + Syncon the trait — and it is not decoration: PyO3 requires a#[pyclass]to beSend, the graph carries the catalog inside, and the bound climbs to the trait. The compiler found it on its own.- An object without
forwardfails when registered, not halfway through a run. - A bool does not cross the boundary.
Trueas the tensor1.0is the kind of silent conversion nobody understands later.