The Reaktor graph language — Kotlin common DSL and arbitrary logic
The language consists of graph declarations plus arbitrary Kotlin common logic. It is a Kotlin DSL that interoperates with ordinary Kotlin Multiplatform code. Developers can use functions, classes, loops, recursion, higher-order functions, collections and compatible coroutine libraries. Useful algorithms do not have to become boxes.
The deployment design compiles the common logic to Kotlin/JS, packages a revisioned region and uses a qualified Reaktor FFI/host adapter to interact with each target. Common code can inspect graph structure and interact with the running distributed system. Graph inspection and system effects have different authority and execution rules.
Declarations, implementations and source ownership
A declaration gives a region, node, port or relation a stable identity and contract. An implementation is ordinary Kotlin or an explicitly identified foreign/source boundary. A build artifact records which source and dependency revisions implement the declared contract for a target.
The new structural builder is a real common-source API. This declaration compiles in the graph-port common tests:
import dev.shibasis.reaktor.portgraph.definition.*
val replicas = graphDefinition("replicas", "r1") {
region("cloud", "Cloud")
for (i in 0..2) {
node("replica$i", "Replica $i", "N25", "cloud", ports = listOf(
PortDefinition("vote", "P03", "VoteV1", PortPolarity.Offer)
))
}
relation("quorum", "H13",
*Array(3) { Incidence("replica$it", "vote", "voter", it) }
)
}
Here Kotlin performs the loop; the graph stores its resulting declaration. H13 identifies a quorum vocabulary pattern. The structural builder validates endpoints and role slots, but does not implement consensus or prove quorum safety. The domain verifier and qualified execution adapter must supply that protocol.
The DSL uses Kotlin receiver functions and a DSL marker. These are normal language mechanisms for building nested structures; they do not create a separate restricted expression language. Kotlin type-safe builders.
Source ownership must be explicit. A hand-authored Kotlin region owns its logic; supported graph edits return source patches for that adapter's understood subset. A generated region can instead own generated Kotlin as an artifact. Lossy diagrams cannot overwrite arbitrary source, comments or custom algorithms.
Arbitrary common logic and existing runtime ports
The current runtime already supplies typed provider/consumer invocation. A focused common test uses this pattern, recursively evaluates ordinary Kotlin, inspects graph nodes and closes the graph:
import dev.shibasis.reaktor.portgraph.graph.PortGraph
import dev.shibasis.reaktor.portgraph.graph.connect
import dev.shibasis.reaktor.portgraph.node.PortNode
import dev.shibasis.reaktor.portgraph.port.registerConsumer
import dev.shibasis.reaktor.portgraph.port.registerProvider
private class CommonGraph : PortGraph<CommonGraph, PortNode<CommonGraph>>()
private fun normalKotlin(n: Int): Int =
if (n <= 1) n else normalKotlin(n - 1) + normalKotlin(n - 2)
fun main() {
val graph = CommonGraph()
val service = PortNode(graph, label = "Service").also(graph::attach)
val controller = PortNode(graph, label = "Controller").also(graph::attach)
val supplied = service.registerProvider<(Int) -> Int>("compute", ::normalKotlin)
val required = controller.registerConsumer<(Int) -> Int>("compute")
connect(required, supplied).getOrThrow()
val result = graph.nodes.filter { it.label == "Controller" }
.map { required { invoke(8) } }.single()
check(result == 21)
graph.close()
}
This is a finite executable example of graph-aware Kotlin, not a proof of general termination or universal target execution. The definition DSL and the live runtime API have explicit roles: declaring structure does not automatically instantiate or bind a runtime.
Domain logic can call normal shared KMP functions for pricing, validation, CRDT operations, reconciliation or native request preparation. Platform-specific APIs belong behind target adapters or source-set implementations.
Graph access and distributed system access
| Operation | Language surface and required boundary |
|---|---|
| Inspect a fixed graph snapshot | Pure bounded query; record snapshot revision and facet visibility |
| Compute a graph change | Ordinary Kotlin produces a typed proposal/patch against a base revision |
| Activate, move or replace a region | Fingerprinted plan, target legality, authority, execution and reconciliation receipt |
| Invoke a service or actor | Typed port with verified principal/context, deadline, errors and transport profile |
| Observe distributed state | Source authority, observation time/cut, staleness and completeness remain explicit |
| Use a device/native resource | Qualified FFI/adapter contract for memory, ABI, lifetime, errors, cancellation and host thread |
A visitor remains a pure bounded query even though the whole language can perform arbitrary computation and IO. Effectful system logic uses commands and ports; calling it a visitor would obscure its authority and failure behavior. Runtime hot calls bind typed endpoints ahead of invocation rather than performing arbitrary graph-database queries for each call.
The user language may be computationally general while a formal check uses a restricted profile. Static dataflow, protocol models and bounded state machines can admit stronger analyses under their stated assumptions. General Kotlin cannot have a complete automatic termination, effect or equivalence checker.
Kotlin/JS deployment and Reaktor FFI
Kotlin/JS translates compatible Kotlin code and dependencies into JavaScript. It is an established compiler path, not a promise that JVM reflection, native libraries or every common dependency can execute in a JS host. JS exports and interop also have constraints that a generated adapter must respect. Kotlin/JS overview, Kotlin-to-JavaScript interop.
The desired lowering path is:
common graph DSL + ordinary KMP logic
→ structural definition + source ownership/contract references
→ target legality and dependency checks
→ Kotlin/JS artifact + schema/bridge adapters + source maps
→ signed region bundle and host admission plan
→ qualified JS embedding / Reaktor FFI calls for that target
→ activation epoch, observations and reconciliation receipts
Every target needs an execution host or an explicit remote/external boundary. Constrained IoT firmware and hard real-time robotics are not automatically JavaScript targets. FFI needs qualified ABI, encoding, ownership, thread affinity and failure behavior. Current FFI documentation and source own what bridges actually exist; the desired path does not upgrade unsupported adapters into shipped functionality.
Code push is a region protocol
A code-push bundle must identify the exact definition/source/dependency revisions, exported interfaces, required host capabilities, effect/authority requirements and state schema. Admission checks those requirements for the selected target. Publication introduces a new runtime epoch.
Old invocations remain pinned to their admitted code/route generation until the host's drain or migration policy completes. Shared mutable state needs its own consistency contract. A rollback can require a compensating migration and may be impossible after irreversible effects.
The mobile feature, distributed release and state migration designs show different consequences of the same protocol.