A modern Coloured Time Petri Net runtime with first-class composability.
Write small, reusable subnets with typed interfaces. Compose them into systems the way you compose modules in a programming language. Run them on a real-time-aware executor. Prove them correct with SMT.
| Implementation | Language | Runtime | Status |
|---|---|---|---|
| libpetri-java | Java 25 | Virtual threads | Production |
| libpetri-ts | TypeScript 6.0 | Promises / event loop | Production |
| libpetri-rust | Rust 2024 | Tokio async tasks | Production |
| libpetri-py | Python ≥3.11 | Tokio async via PyO3 | Beta |
See
spec/for the language-agnostic contract all implementations follow.
- Composable like a module system — Define small subnets with typed interfaces (ports + channels), instantiate them with prefix-scoped state, and compose them by structural rewrite into a flat production net.
FusionSetfor shared cross-instance state, per-instance action overrides. Same five abstractions across Java, TypeScript, and Rust. - Executable, not a simulator — Production runtime where Petri nets are the program. Typed tokens carry data, transitions are instructions, timing constraints are deadlines, and the executor is a scheduler. Suitable for agent orchestration, workflow automation, protocol modeling, game logic, UI state machines, and anything with concurrency.
- Four implementations, one spec — Java 25, TypeScript 6.0, Rust 2024, and Python ≥3.11 (PyO3 bindings on the Rust runtime) share 203 language-agnostic requirements covering every arc type, timing variant, execution phase, and the modular composition surface. Same behavior, verified independently in three of four (Python rides on Rust).
- Correlated by identity — A fork mints a fresh opaque name (
freshName/fresh_name) into the token payload, and a join re-merges exactly the sibling tokens of one unit of work by name equality (ν-nets, viaMatchSpec). The match is part of the firing rule, not a check-then-act lookup in external state, and a boundedBudgetplace keeps the correlated fragment decidable. - Turing-complete — Coloured Petri Nets with inhibitor arcs can simulate any Turing machine. libpetri's nets can model arbitrary computation, not just finite-state workflows.
| Capability | Details |
|---|---|
| Modular composition | Open subnets with typed interfaces (ports + channels), composition via structural rewrite, place fusion for shared state, per-instance action binding |
| Arc types | Input, Output, Inhibitor, Read (non-consuming), Reset (clear all) |
| Input cardinality | one, exactly(n), all (drain), atLeast(n) — with optional guard predicates |
| Output routing | place (single), and (fork), xor (choice), timeout, forwardInput |
| ν-nets (correlated identity) | Fresh-name minting (freshName / fresh_name), join-by-name correlation via MatchSpec, bounded-budget decidability lever, incremental O(N log N) join matcher |
| Timing | Immediate, Deadline, Delayed, Window, Exact — with urgent deadline enforcement |
| Executor | Bitmap-based O(W) enablement, dirty-set optimization, priority + FIFO scheduling. Precompiled flat-array executor with 1.5–4× speedup (Java, TypeScript, Rust). |
| Concurrency | Single-threaded orchestrator, concurrent async actions (virtual threads / promises / Tokio tasks) |
| Environment places | External event injection for long-running, event-driven workflows |
| Events | 13 event types, pluggable stores (in-memory, noop, logging, debug) |
| Formal verification | SMT/IC3 via Z3 — deadlock freedom, mutual exclusion, place bounds, unreachability |
| Structural analysis | P-invariants (Farkas), siphon/trap pre-checks, XOR branch analysis |
| State class graph | Berthomieu-Diaz algorithm for timed reachability, plus a name-aware (ν-partition) quotient for correlated nets (Java, TypeScript, Rust) |
| Export | DOT/Graphviz |
import org.libpetri.core.*;
import org.libpetri.runtime.BitmapNetExecutor;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
var request = Place.of("Request", String.class);
var response = Place.of("Response", String.class);
var process = Transition.builder("Process")
.inputs(In.one(request))
.outputs(Out.place(response))
.timing(Timing.deadline(Duration.ofSeconds(5)))
.action(ctx -> {
String req = ctx.input(request);
ctx.output(response, "Processed: " + req);
return CompletableFuture.completedFuture(null);
})
.build();
var net = PetriNet.builder("Example")
.transitions(process)
.build();
try (var executor = BitmapNetExecutor.builder(net, Map.of(
request, List.of(Token.of("hello"))))
.executor(Executors.newVirtualThreadPerTaskExecutor())
.build()) {
Marking result = executor.run();
System.out.println(result.peekFirst(response).value());
// → "Processed: hello"
}cd java && ./mvnw verifyimport {
place, tokenOf, one, outPlace, deadline,
Transition, PetriNet, BitmapNetExecutor
} from 'libpetri';
const request = place<string>('Request');
const response = place<string>('Response');
const process = Transition.builder('Process')
.inputs(one(request))
.outputs(outPlace(response))
.timing(deadline(5000))
.action(async (ctx) => {
const req = ctx.input(request);
ctx.output(response, `Processed: ${req}`);
})
.build();
const net = PetriNet.builder('Example')
.transitions(process)
.build();
const executor = new BitmapNetExecutor(net, new Map([
[request, [tokenOf('hello')]],
]));
const result = await executor.run();
console.log(result.peekFirst(response)?.value);
// → "Processed: hello"cd typescript && npm install && npm testuse libpetri::*;
use libpetri::runtime::environment::ExternalEvent;
let request = Place::<String>::new("Request");
let response = Place::<String>::new("Response");
let process = Transition::builder("Process")
.input(one(&request))
.output(out_place(&response))
.timing(deadline(5000))
.action(async_action(|mut ctx| async move {
let req: std::sync::Arc<String> = ctx.input("Request")?;
ctx.output("Response", format!("Processed: {req}"))?;
Ok(ctx)
}))
.build();
let net = PetriNet::builder("Example").transition(process).build();
let mut marking = Marking::new();
marking.add(&request, Token::at("hello".to_string(), 0));
let mut executor = BitmapNetExecutor::<NoopEventStore>::new(
&net, marking, ExecutorOptions::default(),
);
let (_tx, rx) = tokio::sync::mpsc::unbounded_channel::<ExternalEvent>();
executor.run_async(rx).await;
// → "Processed: hello"cd rust && cargo testimport libpetri as lp
request = lp.Place("Request")
response = lp.Place("Response")
def process(ctx: lp.TransitionContext) -> None:
req = ctx.input("Request")
ctx.output("Response", f"Processed: {req}")
net = (
lp.Net("Example")
.transition(
lp.Transition("Process")
.input(lp.one(request))
.output(lp.out(response))
.timing(lp.deadline(5000))
.action(process)
.build()
)
.build()
)
result = lp.run_sync(net, initial={request: ["hello"]})
print(result.first(response)) # → "Processed: hello"pip install libpetri # from PyPI
# or, from the repo:
cd python && pip install -e ".[dev]" && pytestTokens are untyped Py<PyAny> across the FFI — net structure is type-checked,
runtime token types are not. See python/README.md for the
typing-relaxation note.
Build large Petri nets the way you build large programs: out of small, reusable, parameterised pieces. A SubnetDef is a PetriNet body paired with a typed Interface of named ports (boundary places) and channels (boundary transitions for synchronous fusion). def.instantiate("p1", params) returns an Instance whose internal names are scoped to p1/…. PetriNet.builder().compose(...) weaves an instance into a host net by structural rewrite; the result is a flat PetriNet indistinguishable from a hand-written one.
// A reusable bounded buffer: one input port, one output port, one sync channel.
var items = Place.of("items", Integer.class);
var slots = Place.of("slots", Integer.class);
var buffer = SubnetDef.builder("Buffer", Integer.class)
.place(items).place(slots)
.transition(Transition.builder("enqueue")
.inputs(In.one(slots)).outputs(Out.place(items)).build())
.inputPort("put", slots).outputPort("get", items)
.channel("backpressure", /* boundary transition */)
.build();
// Instantiate and compose into a host pipeline.
var wire = Place.of("wire", Integer.class);
var system = PetriNet.builder("Pipeline")
.compose(producer.instantiate("p1"), b -> b.bindPort("output", wire))
.compose(buffer.instantiate("b1", 4), b -> b.bindPort("put", wire))
.fuse(FusionSet.of("limiter", /* shared slots */))
.build();Instance renaming runs at instantiate(...); every internal place / transition becomes prefix/name. Port bindings rewrite arcs so the port place merges into the caller's place. Channel bindings merge the boundary transition with a caller-side transition (arc union, timing intersection, caller-wins priority, sequential actions). FusionSet is orthogonal to compose — declare N places of the same token type as equivalent for shared cross-instance state (e.g. a global rate limiter). Per-instance action overrides via instance.bindActions(map).
TypeScript, Rust, and Python expose the same five abstractions — SubnetDef, Instance, Interface, FusionSet, compose() on the builder. Composed nets render one subgraph cluster_* per instance prefix on DOT export (EXP-016); the debug protocol carries SubnetInstance descriptors; SubnetDef.verify(harness) runs SMT properties against a subnet in isolation.
See spec/11-modular-composition.md for the full 22-requirement contract (MOD-001..061).
The libpetri debug UI is itself a Coloured Time Petri Net — 55 transitions, 53 places (including 31 environment places) across 13 subnets. The entire UI lifecycle (WebSocket connection, session management, message dispatch, diagram rendering, replay playback, live debugging, breakpoints, search, session archives) is modeled and executed as a CTPN.
Subnet breakdown + patterns at work
| Subnet | Transitions | Pattern |
|---|---|---|
| Connection | 5 | State machine: idle → connecting → connected / waitReconnect, with 2s delayed reconnect |
| Session | 3 | Subscribe, unsubscribe-and-switch, receive session data with DOT diagram |
| Message Dispatch | 12 | Guard predicates on input arcs filter WebSocket messages by type |
| Diagram | 1 | Async DOT→SVG rendering via Graphviz WASM |
| UI Fan-Out | 4 | Single stateDirty token AND-forks to 3 parallel updates (highlighting, event-log, marking) |
| Replay Playback | 10 | Play/pause/auto-step/step-fwd/step-back/seek/restart/run-to-end/breakpoint-pause/breakpoint-resume |
| Live Mode | 4 | Pause/resume/step-forward/step-back via WebSocket commands |
| Inspector | 1 | Place click → token inspection |
| Modal | 2 | Open/close with mutual exclusion (modalClosed ↔ modalOpen) |
| Breakpoints | 2 | Set/clear with list state as a resource place |
| Filter/Search | 4 | Apply filter, search, search-next, search-prev |
| Speed | 1 | Playback speed adjustment |
| Archives | 6 | Browse/list/import/upload archives, net-name filtering |
Patterns: environment places for WebSocket / DOM / RAF events; dirty-flag fan-out where one stateDirty token AND-forks into three RAF-gated UI updates; resource places for uiState / breakpoints / filterState enforcing single-writer semantics; delayed(2000) for reconnect backoff; guard predicates on wsMessage for type-based dispatch. Full source: debug-ui/src/net/definition.ts.
The examples/java-parser/ module implements a complete Java 25 parser as a Coloured Time Petri Net — 167 grammar productions compiled into 2,335 places and 2,326 transitions. A stress test demonstrating CTPN execution overhead is acceptable even under extreme workloads; not a competitor to javac.
Self-parse results: parses all 85 libpetri core Java source files (17,395 lines) in ~500ms (~34,000 lines/sec) using the PrecompiledNetExecutor.
How it works (grammar fragment image + design notes)
A grammar fragment — 3 productions compiled to a 33-place, 31-transition net showing the hierarchical structure:
- Coloured tokens — A single
ParseStatetoken carries position, call stack, and AST through the net. No global mutable state. - Grammar-to-net compilation — Each grammar element (terminal, sequence, choice, repetition, optional, non-terminal call/return) maps to a specific net pattern.
- Structural sharing — Non-terminal calls push a return-site ID onto the token's call stack; a shared return-dispatch transition routes the token back to the correct call site via XOR guards.
- PrecompiledNetExecutor +
skipOutputValidation— the flat-array executor handles the 2,300+ transitions efficiently.
Arc types
| Arc | Semantics | Java | TypeScript | Rust | Python |
|---|---|---|---|---|---|
| Input | Consume token(s) from place | In.one(p) |
one(p) |
one(&p) |
lp.one(p) |
| Output | Deposit token into place | Out.place(p) |
outPlace(p) |
out_place(&p) |
lp.out(p) |
| Inhibitor | Block when place has tokens | .inhibitor(p) |
.inhibitor(p) |
.inhibitor(inhibitor(&p)) |
.inhibitor(lp.inhibitor(p)) |
| Read | Test without consuming | .read(p) |
.read(p) |
.read(read(&p)) |
.read(lp.read(p)) |
| Reset | Clear all tokens from place | .reset(p) |
.reset(p) |
.reset(reset(&p)) |
.reset(lp.reset(p)) |
Input cardinality
| Cardinality | Semantics | Java | TypeScript | Rust | Python |
|---|---|---|---|---|---|
| One | Consume exactly 1 token | In.one(p) |
one(p) |
one(&p) |
lp.one(p) |
| Exactly(n) | Consume exactly n tokens | In.exactly(n, p) |
exactly(n, p) |
exactly(n, &p) |
lp.exactly(n, p) |
| All | Drain all tokens (at least 1) | In.all(p) |
all(p) |
all(&p) |
lp.all_tokens(p) |
| AtLeast(n) | Consume all, require >= n | In.atLeast(n, p) |
atLeast(n, p) |
at_least(n, &p) |
lp.at_least(n, p) |
All input specs support optional guard predicates to filter tokens.
Output routing
| Routing | Semantics | Java | TypeScript | Rust | Python |
|---|---|---|---|---|---|
| Place | Deposit to a single place | Out.place(p) |
outPlace(p) |
out_place(&p) |
lp.out(p) |
| And | Fork to all children | Out.and(p1, p2) |
and(outPlace(p1), outPlace(p2)) |
and(vec![out_place(&p1), out_place(&p2)]) |
lp.and_(lp.out(p1), lp.out(p2)) |
| Xor | Route to exactly one child | Out.xor(p1, p2) |
xor(outPlace(p1), outPlace(p2)) |
xor(vec![out_place(&p1), out_place(&p2)]) |
lp.xor(lp.out(p1), lp.out(p2)) |
| Timeout | Fallback output after delay | Out.timeout(Duration, p) |
timeout(ms, outPlace(p)) |
timeout(ms, out_place(&p)) |
lp.timeout(ms, lp.out(p)) |
| ForwardInput | Pass consumed token through | Out.forwardInput(from, to) |
forwardInput(from, to) |
forward_input(&from, &to) |
lp.forward_input(from, to) |
Timing variants
| Variant | Interval | Behavior | Java | TypeScript | Rust | Python |
|---|---|---|---|---|---|---|
| Immediate | [0, inf) | Fire as soon as enabled, no deadline | Timing.immediate() |
immediate() |
immediate() |
lp.immediate() |
| Deadline | [0, d] | Fire anytime before deadline | Timing.deadline(Duration) |
deadline(ms) |
deadline(ms) |
lp.deadline(ms) |
| Delayed | [d, +inf) | Wait at least d, then fire | Timing.delayed(Duration) |
delayed(ms) |
delayed(ms) |
lp.delayed(ms) |
| Window | [a, b] | Fire between a and b | Timing.window(Duration, Duration) |
window(a, b) |
window(a, b) |
lp.window(a, b) |
| Exact | [t, t] | Fire at precisely t | Timing.exact(Duration) |
exact(ms) |
exact(ms) |
lp.exact(ms) |
Transitions are force-disabled past their deadline (urgent semantics).
All implementations include structural verification (P-invariants, siphon/trap analysis, state class graphs). Java, TypeScript, and Python (when the wheel ships with Z3 available) support SMT-based verification via Z3 using the IC3/PDR algorithm; Rust has Z3 feature-gated but not yet wired.
| Property | Description |
|---|---|
| Deadlock freedom | No reachable state where all transitions are disabled |
| Mutual exclusion | Two places never hold tokens simultaneously |
| Place bound | A place never exceeds k tokens |
| Unreachability | A set of places never all hold tokens simultaneously |
Pipeline: structural siphon/trap analysis → P-invariant computation (Farkas) → XOR branch analysis → SMT encoding → IC3/PDR solving. Many small nets are proven structurally (Commoner's theorem) without invoking the SMT solver at all.
// Java — same API shape exists in TypeScript and Python (lp.verify, lp.deadlock_free, ...).
var pA = Place.of("A", Void.class);
var pB = Place.of("B", Void.class);
var net = PetriNet.builder("TokenRing")
.transitions(
Transition.builder("AtoB").inputs(In.one(pA)).outputs(Out.place(pB)).build(),
Transition.builder("BtoA").inputs(In.one(pB)).outputs(Out.place(pA)).build())
.build();
var result = SmtVerifier.forNet(net)
.initialMarking(m -> m.tokens(pA, 1))
.property(SmtProperty.deadlockFree())
.timeout(Duration.ofSeconds(30))
.verify();
System.out.println(result.verdict()); // Proven[method=structural, ...]The executor runs a single-threaded orchestration loop with six phases per cycle:
- Process completions — collect outputs from finished async actions
- Process events — inject tokens from environment places
- Update enablement — re-evaluate only dirty transitions via bitmap masks
- Enforce deadlines — force-disable transitions past their deadline (urgent semantics)
- Fire transitions — select by priority, then FIFO by enablement time
- Await work — sleep until an action completes, a timer fires, or an event arrives
| Module | Java | TypeScript | Rust | Python |
|---|---|---|---|---|
| Core model | org.libpetri.core |
libpetri (core exports) |
libpetri-core |
libpetri.model |
| Runtime | org.libpetri.runtime |
libpetri (runtime exports) |
libpetri-runtime |
libpetri.runtime |
| Events | org.libpetri.event |
libpetri (event exports) |
libpetri-event |
(via Rust) |
| Verification | org.libpetri.smt |
libpetri/verification |
libpetri-verification |
libpetri.verification |
| Export | org.libpetri.export |
libpetri/export |
libpetri-export |
libpetri.export |
| Debug | org.libpetri.debug |
libpetri/debug |
libpetri-debug |
libpetri.debug |
| Doclet | org.libpetri.doclet |
libpetri/doclet |
build.rs (Rustdoc SVGs) |
— |
All four share the same architecture: immutable net definitions, builder-pattern construction, bitmap-based enablement with dirty-set optimization, and a single-threaded orchestrator dispatching async actions to a separate task pool. Rust uses a tokio feature flag for async execution and a debug feature flag for the debug protocol; Python wraps the Rust runtime through PyO3 (rust/libpetri-py) and ships with .pyi stubs + py.typed.
The BitmapNetExecutor interprets the net definition on every firing (pattern-matches arc types, HashMap-looks up tokens, TreeMap-sorts enabled transitions). The PrecompiledNetExecutor compiles those abstractions away: integer opcode sequences for consume/reset, per-place token arrays indexed by place ID, priority-partitioned circular queues for O(1) next-to-fire, pooled context objects for zero-alloc on the sync path. Java and Rust additionally use ring buffers and two-level summary bitmaps; Rust precomputes Arc<str> name arrays and uses sparse enablement masks. The Python bindings wrap Rust's OwnedPrecompiledNet (an Arc<PrecompiledNet> cache) and inherit the speedup.
Same six execution phases as BitmapNetExecutor, same results — Java shares the test suite via abstract-base inheritance; Rust has its own. Speedup ranges: 1.4–4× on pure-sync chains (grows with scale), ~1× → 2-3× on async-dominated (scheduling cost dominates at small N), 1.3–3.6× on mixed workloads (the common real-world pattern). See spec/05-concurrency.md for the full opcode set + execution semantics (CONC-001..018).
All times in microseconds (µs/op, lower is better), measured with the noop event store on a single dev machine. Tools: Java JMH (1 fork, 2 warmup, 3 measurement), TypeScript vitest bench, Rust Criterion, Python pytest-benchmark. Java and Rust show both BitmapNetExecutor and PrecompiledNetExecutor; TypeScript shows the same pair; Python wraps Rust's OwnedPrecompiledNet through PyO3 — only one executor path exists, so a single Python column is shown with a small FFI-overhead delta vs. the Rust precompiled column.
Concurrency model: Java orchestrator + actions on separate virtual threads (true multicore for CPU-bound actions); TypeScript single-core event loop (no parallelism); Rust sync is single-threaded, async uses Tokio's multi-threaded task pool; Python inherits Rust's model through the FFI (the GIL is released for the executor loop, re-acquired only for Python callbacks). All bench actions are trivial, so Java's per-thread scheduling cost is visible while its multicore advantage is not.
Scaling note: thanks to dirty-set optimization, the executor only re-evaluates transitions whose input places changed. Times reflect cost per transition that fires — adding inactive transitions does not increase per-cycle cost.
ν-join firing check: a correlated join (MatchSpec) selects the matching name on each enablement check. An incremental matcher (NU-022) keeps draining a deep correlation pool at O(N log N) rather than O(N²), and its choice is byte-identical to the reference selector. Dedicated firing-check benchmarks — depth and arity sweeps, scatter/gather, budgeted variants, and a no-match baseline that isolates the ν tax — ship in all four languages (bench_nu_net.py, Rust Criterion, TypeScript vitest, Java JMH).
All transitions use synchronous (passthrough) actions.
| Transitions | Java Bitmap | Java Precomp. | TS Bitmap | TS Precomp. | Rust Bitmap | Rust Precomp. | Python | Target (PERF-021) |
|---|---|---|---|---|---|---|---|---|
| 10 | 11.9 | 3.8 | 40.6 | 22.5 | 14.7 | 5.0 | 8.7 | < 100 |
| 20 | 20.0 | 8.4 | 74.3 | 44.6 | 27.3 | 9.8 | 14.3 | |
| 50 | 43.5 | 17.5 | 191.4 | 76.3 | 73.4 | 24.0 | 30.3 | < 500 |
| 100 | 91.7 | 39.3 | 399.9 | 80.1 | 140.3 | 44.7 | 49.2 | |
| 200 | 189.0 | 70.7 | 794.3 | 92.2 | 283.3 | 80.7 | 92.6 | |
| 500 | 493.4 | 190.6 | 3685.7 | 140.2 | 811.7 | 219.3 | 252.7 | |
| 1000 | 1484.1 | 371.8 | — | — | — | — | — | |
| 2000 | 2917.9 | 869.8 | — | — | — | — | — | |
| 5000 | 11186.3 | 2259.8 | — | — | — | — | — | |
| 10000 | 30485.5 | 5434.3 | — | — | — | — | — |
All times µs/op (lower is better). Python uses Rust's OwnedPrecompiledNet through PyO3 — single column, footnoted; ~10% overhead vs. the bare Rust precompiled path is the FFI cost.
All transitions dispatch to a virtual thread / microtask / Tokio task.
| Transitions | Java Bitmap | Java Precomp. | TS Bitmap | TS Precomp. | Rust Bitmap | Rust Precomp. | Python |
|---|---|---|---|---|---|---|---|
| 5 | 26.3 | 19.7 | 36.8 | 27.8 | 7.9 | 4.5 | 84.2 |
| 10 | 46.2 | 41.4 | 83.3 | 55.3 | 16.2 | 7.4 | 90.1 |
| 20 | 82.9 | 85.4 | 175.2 | 94.3 | 34.6 | 14.0 | 119.4 |
| 50 | 207.0 | 216.6 | 540.7 | 164.0 | 84.6 | 31.9 | 143.2 |
| 100 | 423.1 | 330.5 | 852.9 | 169.6 | 164.9 | 66.7 | 411.4 |
| 200 | 738.1 | 410.4 | 1501.7 | 179.1 | 333.8 | 128.1 | 366.3 |
| 500 | 2332.4 | 717.4 | 4713.4 | 243.5 | 959.4 | 336.2 | 811.1 |
The common real-world pattern: most transitions dispatch async work (I/O, LLM calls, network) and a small fraction (~2%) are synchronous fast-paths (in-memory decisions, guard checks, validation). The bench uses sync_count = n / 50 synchronous transitions, the rest async — N=50 → 1 sync, N=100 → 2 sync, N=500 → 10 sync; chains of N ≤ 20 are 100% async (the 2% formula rounds to zero).
| Transitions | Java Bitmap | Java Precomp. | TS Bitmap | TS Precomp. | Rust Bitmap | Rust Precomp. | Python (async cb) |
|---|---|---|---|---|---|---|---|
| 10 | 39.1 | 37.0 | 65.8 | 47.0 | 7.3 | 2.2 | 164.0 |
| 20 | 82.3 | 76.4 | 106.4 | 93.9 | 14.1 | 2.5 | 289.2 |
| 50 | 175.5 | 182.7 | 267.7 | 150.0 | 36.9 | 3.7 | 542.6 |
| 100 | 353.9 | 311.0 | 580.3 | 150.3 | 74.3 | 5.3 | 1005.3 |
| 200 | 725.2 | 343.6 | 1382.9 | 165.7 | 140.5 | 8.7 | — |
| 500 | 1689.9 | 668.0 | 4009.6 | 203.2 | 336.7 | 18.7 | — |
Python's closest equivalent is bench_chain_async_callback[N] — every transition runs a Python async def f(ctx): ctx.output(...), an upper bound for the mostly-async pattern.
One dispatch transition fans out to N parallel async branches, then joins.
| Branches | Java Bitmap | Java Precomp. | TS Bitmap | TS Precomp. | Rust Bitmap | Rust Precomp. | Python |
|---|---|---|---|---|---|---|---|
| 5 | 35.8 | 26.6 | 47.0 | 35.0 | 11.7 | 5.2 | 8.6 |
| 10 | 36.9 | 38.3 | 86.2 | 65.1 | 27.1 | 9.5 | 14.2 |
| 20 | 68.5 | 38.0 | 181.4 | 214.1 | 59.8 | 18.5 | 23.3 |
| Scenario | Java Bitmap | Java Precomp. | TS Bitmap | TS Precomp. | Rust Bitmap | Rust Precomp. | Python |
|---|---|---|---|---|---|---|---|
| Order pipeline (8t, 13p) | 26.6 | 9.0 | 38.7 | 50.2 | 11.8 | 3.9 | 7.7 |
| Large workflow (16t, 17p) | 42.6 | 36.7 | — | — | — | — | — |
Impact of different event store implementations on a small workload (Java is the complex workflow; TS / Rust use the noop-vs-inMemory linear-chain comparison). All numbers µs/op.
| Implementation | noop | inMemory | debug | debugAware | + LogCapture |
|---|---|---|---|---|---|
| Java BitmapNetExecutor | 26.6 | 24.6 (≈) | 22.2 (≈) | 30.6 (≈) | 32.5 |
| Java PrecompiledNetExecutor | 9.0 | 9.4 (≈) | 19.0 (≈) | 25.4 (≈) | 33.1 |
| TypeScript BitmapNetExecutor | 41.5 | 48.4 (+17%) | — | — | — |
| TypeScript Precompiled | 33.3 | 33.1 (flat) | — | — | — |
| Rust BitmapNetExecutor | 12.7 | 14.5 (+14%) | — | — | — |
Java's debug / debugAware overhead is real but variable; (≈) marks values pulled from JMH-parameterized bench runs whose exact event-store binding is determined at fork time — the magnitudes are stable across runs (debug ≪ debugAware < +LogCapture), individual cell ordering may vary by ~1µs. Rust/TS don't ship dedicated debug-store benchmarks (the debug event store is identical in cost to inMemory because it writes to the same in-memory journal).
Time to compile a PetriNet into a CompiledNet / PrecompiledNet.
| Places | TS Compiled | TS Precomp. | Rust Compiled | Rust Precomp. | Python (compile + build) |
|---|---|---|---|---|---|
| 10 | 15.3 | 15.3 | 5.9 | 8.0 | 9.6 |
| 50 | 36.7 | 56.8 | 30.9 | 45.9 | 45.4 |
| 100 | 71.1 | 111.5 | 60.1 | 105.0 | 87.8 |
| 500 | 362.7 | 552.6 | 297.8 | 401.2 | 466.3 |
cd java && ./mvnw test-compile exec:exec -Pjmh # JMH → benchmark-results.json
cd typescript && npm run bench # vitest bench → stdout
cd rust && cargo bench # Criterion → target/criterion/
cd python && pytest benches/ --benchmark-only --override-ini="testpaths=benches" # pytest-benchmark → stdoutThe spec/ directory defines the complete engine contract — 203 requirements across 12 files.
| File | Prefix | Scope | Count |
|---|---|---|---|
| 01-core-model.md | CORE | Places, tokens, transitions, arcs, net construction | 34 |
| 02-input-output-specs.md | IO | Input cardinality, output routing, validation | 14 |
| 03-timing.md | TIME | Firing intervals, clock semantics, deadlines | 11 |
| 04-execution-model.md | EXEC | Orchestrator loop, scheduling, quiescence | 15 |
| 05-concurrency.md | CONC | Bitmap executor, precompiled flat-array executor, async actions, wake-up | 18 |
| 06-environment-places.md | ENV | External event injection, implicit long-running behavior, executor lifecycle | 11 |
| 07-verification.md | VER | SMT/IC3, state class graph, structural analysis | 11 |
| 08-events-observability.md | EVT | Event types, event store, log capture | 23 |
| 09-export.md | EXP | Graph export, formal interchange, junction nodes, subnet clusters | 17 |
| 10-performance.md | PERF | Scaling, benchmarks, memory efficiency, flat-array executor performance | 14 |
| 11-modular-composition.md | MOD | Subnet definitions, instances, port/channel composition, place fusion, action binding | 26 |
| 12-nu-nets.md | NU | Token name identity, fresh-name minting, join by name equality, bounded-budget decidability | 9 |
| Total | 203 |
See spec/00-index.md for the full cross-reference index, priority breakdown, and coverage matrix.
cd java
./mvnw verify # Full build + tests
./mvnw test # Run all tests
./mvnw test -Dtest="org.libpetri.core.PetriNetTest" # Single test class
./mvnw test -Dtest="*BitmapNetExecutor*" # Wildcard match
./mvnw test-compile exec:exec -Pjmh # Run JMH benchmarks
./mvnw javadoc:javadoc # Generate JavadocsJava 25 (no preview features — all used features are finalized). Uses Maven 3.9.x via wrapper.
cd typescript
npm install # Install dependencies
npm run build # Build with tsup
npm run check # Type-check (tsc --noEmit)
npm test # Run vitest
npm run test:watch # Watch mode
npm test -- core # Run tests matching "core"TypeScript 6.0, ESM-only, strict mode. Built with tsup, tested with vitest.
cd rust
cargo build --workspace --exclude libpetri-py # Build all (libpetri-py is cdylib via maturin)
cargo test --workspace --exclude libpetri-py # Run all tests
cargo test -p libpetri-core # Single crate
cargo bench # Criterion benchmarksRust 2024 edition, workspace with 8 crates (including the PyO3 binding libpetri-py). Feature flags: tokio (async executor), z3 (SMT verification, not yet wired), debug (debug protocol).
cd python
pip install -e ".[dev]" # Editable install (uses maturin)
pytest # Run all tests
maturin develop --release # Re-build the Rust extension in-place
pytest benches/ --benchmark-only \
--override-ini="testpaths=benches"Python ≥3.11, PyO3 0.28 bindings via maturin. Wraps the Rust runtime; ships .pyi stubs + py.typed for IDE / mypy support. See python/README.md.
| Versions | Issue | Severity | Fixed in |
|---|---|---|---|
| 1.5.0–1.5.1 (Java), 1.5.0 (Rust) | drain() with enabled timed transitions causes 100% CPU spin (Java) or premature termination (Rust). Workaround: call close() instead. |
High | 1.5.2 (Java), 1.5.1 (Rust) |
| 1.5.0 (Java) | close() with in-flight async actions causes 100% CPU spin. |
High | 1.5.1 (Java) |
See CHANGELOG.md for details.