A high-performance Coloured Time Petri Net runtime — a Turing-complete execution engine where typed tokens flow through places, transitions fire under real-time constraints, and async actions execute concurrently. Formal verification proves safety properties via SMT/IC3.
| Implementation | Language | Runtime | Status |
|---|---|---|---|
| libpetri-java | Java 25 | Virtual threads | Production |
| libpetri-ts | TypeScript 5.7 | Promises / event loop | Production |
| libpetri-rust | Rust 2024 | Tokio async tasks | Production |
See
spec/for the language-agnostic contract all implementations follow.
- Executable formal models — Not a simulator. A production runtime where Petri nets are the program: typed tokens are 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.
- Three implementations, one spec — Java, TypeScript, and Rust share 145 language-agnostic requirements covering every arc type, timing variant, and execution phase. Same behavior, verified independently.
- 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 |
|---|---|
| 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 |
| 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 (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 testThe libpetri debug UI is itself a Coloured Time Petri Net — 53 transitions, 51 places (including 30 environment places). 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:
| 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 | 8 | Play/pause/auto-step/step-fwd/step-back/seek/restart/run-to-end with checkpoint-based random access |
| 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 at work:
- Environment places — WebSocket open/close/message events, DOM user interactions (clicks, slider, form submissions), and
requestAnimationFrameticks are injected as external events - Dirty-flag fan-out — A single
stateDirtytoken AND-forks into three independent UI update channels, each gated by arafTickenvironment place for frame-rate throttling - Resource places —
uiState,breakpoints,filterState,searchStateare consumed and re-produced by their transitions, ensuring mutual exclusion on state updates - Timing —
delayed(2000)for reconnect backoff; all other transitions areimmediate() - Guard predicates — Message dispatch transitions use typed guards (
msg.type === 'event', etc.) on thewsMessageenvironment place to route messages to the correct handler
TypeScript code (from debug-ui/src/net/definition.ts)
// Connection transitions
const t_connect = Transition.builder('t_connect')
.inputs(one(idle))
.outputs(outPlace(connecting))
.timing(immediate())
.action(async (ctx) => { /* createWebSocket, setConnecting */ })
.build();
const t_on_open = Transition.builder('t_on_open')
.inputs(one(connecting), one(wsOpenSignal.place))
.outputs(outPlace(connected))
.timing(immediate())
.action(async (ctx) => { /* setConnected, refreshSessions */ })
.build();
const t_reconnect = Transition.builder('t_reconnect')
.inputs(one(waitReconnect))
.outputs(outPlace(idle))
.timing(delayed(2000)) // 2s backoff
.action(async (ctx) => { ctx.output(idle, undefined); })
.build();
// Message dispatch with guard predicates
const t_on_event = Transition.builder('t_on_event')
.inputs(one(uiState), one(wsMessage.place, (msg) => msg.type === 'event'))
.outputs(and(outPlace(uiState), outPlace(stateDirty)))
.timing(immediate())
.action(async (ctx) => { /* applyEventToState, updateTimeline */ })
.build();
// UI fan-out: stateDirty → 3 parallel updates
const t_fan_out_dirty = Transition.builder('t_fan_out_dirty')
.inputs(one(stateDirty))
.outputs(and(outPlace(highlightDirty), outPlace(logDirty), outPlace(markingDirty)))
.timing(immediate())
.build();
// Frame-rate gated UI updates
const t_update_highlighting = Transition.builder('t_update_highlighting')
.inputs(one(highlightDirty), one(rafTick.place))
.reads(svgReady, uiState)
.timing(immediate())
.action(async (ctx) => { /* updateDiagramHighlighting */ })
.build();
// ... 53 transitions total
const net = PetriNet.builder('DebugUI')
.transitions(t_connect, t_on_open, /* ... */)
.build();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. This is a stress test demonstrating that Petri net execution overhead is acceptable even under extreme workloads, not a competitor to javac's hand-tuned recursive descent parser.
Grammar fragment — 3 productions compiled to a 33-place, 31-transition net, showing the hierarchical structure of compiled grammar rules:
Full parser net — all 167 productions, 2,335 places, 2,326 transitions:
How it works:
- 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. The compiler produces a
PetriNetfrom an EBNF-likeGrammardefinition. - 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 — The flat-array executor with
skipOutputValidationhandles the 2,300+ transitions efficiently. StandardBitmapNetExecutorworks too, but the precompiled path is faster at this scale.
Self-parse results: Parses all 85 libpetri core Java source files (17,395 lines) in ~500ms (~34,000 lines/sec). The overhead is measurable compared to javac, but the fact that a general-purpose Petri net runtime can parse a full programming language at all — and do so correctly across 85 real-world files — proves CTPN execution scales far beyond typical workflow orchestration.
Arc types
| Arc | Semantics | Java | TypeScript | Rust |
|---|---|---|---|---|
| Input | Consume token(s) from place | In.one(p) |
one(p) |
one(&p) |
| Output | Deposit token into place | Out.place(p) |
outPlace(p) |
out_place(&p) |
| Inhibitor | Block when place has tokens | .inhibitor(p) |
.inhibitor(p) |
.inhibitor(inhibitor(&p)) |
| Read | Test without consuming | .read(p) |
.read(p) |
.read(read(&p)) |
| Reset | Clear all tokens from place | .reset(p) |
.reset(p) |
.reset(reset(&p)) |
Input cardinality
| Cardinality | Semantics | Java | TypeScript | Rust |
|---|---|---|---|---|
| One | Consume exactly 1 token | In.one(p) |
one(p) |
one(&p) |
| Exactly(n) | Consume exactly n tokens | In.exactly(n, p) |
exactly(n, p) |
exactly(n, &p) |
| All | Drain all tokens (at least 1) | In.all(p) |
all(p) |
all(&p) |
| AtLeast(n) | Consume all, require >= n | In.atLeast(n, p) |
atLeast(n, p) |
at_least(n, &p) |
All input specs support optional guard predicates to filter tokens.
Output routing
| Routing | Semantics | Java | TypeScript | Rust |
|---|---|---|---|---|
| Place | Deposit to a single place | Out.place(p) |
outPlace(p) |
out_place(&p) |
| And | Fork to all children | Out.and(p1, p2) |
and(outPlace(p1), outPlace(p2)) |
and(vec![out_place(&p1), out_place(&p2)]) |
| Xor | Route to exactly one child | Out.xor(p1, p2) |
xor(outPlace(p1), outPlace(p2)) |
xor(vec![out_place(&p1), out_place(&p2)]) |
| Timeout | Fallback output after delay | Out.timeout(Duration, p) |
timeout(ms, outPlace(p)) |
timeout(ms, out_place(&p)) |
| ForwardInput | Pass consumed token through | Out.forwardInput(from, to) |
forwardInput(from, to) |
forward_input(&from, &to) |
Timing variants
| Variant | Interval | Behavior | Java | TypeScript | Rust |
|---|---|---|---|---|---|
| Immediate | [0, inf) | Fire as soon as enabled, no deadline | Timing.immediate() |
immediate() |
immediate() |
| Deadline | [0, d] | Fire anytime before deadline | Timing.deadline(Duration) |
deadline(ms) |
deadline(ms) |
| Delayed | [d, +inf) | Wait at least d, then fire | Timing.delayed(Duration) |
delayed(ms) |
delayed(ms) |
| Window | [a, b] | Fire between a and b | Timing.window(Duration, Duration) |
window(a, b) |
window(a, b) |
| Exact | [t, t] | Fire at precisely t | Timing.exact(Duration) |
exact(ms) |
exact(ms) |
Transitions are force-disabled past their deadline (urgent semantics).
All three implementations include structural verification (P-invariants, siphon/trap analysis, state class graphs). Java and TypeScript additionally support SMT-based verification via Z3 using the IC3/PDR algorithm. Rust has Z3 SMT 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
Prove that a circular token-passing net is deadlock-free — proven structurally via Commoner's theorem without invoking the SMT solver:
import org.libpetri.core.*;
import org.libpetri.smt.*;
import java.time.Duration;
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, ...]Prove that a circular token-passing net is deadlock-free — proven structurally via Commoner's theorem without invoking the SMT solver:
import { SmtVerifier, deadlockFree } from 'libpetri/verification';
const pA = place('A');
const pB = place('B');
const net = PetriNet.builder('TokenRing')
.transitions(
Transition.builder('AtoB').inputs(one(pA)).outputs(outPlace(pB)).build(),
Transition.builder('BtoA').inputs(one(pB)).outputs(outPlace(pA)).build(),
)
.build();
const result = await SmtVerifier.forNet(net)
.initialMarking(m => m.tokens(pA, 1))
.property(deadlockFree())
.timeout(30_000)
.verify();
console.log(result.verdict.type); // 'proven'
console.log(result.verdict.method); // 'structural' (Commoner's theorem)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 |
|---|---|---|---|
| Core model | org.libpetri.core |
libpetri (core exports) |
libpetri-core |
| Runtime | org.libpetri.runtime |
libpetri (runtime exports) |
libpetri-runtime |
| Events | org.libpetri.event |
libpetri (event exports) |
libpetri-event |
| Verification | org.libpetri.smt |
libpetri/verification |
libpetri-verification |
| Export | org.libpetri.export |
libpetri/export |
libpetri-export |
| Analysis | org.libpetri.analysis |
libpetri/verification (analysis exports) |
libpetri-verification |
| Debug | org.libpetri.debug |
libpetri/debug |
libpetri-debug |
| Doclet | org.libpetri.doclet |
libpetri/doclet |
build.rs (Rustdoc SVGs) |
All three 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.
The BitmapNetExecutor interprets the net definition on every firing: it pattern-matches on sealed arc types, looks up tokens in HashMaps, and sorts enabled transitions by priority. The PrecompiledNetExecutor eliminates all of this by precompiling the net into flat arrays and operation sequences for direct execution.
Compilation. PrecompiledNet.compile(net) transforms a PetriNet into flat arrays and operation sequences. Each transition's input and reset arcs become a sequence of integer opcodes:
CONSUME_ONE(0) placeId
CONSUME_N(1) placeId count
CONSUME_ALL(2) placeId
CONSUME_ATLEAST(3) placeId minimum
RESET(4) placeId
Timing constraints are precomputed to milliseconds. Priority levels are pre-sorted and indexed. Output specs are analyzed so that the common case (single output place) is a direct array lookup. The compiled PrecompiledNet is immutable and can be reused across executor instances — in Rust, the executor borrows &PrecompiledNet for zero-cost reuse.
Execution. The precompiled executor replaces every abstraction on the hot path with flat-array operations:
| BitmapNetExecutor | PrecompiledNetExecutor |
|---|---|
Map<Place, ArrayDeque<Token>> |
Per-place token arrays indexed by place ID — no hashing |
| Sealed-type pattern matching per arc | switch on int opcodes in a flat int[] operation sequence |
TreeMap sort of enabled transitions per cycle |
Priority-partitioned circular queues — O(1) next-to-fire |
new TransitionContext() per firing |
Pooled context/input/output objects — zero allocation on sync path |
Per-place Map.get() for token access |
Direct array index by place ID — cached place references |
Java and Rust additionally use ring buffers for token storage and two-level summary bitmaps for dirty/enabled iteration. Rust-specific optimizations: precomputed Arc<str> name arrays, reusable HashMap buffers, sparse enablement masks (Empty/Single/Multi variants).
The execution loop has the same six phases as BitmapNetExecutor and produces identical results. In Java it passes the same 141-test suite via abstract base class inheritance; in Rust it has its own 25-test suite. The concurrency model is also identical: single orchestrator thread with concurrent async actions (virtual threads in Java, Tokio tasks in Rust).
Where the speedup comes from. On pure-sync chains, the compiled path does almost no allocation and touches only contiguous arrays, giving 1.4–4× speedup that grows with scale. On async-dominated workloads the scheduling cost dominates both executors equally, so the speedup converges to ~1× for small chains and emerges at scale. Mixed workloads (the common real-world pattern) see 1.3–3.6×.
Measured with noop event store. Java uses JMH (1 fork, 2 warmup, 3 measurement iterations); TypeScript uses vitest bench; Rust uses Criterion. All times in microseconds (µs/op, lower is better). Java and Rust columns show both the standard BitmapNetExecutor and the PrecompiledNetExecutor (a precompiled flat-array executor that compiles nets to operation sequences).
Scaling note: Thanks to dirty-set optimization, the executor only re-evaluates transitions whose input places changed. The times below therefore reflect cost per transition that is enabled and fires — adding more transitions to a net does not increase per-cycle cost unless they actually fire.
Concurrency model note: In Java the orchestrator runs on its own virtual thread and dispatches each action to a separate virtual thread, so no action can ever block the runtime loop. This gives true multicore parallelism for CPU-bound actions. In TypeScript the orchestrator and all actions share a single-core event loop with zero scheduling overhead but no parallelism. In Rust the sync executor is single-threaded with no runtime overhead; the async executor uses Tokio's multi-threaded task pool for true parallelism. In these benchmarks all actions are trivial, so Java's per-thread scheduling cost is visible while its multicore advantage is not. For real workloads with CPU-bound actions Java and Rust scale across cores while TypeScript remains single-threaded.
All transitions use synchronous (passthrough) actions.
| Transitions | Java Bitmap (µs) | Java Precompiled (µs) | Speedup | TS Bitmap (µs) | TS Precompiled (µs) | Speedup | Rust Bitmap (µs) | Rust Precompiled (µs) | Speedup | Target (PERF-021) |
|---|---|---|---|---|---|---|---|---|---|---|
| 10 | 8.7 | 3.8 | 2.3x | 28.1 | 20.2 | 1.4x | 12.8 | 5.0 | 2.6x | < 100 |
| 20 | 17.3 | 7.2 | 2.4x | 52.4 | 37.0 | 1.4x | 26.8 | 9.3 | 2.9x | |
| 50 | 41.7 | 16.9 | 2.5x | 91.0 | 59.8 | 1.5x | 66.7 | 21.3 | 3.1x | < 500 |
| 100 | 83.5 | 35.7 | 2.3x | 123.8 | 66.3 | 1.9x | 135.6 | 41.1 | 3.3x | |
| 200 | 174.8 | 71.1 | 2.5x | 191.6 | 82.1 | 2.3x | 286.7 | 83.3 | 3.4x | |
| 500 | 552.0 | 185.4 | 3.0x | 429.9 | 112.2 | 3.8x | 783.2 | 206.6 | 3.8x | |
| 1000 | 1432.6 | 394.0 | 3.6x | — | — | — | ||||
| 2000 | 3128.8 | 875.8 | 3.6x | — | — | — | ||||
| 5000 | 10402.3 | 2578.4 | 4.0x | — | — | — | ||||
| 10000 | 30473.9 | 7077.7 | 4.3x | — | — | — |
All transitions dispatch to a virtual thread / microtask / Tokio task.
| Transitions | Java Bitmap (µs) | Java Precompiled (µs) | Speedup | TS Bitmap (µs) | TS Precompiled (µs) | Speedup | Rust Bitmap (µs) | Rust Precompiled (µs) | Speedup |
|---|---|---|---|---|---|---|---|---|---|
| 5 | 19.3 | 19.5 | 1.0x | 26.1 | 23.2 | 1.1x | 8.1 | 4.4 | 1.8x |
| 10 | 38.4 | 37.5 | 1.0x | 49.3 | 44.5 | 1.1x | 15.3 | 7.9 | 1.9x |
| 20 | 79.1 | 78.7 | 1.0x | 96.0 | 83.7 | 1.1x | 31.2 | 15.2 | 2.1x |
| 50 | 189.5 | 194.1 | 1.0x | 161.6 | 133.8 | 1.2x | 80.9 | 34.3 | 2.4x |
| 100 | 413.6 | 352.2 | 1.2x | 197.6 | 143.3 | 1.4x | 160.4 | 65.5 | 2.4x |
| 200 | 804.7 | 308.0 | 2.6x | 274.1 | 162.2 | 1.7x | 328.7 | 129.1 | 2.5x |
| 500 | 1719.9 | 712.3 | 2.4x | 515.0 | 213.4 | 2.4x | 890.6 | 335.0 | 2.7x |
Two transitions are async, the rest synchronous — the common real-world pattern.
| Transitions | Java Bitmap (µs) | Java Precompiled (µs) | Speedup | TS Bitmap (µs) | TS Precompiled (µs) | Speedup | Rust Bitmap (µs) | Rust Precompiled (µs) | Speedup |
|---|---|---|---|---|---|---|---|---|---|
| 10 | 17.3 | 11.1 | 1.6x | 33.3 | 25.5 | 1.3x | 7.3 | 2.5 | 2.9x |
| 20 | 27.3 | 15.8 | 1.7x | 59.3 | 45.8 | 1.3x | 13.8 | 2.7 | 5.1x |
| 50 | 52.9 | 27.0 | 2.0x | 97.3 | 68.2 | 1.4x | 33.5 | 3.3 | 10.2x |
| 100 | 98.9 | 44.5 | 2.2x | 131.7 | 71.3 | 1.8x | 66.0 | 4.5 | 14.7x |
| 200 | 196.0 | 80.7 | 2.4x | 196.6 | 82.5 | 2.4x | 125.1 | 7.0 | 17.9x |
| 500 | 634.7 | 191.3 | 3.3x | 424.5 | 119.3 | 3.6x | 322.3 | 13.7 | 23.5x |
One dispatch transition fans out to N parallel async branches, then joins.
| Branches | Java Bitmap (µs) | Java Precompiled (µs) | Speedup | TS Bitmap (µs) | TS Precompiled (µs) | Speedup | Rust Bitmap (µs) | Rust Precompiled (µs) | Speedup |
|---|---|---|---|---|---|---|---|---|---|
| 5 | 24.9 | 19.7 | 1.3x | 34.7 | 28.6 | 1.2x | 11.7 | 5.3 | 2.2x |
| 10 | 33.1 | 24.0 | 1.4x | 62.7 | 53.7 | 1.2x | 23.1 | 9.2 | 2.5x |
| 20 | 46.7 | 31.9 | 1.5x | 134.5 | 119.3 | 1.1x | 47.0 | 17.7 | 2.7x |
| Scenario | Java Bitmap (µs) | Java Precompiled (µs) | Speedup | TS Bitmap (µs) | TS Precompiled (µs) | Speedup | Rust Bitmap (µs) | Rust Precompiled (µs) | Speedup |
|---|---|---|---|---|---|---|---|---|---|
| Order pipeline (8t, 13p) | 19.0 | 9.7 | 2.0x | 29.1 | 24.0 | 1.2x | 10.9 | 4.1 | 2.7x |
| Large workflow (16t, 17p) | 35.9 | 25.8 | 1.4x | — | — | — |
Impact of different event store implementations on the complex workflow:
| Event Store | µs/op | Overhead vs noop |
|---|---|---|
| noop | 19.2 | — |
| inMemory | 20.7 | +8% |
| debug | 20.5 | +7% |
| debugAware | 21.7 | +13% |
| debug + LogCapture | 22.1 | +15% |
| Event Store | µs/op | Overhead vs noop |
|---|---|---|
| noop | 8.9 | — |
| inMemory | 8.3 | −7%* |
| debug | 11.8 | +33% |
| debugAware | 14.0 | +57% |
*inMemory appearing faster than noop is within measurement noise.
Time to compile a PetriNet into a CompiledNet / PrecompiledNet (bitmap masks, reverse indexes, opcode sequences).
| Places | TS CompiledNet (µs) | TS PrecompiledNet (µs) | Rust CompiledNet (µs) | Rust PrecompiledNet (µs) |
|---|---|---|---|---|
| 10 | 7.1 | 9.9 | 5.9 | 13.8 |
| 50 | 29.5 | 42.9 | 32.1 | 69.3 |
| 100 | 58.3 | 83.7 | 63.0 | 132.0 |
| 500 | 281.9 | 462.3 | 314.3 | 689.4 |
| Event Store | µs/op | Overhead vs noop |
|---|---|---|
| noop | 29.1 | — |
| inMemory | 29.8 | +2% |
| Event Store | µs/op | Overhead vs noop |
|---|---|---|
| noop | 23.4 | — |
| inMemory | 24.2 | +3% |
| Event Store | µs/op | Overhead vs noop |
|---|---|---|
| noop | 12.9 | — |
| inMemory | 16.0 | +24% |
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/The spec/ directory defines the complete engine contract — 145 requirements across 10 files.
| File | Prefix | Scope | Count |
|---|---|---|---|
| 01-core-model.md | CORE | Places, tokens, transitions, arcs, net construction | 33 |
| 02-input-output-specs.md | IO | Input cardinality, output routing, validation | 15 |
| 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, async actions, wake-up | 11 |
| 06-environment-places.md | ENV | External event injection, long-running mode | 9 |
| 07-verification.md | VER | SMT/IC3, state class graph, structural analysis | 10 |
| 08-events-observability.md | EVT | Event types, event store, log capture | 20 |
| 09-export.md | EXP | Graph export, formal interchange | 10 |
| 10-performance.md | PERF | Scaling, benchmarks, memory efficiency | 11 |
| Total | 145 |
Priority: 110 MUST · 29 SHOULD · 6 MAY
See spec/00-index.md for the full cross-reference index 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 5.7, ESM-only, strict mode. Built with tsup, tested with vitest.
cd rust
cargo build # Build all crates
cargo test # Run all tests (256 pass, 2 ignored)
cargo test -p libpetri-core # Single crate
cargo bench # Criterion benchmarksRust 2024 edition, workspace with 6 crates. Feature flags: tokio (async executor), z3 (SMT verification, not yet wired).