Prerequisite: The Token Language, which introduces the four terms this post builds on.
Jon Bentley argued that tiny purpose-built languages — make, awk, pic — outperform general-purpose ones for well-scoped problems. Eric Evans argued that developers and domain experts should share a single vocabulary — what he called "ubiquitous language." Both were pointing at the same thing: the best tool for a domain is a language that speaks the domain's own words. What neither says outright is that in the systems that work, the structural language and the domain language are different layers. The labels make the model readable to humans; the primitives make it analyzable by machines.
The beats sequencer is where the two layers come apart most visibly, though not quite where we first said they did. Each instrument is a ring: euclidean.go builds places p0…p(n-1) and transitions t0…t(n-1), puts one token on p0, and attaches a MIDI binding (note, channel, velocity) to the transitions at hit positions. The places carry no domain words at all. The vocabulary lives one level up — the net is the kick, the snare, the hihat — and in the control nets, whose transitions fire actions named mute-track, unmute-track, activate-slot and fire-macro (arrange.go). A musician works in those words and never needs Petri net theory. And because the sequencer turns token flow into sound, a wrong model is something you hear, not only a test that fails.
Underneath, the four structural primitives — cell, func, arrow, guard — give that vocabulary a formal meaning. Some of it can be read straight off the structure: every transition in a ring consumes from one place and produces into the next, so the token count around the ring is a conservation law of the net. We have not run go-pflow's reachability or deadlock analyzers over generated tracks for this post; that the tools could is a claim about the substrate, not something shown here on a real track.
The word "composition" is doing triple duty. Firing a sequence of transitions adds the corresponding columns of the incidence matrix to the marking (M' = M + C·u), so the arithmetic of runs is integer linear algebra — algebraic composition. The nets themselves compose as morphisms in a symmetric monoidal category, wiring outputs to inputs — categorical composition. And the control nets that wire kick ⊗ snare ⊗ hihat into an arrangement are musical composition. The same four words describe all three.
Forth, the stack language Charles Moore grew out of a personal system he'd been building since 1968, is the oldest instance of this two-layer architecture we know of.
A Forth programmer builds a domain vocabulary and then writes the application in it. A Forth program for controlling a telescope wouldn't look like Forth; it would look like telescope commands. The stack primitives (DUP, SWAP, DROP, OVER) are the structural layer — the invisible substrate nobody talks about. What people read and speak are the words built on top: SLEW, TRACK, CALIBRATE (our illustration, not taken from a real system).
Moore was practicing ubiquitous language decades before Evans gave it a name. You don't see DUP SWAP in a finished Forth application any more than a musician sees cell func arrow while sequencing a drum pattern.
But Forth is Turing-complete, so you can build anything and prove very little about it. The trade we want is to keep the pattern and give up the power.
The token language that drives our Petri net tools is built on four structural primitives:
| Primitive | Role | Structure |
|---|---|---|
cell |
a place that holds things | state container |
func |
an action that changes things | state transition |
arrow |
a connection with direction | flow and dependency |
guard |
a condition that must be true | constraint |
(arrow is our name for what Petri net theory calls an arc — the same concept, friendlier.) Those four are the structural core, but they are a vocabulary for talking about the grammar, not a count of the parser's keywords. The working DSL in go-pflow spells them state, action, arc and :guard, and carries more besides — typed data state like map[address]uint256, arc keys, and constraint forms for invariants (tokenmodel/dsl). go-pflow's lower-level petri package also has inhibitor arcs, which are a second kind of arrow.
These terms are the encoding, the substrate domain languages get built on, and nobody uses them in a meeting. Nobody says "we need a new func." They say "we need a transfer operation" or "add a mute control."
The ubiquitous language lives in the labels. In an ERC-20 token model the cells are balances, totalSupply, allowances, and the funcs are transfer, mint, burn. A financial engineer and a Solidity developer already share these words. They don't need to know what a "cell" is — they need to know that balances feeds into transfer, guarded by sufficient funds. That sentence is the ubiquitous language, and the guard go-pflow actually evaluates is balances[from] >= amount && to != address(0) (tags_test.go builds the full model).
| Forth | Petri Net DSL | |
|---|---|---|
| Structural primitives | DUP, SWAP, DROP, OVER |
cell, func, arrow, guard |
| Domain vocabulary | User-defined words (SLEW, TRACK) |
Labels on places, transitions and nets (balances, transfer, kick) |
| Visible state | The stack | The marking (tokens in places) |
| Composition | Word concatenation | Net wiring (shared places, control nets) |
| What you ship | A domain vocabulary | A labeled net |
Each model constructs a domain vocabulary on top of a fixed grammar. The domain language is whatever the domain already speaks.
Bentley's little languages work because they're closed — you can't escape into general-purpose complexity. A regular expression can't count, which is exactly why it compiles to a finite automaton. The constraint is the feature.
Every Petri net has an incidence matrix C: rows are places, columns are transitions, and C[p][t] is the weight of t's output arc into p minus the weight of p's input arc into t (reachability/invariants.go). An earlier version of this post said the entire behavior of any model is captured in that matrix. It isn't, and the code says so. Inhibitor arcs are skipped when C is built, because they gate firing without moving tokens. Guards never reach it — they are predicates over the marking, the contextual layer that The Category Settle calls R. And in the token-model package, keyed arcs into data maps like balances enter C at unit weight (tokenmodel/petri/analysis.go), so for ERC-20 the matrix sees that transfer moves a balance, not how much.
What C does give us is integer linear algebra, and that is a lot. P-invariants are the semi-positive vectors y with yC = 0; go-pflow computes a minimal-support basis with the Farkas algorithm, so an empty result is a proof that the net has no token-conservation law — structure alone, no state space explored. ODE simulation works because C defines the continuous relaxation. Reachability and deadlock detection are a different kind of check: go-pflow finds them by exploring the state graph (reachability/graph.go), which does respect inhibitor arcs and stops at 10,000 states unless told otherwise. They are answerable for a bounded net because its state space is finite, not because the matrix answers them.
What we give up is the point: no unbounded recursion, no implicit state. The line is thinner than "four primitives, no fifth" makes it sound, though. Ordinary place/transition nets are not Turing-complete, but nets with inhibitor arcs can simulate counter machines and are, and a guard over a data map is an arbitrary predicate — both are already in the toolkit. What keeps the analysis honest is not the number of primitives but which layer each one lives in. What C sees can be proved structurally; what sits outside C has to be checked by exploring states.
Without a structural substrate, labels have no formal meaning. Write a state machine in Go and transfer is just a function name — you can't ask "is this deadlock-free?" without writing a model checker. Write it on the four-term substrate and transfer becomes a transition with input arcs, output arcs, and guards that tools can reason about. Same vocabulary, now analyzable.
We think this is one reason Evans' ubiquitous language erodes in practice. Teams agree on shared vocabulary, write it into Go or Java, and the vocabulary drifts because the programming language imposes its own structure. The domain words get buried in implementation noise. The four-term substrate has no vocabulary of its own to compete with them.
TLA+ is the most serious alternative here. Newcombe et al.'s account of TLA+ at Amazon Web Services describes the model checker finding a bug in DynamoDB's replication and fault-tolerance design that could lose data. But TLA+ conflates the two layers — its structural primitives are the vocabulary. Domain words like balances and sender are embedded in set theory and temporal logic (\E x \in S : P(x), primed variables, EXCEPT notation). A financial engineer won't read a TLA+ spec and see their workflow.
The tradeoff is real: we give up TLA+'s generality — no arbitrary temporal properties, no fairness specs. For properties that fall out of net structure, we get the checks with a lower barrier to entry: conservation from C, and reachability, deadlock freedom and boundedness by exploring a bounded net's states. A domain expert names places and transitions in their own words, and the analysis happens underneath. We have not put the two side by side on one model; that comparison would be the real test of the barrier-to-entry claim.
The test we keep applying is whether a domain expert can label the places and transitions, and a developer can read those labels as a spec. Token standards pass cleanly: "the balances place feeds into transfer, guarded by sufficient funds" is both what an engineer would say and what the ERC-20 schema declares, and go-pflow parses and executes that schema. Games pass more weakly — the tic-tac-toe model names squares P00–P22 and moves X00…O22, which is board notation more than a designer's vocabulary. Music passes one level up from where the thesis says it should: beats puts kick, snare and mute-track on nets and control actions, while its places stay p0, p1, … — and if the model is wrong, you hear it.
The ubiquitous language for a domain can come from labeling a Petri net — its places and transitions, or as beats shows, the nets themselves — in that domain's own words, with four structural primitives underneath that make the labels executable, and provable for the part the incidence matrix sees.
For the class of problems that involve "things in states, changing according to rules," the pattern has held in the models we've built: a small structural grammar, a domain-specific vocabulary, and a clean separation between the two.
For readers of the April version:
kick_0, snare_3, hihat_5 and its transitions are trigger, mute, unmute. The generator names places p0… and transitions t0…; instrument names are on the nets, and the control actions are mute-track, unmute-track, activate-slot, fire-macro. Corrected, with links to the generator source.p0–p8 / play_X_0; the model uses P00–P22 / X00.Hypothesis: four structural primitives plus a labeling layer suffice for "things in states, changing according to rules." Tried on music, token standards and games. It fails if a domain expert can't label the parts, if composed nets don't behave as their composition, or if a property we need can't be checked either from C or by exploring states. The original footer claimed the incidence matrix recovers all behavioral properties and that no test had failed; that one did, and the incidence matrix section now says what C misses.
Related: The Token Language · The Category Settle · Petri Nets as a Music Sequencer · A Generator You Can Read · Symmetric Monoidal Categories · Small Models > LLMs · Earned Compression · pflow.xyz · beats.bitwrap.io