libpetri 2.12.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,172 +1,83 @@
1
- # libpetri
1
+ # libpetri for TypeScript
2
2
 
3
- TypeScript implementation of a **Coloured Time Petri Net** (CTPN) engine with bitmap-based execution, formal verification via Z3, and DOT/Graphviz visualization.
3
+ [![npm](https://img.shields.io/npm/v/libpetri)](https://www.npmjs.com/package/libpetri)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6-blue)](https://www.typescriptlang.org/)
5
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/debe/libpetri/blob/main/LICENSE)
4
6
 
5
- ## Architecture
7
+ The TypeScript 6 implementation of libpetri: typed Coloured Time Petri Nets for Promise-based applications, with modular composition, two execution backends, observability, DOT export, and formal verification.
6
8
 
7
- ```
8
- src/
9
- ├── core/ # Net definition: places, transitions, arcs, timing, output specs
10
- ├── runtime/ # Async bitmap-based executor, marking, compiled net
11
- ├── event/ # Event store and net event types (discriminated union)
12
- ├── export/ # DOT (Graphviz) diagram exporter
13
- └── verification/ # SMT-based property verification (Z3)
14
- ```
15
-
16
- ### Core (`src/core/`)
17
-
18
- Immutable net definitions with typed, colored tokens.
19
-
20
- | Type | Description |
21
- |------|-------------|
22
- | `Place<T>` | Typed token container (phantom type for compile-time safety) |
23
- | `EnvironmentPlace<T>` | External event injection point |
24
- | `Transition` | Arc specs, timing, priority, guards, action binding |
25
- | `PetriNet` | Immutable net definition; `bindActions()` separates structure from runtime behavior |
26
- | `Out` | Discriminated union for output specs: `and`, `xor`, `place`, `timeout`, `forward-input` |
27
- | `In` | Input arc specs with cardinality: `one`, `exactly`, `all`, `at-least` |
28
- | `Timing` | TPN firing intervals: `immediate`, `deadline`, `delayed`, `window`, `exact` |
29
- | `TransitionAction` | `(ctx: TransitionContext) => Promise<void>` — async action bound to a transition |
30
-
31
- ### Runtime (`src/runtime/`)
32
-
33
- Async single-threaded executor using bitmap-based enablement tracking.
34
-
35
- | Type | Description |
36
- |------|-------------|
37
- | `BitmapNetExecutor` | Main executor — dirty-set tracking, priority scheduling, deadline enforcement |
38
- | `CompiledNet` | Precomputed bitmap masks and reverse indices for O(W) enablement checks |
39
- | `Marking` | Mutable FIFO token state per place |
40
-
41
- Key performance features:
42
- - `Uint32Array` bitmaps for place marking and transition dirty sets
43
- - Kernighan's bit-trick for dirty set iteration
44
- - Pre-allocated buffers to reduce GC pressure
45
- - Precomputed reverse index (place → affected transitions)
46
-
47
- ### Event (`src/event/`)
48
-
49
- Observable execution events as a discriminated union (`NetEvent`).
50
-
51
- Event types: `execution-started`, `execution-completed`, `transition-enabled`, `transition-started`, `transition-completed`, `transition-failed`, `transition-timed-out`, `action-timed-out`, `token-added`, `token-removed`, `log-message`, `marking-snapshot`.
52
-
53
- `InMemoryEventStore` captures events; `noopEventStore()` is a zero-cost singleton for production.
54
-
55
- ### Export (`src/export/`)
56
-
57
- `dotExport(net, config)` generates DOT (Graphviz) diagram syntax with proper Petri net visual conventions, including arc types (inhibitor, read, reset), timing annotations, and priority labels.
58
-
59
- ### Verification (`src/verification/`)
9
+ See the [project README](https://github.com/debe/libpetri#why-a-petri-net) for the motivation and an order workflow using every arc type, concurrent actions, and timeout routing.
60
10
 
61
- SMT-based formal verification using Z3. Encodes the Petri net as an integer linear program and checks reachability properties.
11
+ ## Install
62
12
 
63
- Supported properties:
64
- - **Deadlock freedom** — no reachable state where all transitions are disabled
65
- - **Mutual exclusion** — two places never both hold tokens simultaneously
66
- - **Place bounds** — token count in a place never exceeds a limit
67
- - **Unreachability** — a marking is never reachable
68
-
69
- Also computes **P-invariants** (Farkas variant) and supports IC3/PDR-style incremental verification.
70
-
71
- ### ν-nets (`src/runtime/`)
13
+ ```bash
14
+ npm install libpetri
15
+ ```
72
16
 
73
- Correlated fork/join by identity. A `MatchSpec` declares a `value → NameId` key projection over a transition's input places; a fork mints a fresh opaque name via `ctx.freshName()` into the token payload, and a join consumes only the sibling tokens that project to the same name. The deterministic `(oldest-timestamp, then name)` tie-break is byte-identical across implementations (NU-022), and an incremental matcher keeps correlated-join drain at O(N log N). A bounded `Budget` place is the decidability lever for verification.
17
+ libpetri is ESM-only. The core runtime has no browser-only assumption; optional viewer and documentation entry points declare their own peer dependencies.
74
18
 
75
- ## Quick Start
19
+ ## Quick start
76
20
 
77
21
  ```typescript
78
- import { place, PetriNet, Transition, one, outPlace, tokenOf, BitmapNetExecutor } from 'libpetri';
22
+ import {
23
+ BitmapNetExecutor, PetriNet, Transition,
24
+ one, outPlace, place, tokenOf,
25
+ } from 'libpetri';
79
26
 
80
- // Define places
81
27
  const input = place<string>('input');
82
28
  const output = place<string>('output');
83
29
 
84
- // Define transition
85
- const process = Transition.builder('process')
30
+ const uppercase = Transition.builder('uppercase')
86
31
  .inputs(one(input))
87
32
  .outputs(outPlace(output))
88
33
  .action(async (ctx) => {
89
- const value = ctx.input(input);
90
- ctx.output(output, value.toUpperCase());
34
+ ctx.output(output, ctx.input(input).toUpperCase());
91
35
  })
92
36
  .build();
93
37
 
94
- // Build net
95
- const net = PetriNet.builder('Example').transition(process).build();
96
-
97
- // Execute
38
+ const net = PetriNet.builder('example').transition(uppercase).build();
98
39
  const executor = new BitmapNetExecutor(
99
40
  net,
100
41
  new Map([[input, [tokenOf('hello')]]]),
101
42
  );
102
- const marking = await executor.run();
103
- console.log(marking.peekTokens(output)); // [Token { value: 'HELLO' }]
104
- ```
105
43
 
106
- ## Modular composition
107
-
108
- Build large nets by reusing open-net fragments. A `SubnetDef` is a structurally
109
- complete `PetriNet` plus a typed `Interface` of **ports** (typed places) and
110
- **channels** (transitions). Instantiating renames every internal element with a
111
- `prefix/name`; composing into a host substitutes port places and merges
112
- channel transitions.
44
+ const result = await executor.run();
45
+ console.log(result.peekFirst(output)?.value); // HELLO
46
+ ```
113
47
 
114
- ```typescript
115
- import { PetriNet, SubnetDef, FusionSet, place } from 'libpetri';
116
-
117
- const items = place<string>('items');
118
- const slots = place<string>('slots');
119
- const put = place<string>('put');
120
- const get = place<string>('get');
121
-
122
- const buffer = SubnetDef.builder<void>('Buffer')
123
- .place(items).place(slots)
124
- .transition(enqueue).transition(dequeue)
125
- .inputPort('put', put)
126
- .outputPort('get', get)
127
- .build();
48
+ ## Execution and concurrency
128
49
 
129
- const b1 = buffer.instantiate('b1').bindActions({ enqueue: enqueueImpl, dequeue: fork() });
130
- const b2 = buffer.instantiate('b2').bindActions({ enqueue: enqueueImpl, dequeue: fork() });
50
+ `BitmapNetExecutor` is the reference implementation. `PrecompiledNetExecutor` compiles the same net into flat arrays, opcode streams, and priority-partitioned ready queues for production hot paths.
131
51
 
132
- const producerToB1 = place<string>('p1_to_b1');
133
- const b1ToB2 = place<string>('b1_to_b2');
134
- const b2ToConsumer = place<string>('b2_to_c');
52
+ The orchestrator owns the marking and invokes ready actions without awaiting earlier actions first. Promise continuations therefore overlap naturally, while marking updates remain serialized. CPU-heavy synchronous code still blocks the JavaScript event loop; move it to a worker or external service.
135
53
 
136
- const net = PetriNet.builder('Pipeline')
137
- .compose(b1, (bind) =>
138
- bind.bindPort('put', producerToB1).bindPort('get', b1ToB2))
139
- .compose(b2, (bind) =>
140
- bind.bindPort('put', b1ToB2).bindPort('get', b2ToConsumer))
141
- .fuse(FusionSet.of('sharedSlots', b1.port<string>('slots'), b2.port<string>('slots')))
142
- .build();
143
- ```
54
+ Use places and transitions for coordination rather than hiding concurrency inside `Promise.all`: the net can then visualize, trace, replay, and verify the fan-out and join.
144
55
 
145
- Composition produces a flat `PetriNet`. Ports merge places by structural
146
- rewrite; channels (`bindChannel`) merge transitions with arc union, timing
147
- intersection, and caller-wins identity. `FusionSet` declares N-ary place
148
- equivalence applied **after** all `compose(...)` calls, regardless of
149
- registration order. Per-instance action overrides are supplied via
150
- `Instance.bindActions({ originalName: action })`. See
151
- [`spec/11-modular-composition.md`](../spec/11-modular-composition.md).
56
+ ## Package entry points
152
57
 
153
- ## Verification Example
58
+ | Import | Purpose |
59
+ |---|---|
60
+ | `libpetri` | Core model, runtime, events, and composition |
61
+ | `libpetri/export` | DOT mapping and rendering |
62
+ | `libpetri/verification` | Structural analysis, state classes, and Z3-backed SMT verification |
63
+ | `libpetri/debug` | Debug protocol and session archives |
64
+ | `libpetri/viewer` | Interactive DOT/SVG viewer |
65
+ | `libpetri/doclet` | TypeDoc integration |
154
66
 
155
- ```typescript
156
- import { SmtVerifier, deadlockFree } from 'libpetri/verification';
67
+ The model supports input, output, read, inhibitor, and reset arcs; immediate, deadline, delayed, window, and exact timing; AND/XOR/timeout routing; environment places; reusable subnets; place fusion; and ν-net identity correlation.
157
68
 
158
- const result = await SmtVerifier.forNet(net)
159
- .initialMarking(m => m.tokens(input, 1))
160
- .property(deadlockFree())
161
- .verify();
69
+ ## Build and test
162
70
 
163
- console.log(result.verdict); // { type: 'proven', method: 'structural' }
71
+ ```bash
72
+ npm install
73
+ npm run build
74
+ npm run check
75
+ npm test
164
76
  ```
165
77
 
166
- ## Build & Test
78
+ ## Project links
167
79
 
168
- ```bash
169
- npm run build # Build with tsup
170
- npm run check # Type-check with tsc --noEmit
171
- npm test # Run tests with vitest
172
- ```
80
+ - [Language-agnostic specification](https://github.com/debe/libpetri/blob/main/spec/00-index.md)
81
+ - [Lean soundness and backend-refinement proofs](https://github.com/debe/libpetri/blob/main/lean/README.md)
82
+ - [Changelog](https://github.com/debe/libpetri/blob/main/CHANGELOG.md)
83
+ - [Apache License 2.0](https://github.com/debe/libpetri/blob/main/LICENSE)
@@ -3,6 +3,55 @@ import {
3
3
  latest
4
4
  } from "./chunk-ATT7U5H5.js";
5
5
 
6
+ // src/core/in.ts
7
+ function one(place) {
8
+ return { type: "one", place };
9
+ }
10
+ function exactly(count, place) {
11
+ if (count < 1) {
12
+ throw new Error(`count must be >= 1, got: ${count}`);
13
+ }
14
+ return { type: "exactly", place, count };
15
+ }
16
+ function all(place) {
17
+ return { type: "all", place };
18
+ }
19
+ function atLeast(minimum, place) {
20
+ if (minimum < 1) {
21
+ throw new Error(`minimum must be >= 1, got: ${minimum}`);
22
+ }
23
+ return { type: "at-least", place, minimum };
24
+ }
25
+ function requiredCount(spec) {
26
+ switch (spec.type) {
27
+ case "one":
28
+ return 1;
29
+ case "exactly":
30
+ return spec.count;
31
+ case "all":
32
+ return 1;
33
+ case "at-least":
34
+ return spec.minimum;
35
+ }
36
+ }
37
+ function consumptionCount(spec, available) {
38
+ if (available < requiredCount(spec)) {
39
+ throw new Error(
40
+ `Cannot consume from '${spec.place.name}': available=${available}, required=${requiredCount(spec)}`
41
+ );
42
+ }
43
+ switch (spec.type) {
44
+ case "one":
45
+ return 1;
46
+ case "exactly":
47
+ return spec.count;
48
+ case "all":
49
+ return available;
50
+ case "at-least":
51
+ return available;
52
+ }
53
+ }
54
+
6
55
  // src/core/out.ts
7
56
  function and(...children) {
8
57
  if (children.length === 0) {
@@ -97,6 +146,80 @@ function crossProduct(a, b) {
97
146
  return result;
98
147
  }
99
148
 
149
+ // src/core/transition-action.ts
150
+ function passthrough() {
151
+ return PASSTHROUGH;
152
+ }
153
+ var PASSTHROUGH = async () => {
154
+ };
155
+ function isPassthrough(action) {
156
+ return action === PASSTHROUGH;
157
+ }
158
+ function transform(fn) {
159
+ return async (ctx) => {
160
+ const result = fn(ctx);
161
+ for (const outputPlace of ctx.outputPlaces()) {
162
+ ctx.output(outputPlace, result);
163
+ }
164
+ };
165
+ }
166
+ function fork() {
167
+ return transform((ctx) => {
168
+ const inputPlaces = ctx.inputPlaces();
169
+ if (inputPlaces.size !== 1) {
170
+ throw new Error(`Fork requires exactly 1 input place, found ${inputPlaces.size}`);
171
+ }
172
+ const inputPlace = inputPlaces.values().next().value;
173
+ return ctx.input(inputPlace);
174
+ });
175
+ }
176
+ function transformFrom(inputPlace, fn) {
177
+ return transform((ctx) => fn(ctx.input(inputPlace)));
178
+ }
179
+ function transformAsync(fn) {
180
+ return async (ctx) => {
181
+ const result = await fn(ctx);
182
+ for (const outputPlace of ctx.outputPlaces()) {
183
+ ctx.output(outputPlace, result);
184
+ }
185
+ };
186
+ }
187
+ function produce(place, value) {
188
+ return async (ctx) => {
189
+ ctx.output(place, value);
190
+ };
191
+ }
192
+ function withTimeout(action, timeoutMs, timeoutPlace2, timeoutValue) {
193
+ return (ctx) => {
194
+ return new Promise((resolve, reject) => {
195
+ let completed = false;
196
+ const timer = setTimeout(() => {
197
+ if (!completed) {
198
+ completed = true;
199
+ ctx.output(timeoutPlace2, timeoutValue);
200
+ resolve();
201
+ }
202
+ }, timeoutMs);
203
+ action(ctx).then(
204
+ () => {
205
+ if (!completed) {
206
+ completed = true;
207
+ clearTimeout(timer);
208
+ resolve();
209
+ }
210
+ },
211
+ (err) => {
212
+ if (!completed) {
213
+ completed = true;
214
+ clearTimeout(timer);
215
+ reject(err);
216
+ }
217
+ }
218
+ );
219
+ });
220
+ };
221
+ }
222
+
100
223
  // src/verification/marking-state.ts
101
224
  var MARKING_STATE_KEY = /* @__PURE__ */ Symbol("MarkingState.internal");
102
225
  var MarkingState = class _MarkingState {
@@ -1400,6 +1523,17 @@ var StateClass = class {
1400
1523
  }
1401
1524
  };
1402
1525
 
1526
+ // src/core/internal/output-action-check.ts
1527
+ function requireOutputProducingActions(net) {
1528
+ for (const t of net.transitions) {
1529
+ if (t.outputSpec !== null && isPassthrough(t.action)) {
1530
+ throw new Error(
1531
+ `Transition '${t.name}' declares an output spec but carries passthrough(), which produces no tokens. Every firing would fail output validation (IO-015) and the declared output would never arrive. Bind an action that produces it \u2014 fork() moves the input token across \u2014 or drop the output spec if the transition is meant to be a sink.`
1532
+ );
1533
+ }
1534
+ }
1535
+ }
1536
+
1403
1537
  // src/verification/analysis/state-class-graph.ts
1404
1538
  var StateClassGraph = class _StateClassGraph {
1405
1539
  net;
@@ -1430,8 +1564,13 @@ var StateClassGraph = class _StateClassGraph {
1430
1564
  }
1431
1565
  }
1432
1566
  }
1433
- /** Builds the state class graph for a Time Petri Net. */
1567
+ /**
1568
+ * Builds the state class graph for a Time Petri Net.
1569
+ *
1570
+ * @throws Error if the net violates CORE-043 — analysis rejects the same nets execution rejects.
1571
+ */
1434
1572
  static build(net, initialMarking, maxClasses, environmentPlaces, environmentMode) {
1573
+ requireOutputProducingActions(net);
1435
1574
  const envMode = environmentMode ?? ignore();
1436
1575
  const envPlaces = /* @__PURE__ */ new Set();
1437
1576
  if (environmentPlaces) {
@@ -1644,18 +1783,8 @@ function inputRequiredCount(spec) {
1644
1783
  return spec.minimum;
1645
1784
  }
1646
1785
  }
1647
- function inputConsumeCount(spec) {
1648
- switch (spec.type) {
1649
- case "one":
1650
- return 1;
1651
- case "exactly":
1652
- return spec.count;
1653
- case "all":
1654
- return 1;
1655
- // Analysis: consume minimum (1 token)
1656
- case "at-least":
1657
- return spec.minimum;
1658
- }
1786
+ function inputConsumeCount(spec, available) {
1787
+ return consumptionCount(spec, available);
1659
1788
  }
1660
1789
  function checkPlaceEnabled(place, required, marking, environmentPlaces, environmentMode) {
1661
1790
  if (!environmentPlaces.has(place)) {
@@ -1673,7 +1802,11 @@ function checkPlaceEnabled(place, required, marking, environmentPlaces, environm
1673
1802
  function fireTransition(marking, transition, outputPlaces, environmentPlaces, environmentMode) {
1674
1803
  const builder = MarkingState.builder().copyFrom(marking);
1675
1804
  for (const spec of transition.inputSpecs) {
1676
- const toConsume = inputConsumeCount(spec);
1805
+ const available = marking.tokens(spec.place);
1806
+ if (available < inputRequiredCount(spec)) {
1807
+ continue;
1808
+ }
1809
+ const toConsume = inputConsumeCount(spec, available);
1677
1810
  consumeFromPlace(builder, spec.place, toConsume, environmentPlaces, environmentMode);
1678
1811
  }
1679
1812
  for (const arc of transition.resets) {
@@ -2298,11 +2431,11 @@ var NameMarking = class _NameMarking {
2298
2431
  return syms ? [...syms.keys()] : [];
2299
2432
  }
2300
2433
  liveSymbols() {
2301
- const all = /* @__PURE__ */ new Set();
2434
+ const all2 = /* @__PURE__ */ new Set();
2302
2435
  for (const syms of this.perPlace.values()) {
2303
- for (const s of syms.keys()) all.add(s);
2436
+ for (const s of syms.keys()) all2.add(s);
2304
2437
  }
2305
- return [...all];
2438
+ return [...all2];
2306
2439
  }
2307
2440
  /**
2308
2441
  * Symmetry-canonical key over `colouredOrder` (the finiteness mechanism). Two
@@ -2775,8 +2908,11 @@ var SmtVerifier = class _SmtVerifier {
2775
2908
  }
2776
2909
  /**
2777
2910
  * Runs the verification pipeline.
2911
+ *
2912
+ * @throws Error if the net violates CORE-043 — verification rejects the same nets execution rejects.
2778
2913
  */
2779
2914
  async verify() {
2915
+ requireOutputProducingActions(this.net);
2780
2916
  const start = performance.now();
2781
2917
  const report = [];
2782
2918
  report.push("=== IC3/PDR SAFETY VERIFICATION ===\n");
@@ -2982,7 +3118,7 @@ var SmtVerifier = class _SmtVerifier {
2982
3118
  report.push("=== RESULT ===\n");
2983
3119
  report.push(`PROVEN (IC3/PDR): ${propDesc}`);
2984
3120
  report.push(" Z3 Spacer proved no reachable state violates the property.");
2985
- report.push(" NOTE: Verification ignores timing constraints and JS guards.");
3121
+ report.push(" NOTE: Verification ignores timing constraints.");
2986
3122
  report.push(" An untimed proof is STRONGER than a timed one (timing only restricts behavior).");
2987
3123
  return this.applyNuGuard(buildResult(
2988
3124
  {
@@ -3015,7 +3151,6 @@ var SmtVerifier = class _SmtVerifier {
3015
3151
  }
3016
3152
  report.push("\n WARNING: This counterexample is in UNTIMED semantics.");
3017
3153
  report.push(" It may be spurious if timing constraints prevent this sequence.");
3018
- report.push(" JS guards are also ignored in this analysis.");
3019
3154
  return this.applyNuGuard(buildResult(
3020
3155
  { type: "violated" },
3021
3156
  report.join("\n"),
@@ -3159,6 +3294,12 @@ function isViolated(result) {
3159
3294
  }
3160
3295
 
3161
3296
  export {
3297
+ one,
3298
+ exactly,
3299
+ all,
3300
+ atLeast,
3301
+ requiredCount,
3302
+ consumptionCount,
3162
3303
  and,
3163
3304
  andPlaces,
3164
3305
  xor,
@@ -3169,6 +3310,14 @@ export {
3169
3310
  forwardInput,
3170
3311
  allPlaces,
3171
3312
  enumerateBranches,
3313
+ passthrough,
3314
+ isPassthrough,
3315
+ transform,
3316
+ fork,
3317
+ transformFrom,
3318
+ transformAsync,
3319
+ produce,
3320
+ withTimeout,
3172
3321
  MarkingState,
3173
3322
  MarkingStateBuilder,
3174
3323
  deadlockFree,
@@ -3199,10 +3348,11 @@ export {
3199
3348
  encode,
3200
3349
  DBM,
3201
3350
  StateClass,
3351
+ requireOutputProducingActions,
3202
3352
  StateClassGraph,
3203
3353
  decode,
3204
3354
  SmtVerifier,
3205
3355
  isProven,
3206
3356
  isViolated
3207
3357
  };
3208
- //# sourceMappingURL=chunk-V3WTQRHC.js.map
3358
+ //# sourceMappingURL=chunk-5W6SVYPD.js.map