libpetri 5.0.0 → 6.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/dist/{chunk-4HMFNYTH.js → chunk-5LE2M5PW.js} +2 -2
- package/dist/{chunk-SXK2Z45Z.js → chunk-H2KAMPGN.js} +1 -1
- package/dist/chunk-H2KAMPGN.js.map +1 -0
- package/dist/chunk-MQZ6IM63.js +8383 -0
- package/dist/chunk-MQZ6IM63.js.map +1 -0
- package/dist/debug/index.d.ts +2 -2
- package/dist/debug/index.js +1 -1
- package/dist/doclet/index.d.ts +1 -1
- package/dist/doclet/resources/petrinet-diagrams.js +4892 -4896
- package/dist/{event-store-hlH3-bzJ.d.ts → event-store-D6i4u41W.d.ts} +7 -1
- package/dist/export/index.d.ts +1 -1
- package/dist/index.d.ts +62 -10
- package/dist/index.js +359 -2156
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-CH7UvjOW.d.ts → petri-net-34SkD5RT.d.ts} +85 -3
- package/dist/render-dom/index.js +1 -1
- package/dist/verification/index.d.ts +843 -16
- package/dist/verification/index.js +755 -1
- package/dist/verification/index.js.map +1 -1
- package/dist/viewer/index.js +1 -1
- package/dist/viewer/viewer.iife.js +4892 -4896
- package/package.json +5 -5
- package/dist/chunk-75KEJQGC.js +0 -4505
- package/dist/chunk-75KEJQGC.js.map +0 -1
- package/dist/chunk-SXK2Z45Z.js.map +0 -1
- /package/dist/{chunk-4HMFNYTH.js.map → chunk-5LE2M5PW.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
ComposeBindings,
|
|
3
|
+
FusionSet,
|
|
4
|
+
FusionSetBuilder,
|
|
5
|
+
Instance,
|
|
6
|
+
Interface,
|
|
7
|
+
InterfaceBuilder,
|
|
8
|
+
PetriNet,
|
|
9
|
+
PetriNetBuilder,
|
|
10
|
+
SubnetDef,
|
|
11
|
+
SubnetDefBuilder,
|
|
12
|
+
Transition,
|
|
13
|
+
TransitionBuilder,
|
|
3
14
|
all,
|
|
4
15
|
allPlaces,
|
|
5
|
-
alwaysAvailable,
|
|
6
16
|
and,
|
|
7
17
|
andPlaces,
|
|
8
18
|
atLeast,
|
|
9
19
|
consumptionCount,
|
|
10
20
|
enumerateBranches,
|
|
21
|
+
environmentPlace,
|
|
11
22
|
exactly,
|
|
12
23
|
fork,
|
|
13
24
|
forwardInput,
|
|
14
25
|
isPassthrough,
|
|
15
|
-
isProven,
|
|
16
|
-
isViolated,
|
|
17
26
|
one,
|
|
18
27
|
outPlace,
|
|
19
28
|
passthrough,
|
|
29
|
+
place,
|
|
20
30
|
produce,
|
|
21
31
|
requireOutputProducingActions,
|
|
22
32
|
requiredCount,
|
|
@@ -28,11 +38,11 @@ import {
|
|
|
28
38
|
withTimeout,
|
|
29
39
|
xor,
|
|
30
40
|
xorPlaces
|
|
31
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-MQZ6IM63.js";
|
|
32
42
|
import {
|
|
33
43
|
eventTransitionName,
|
|
34
44
|
isFailureEvent
|
|
35
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-H2KAMPGN.js";
|
|
36
46
|
import {
|
|
37
47
|
keyForPlace,
|
|
38
48
|
matchCorrelates,
|
|
@@ -69,14 +79,6 @@ function isUnit(token) {
|
|
|
69
79
|
return token === UNIT_TOKEN;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
// src/core/place.ts
|
|
73
|
-
function place(name) {
|
|
74
|
-
return { name };
|
|
75
|
-
}
|
|
76
|
-
function environmentPlace(name) {
|
|
77
|
-
return { place: place(name) };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
82
|
// src/core/arc.ts
|
|
81
83
|
function inputArc(place2) {
|
|
82
84
|
return { type: "input", place: place2 };
|
|
@@ -235,2146 +237,211 @@ var TransitionContext = class {
|
|
|
235
237
|
this.requireInput(actual);
|
|
236
238
|
return this.rawInput.get(actual);
|
|
237
239
|
}
|
|
238
|
-
/** Returns declared input places (consumed). */
|
|
239
|
-
inputPlaces() {
|
|
240
|
-
return this._inputPlaces;
|
|
241
|
-
}
|
|
242
|
-
requireInput(place2) {
|
|
243
|
-
if (!this.allowedInputs.has(place2.name)) {
|
|
244
|
-
throw new Error(
|
|
245
|
-
`Place '${place2.name}' not in declared inputs: [${[...this.allowedInputs].join(", ")}]`
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// ==================== Read Access (not consumed) ====================
|
|
250
|
-
/** Get read-only context value. Throws if place not declared as read. */
|
|
251
|
-
read(place2) {
|
|
252
|
-
const actual = this.resolve(place2);
|
|
253
|
-
this.requireRead(actual);
|
|
254
|
-
return this.rawInput.value(actual);
|
|
255
|
-
}
|
|
256
|
-
/** Get all read-only context values for a place. */
|
|
257
|
-
reads(place2) {
|
|
258
|
-
const actual = this.resolve(place2);
|
|
259
|
-
this.requireRead(actual);
|
|
260
|
-
return this.rawInput.values(actual);
|
|
261
|
-
}
|
|
262
|
-
/** Returns declared read places (context, not consumed). */
|
|
263
|
-
readPlaces() {
|
|
264
|
-
return this._readPlaces;
|
|
265
|
-
}
|
|
266
|
-
requireRead(place2) {
|
|
267
|
-
if (!this.allowedReads.has(place2.name)) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`Place '${place2.name}' not in declared reads: [${[...this.allowedReads].join(", ")}]`
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
// ==================== Output Access ====================
|
|
274
|
-
/**
|
|
275
|
-
* Add one or more output values to the same place in a single call.
|
|
276
|
-
*
|
|
277
|
-
* Validates the place once, then appends each value to the output
|
|
278
|
-
* collector. Calling with zero values is a no-op.
|
|
279
|
-
*
|
|
280
|
-
* @example
|
|
281
|
-
* ctx.output(outPlace, 'a', 'b', 'c');
|
|
282
|
-
* ctx.output(outPlace, ...someArray);
|
|
283
|
-
*
|
|
284
|
-
* @throws if place not declared as output.
|
|
285
|
-
*/
|
|
286
|
-
output(place2, ...values) {
|
|
287
|
-
const actual = this.resolve(place2);
|
|
288
|
-
this.requireOutput(actual);
|
|
289
|
-
for (const value of values) {
|
|
290
|
-
this.writeTarget.add(actual, value);
|
|
291
|
-
}
|
|
292
|
-
return this;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Add one or more pre-built output tokens to the same place in a single call.
|
|
296
|
-
*
|
|
297
|
-
* Validates the place once, then appends each token. Calling with zero
|
|
298
|
-
* tokens is a no-op.
|
|
299
|
-
*
|
|
300
|
-
* @throws if place not declared as output.
|
|
301
|
-
*/
|
|
302
|
-
outputToken(place2, ...tokens) {
|
|
303
|
-
const actual = this.resolve(place2);
|
|
304
|
-
this.requireOutput(actual);
|
|
305
|
-
for (const token of tokens) {
|
|
306
|
-
this.writeTarget.addToken(actual, token);
|
|
307
|
-
}
|
|
308
|
-
return this;
|
|
309
|
-
}
|
|
310
|
-
/** Returns declared output places. */
|
|
311
|
-
outputPlaces() {
|
|
312
|
-
return this._outputPlaces;
|
|
313
|
-
}
|
|
314
|
-
requireOutput(place2) {
|
|
315
|
-
if (!this.allowedOutputs.has(place2.name)) {
|
|
316
|
-
throw new Error(
|
|
317
|
-
`Place '${place2.name}' not in declared outputs: [${[...this.allowedOutputs].join(", ")}]`
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Severs the action's output from the marking, for use when a firing times out.
|
|
323
|
-
*
|
|
324
|
-
* After this call the action's `output(...)` writes are dropped, and the executor
|
|
325
|
-
* harvests a fresh collector the action holds no reference to. Anything the action
|
|
326
|
-
* wrote *before* the timeout is discarded with it: a partial result merged with the
|
|
327
|
-
* timeout branch would violate the transition's own output spec.
|
|
328
|
-
*
|
|
329
|
-
* This is the only isolation available — libpetri does not own the promise the action
|
|
330
|
-
* runs on, so it cannot stop the work, only stop the result from landing.
|
|
331
|
-
*
|
|
332
|
-
* @internal Executor machinery — must be called by the executor, never by an action.
|
|
333
|
-
*/
|
|
334
|
-
detachForTimeout() {
|
|
335
|
-
this.writeTarget.detach();
|
|
336
|
-
this._rawOutput = new TokenOutput();
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Produces into the executor's harvest collector rather than the action's write target.
|
|
340
|
-
*
|
|
341
|
-
* Identical to {@link output} before {@link detachForTimeout}; after it, this is the
|
|
342
|
-
* only route that still reaches the marking. Used by the executor to deposit the
|
|
343
|
-
* timeout branch.
|
|
344
|
-
*
|
|
345
|
-
* @internal Executor machinery — must be called by the executor, never by an action.
|
|
346
|
-
*/
|
|
347
|
-
outputToHarvest(place2, value) {
|
|
348
|
-
const actual = this.resolve(place2);
|
|
349
|
-
this.requireOutput(actual);
|
|
350
|
-
this._rawOutput.add(actual, value);
|
|
351
|
-
return this;
|
|
352
|
-
}
|
|
353
|
-
// ==================== ν-name minting (NU-010) ====================
|
|
354
|
-
/**
|
|
355
|
-
* @internal Installs the ν-name minter. Wired by the executor at firing time
|
|
356
|
-
* so names minted by {@link freshName} are monotonic across the run and
|
|
357
|
-
* instance-prefixed (spec NU-010, NU-030).
|
|
358
|
-
*/
|
|
359
|
-
setFreshNameSupplier(supplier) {
|
|
360
|
-
this._freshNameSupplier = supplier;
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Mints a fresh ν-name (the ν-binder primitive — spec NU-010).
|
|
364
|
-
*
|
|
365
|
-
* An action calls this on the fork side to create a correlation id, then
|
|
366
|
-
* writes it into the sibling output payloads; a later join correlates those
|
|
367
|
-
* siblings via a {@link import('./match-spec.js').MatchSpec}. Uses the
|
|
368
|
-
* executor-installed minter when present; otherwise falls back to a
|
|
369
|
-
* process-global counter prefixed by the transition name.
|
|
370
|
-
*/
|
|
371
|
-
freshName() {
|
|
372
|
-
if (this._freshNameSupplier) return this._freshNameSupplier();
|
|
373
|
-
return nameId(`${this._transitionName}#${GLOBAL_FRESH_NAME_COUNTER++}`);
|
|
374
|
-
}
|
|
375
|
-
// ==================== Structure Info ====================
|
|
376
|
-
/** Returns the transition name. */
|
|
377
|
-
transitionName() {
|
|
378
|
-
return this._transitionName;
|
|
379
|
-
}
|
|
380
|
-
// ==================== Execution Context ====================
|
|
381
|
-
/** Retrieves an execution context object by key. */
|
|
382
|
-
executionContext(key) {
|
|
383
|
-
return this.executionCtx.get(key);
|
|
384
|
-
}
|
|
385
|
-
/** Checks if an execution context object of the given key is present. */
|
|
386
|
-
hasExecutionContext(key) {
|
|
387
|
-
return this.executionCtx.has(key);
|
|
388
|
-
}
|
|
389
|
-
// ==================== Logging ====================
|
|
390
|
-
/** Emits a structured log message into the event store. */
|
|
391
|
-
log(level, message, error) {
|
|
392
|
-
this._logFn?.(level, message, error);
|
|
393
|
-
}
|
|
394
|
-
// ==================== Internal ====================
|
|
395
|
-
/** @internal Used by BitmapNetExecutor to collect outputs after action completion. */
|
|
396
|
-
rawOutput() {
|
|
397
|
-
return this._rawOutput;
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
// src/core/token-input.ts
|
|
402
|
-
var TokenInput = class {
|
|
403
|
-
tokens = /* @__PURE__ */ new Map();
|
|
404
|
-
/** Add a token (used by executor when firing transition). */
|
|
405
|
-
add(place2, token) {
|
|
406
|
-
const existing = this.tokens.get(place2.name);
|
|
407
|
-
if (existing) {
|
|
408
|
-
existing.push(token);
|
|
409
|
-
} else {
|
|
410
|
-
this.tokens.set(place2.name, [token]);
|
|
411
|
-
}
|
|
412
|
-
return this;
|
|
413
|
-
}
|
|
414
|
-
/** Get all tokens for a place. */
|
|
415
|
-
getAll(place2) {
|
|
416
|
-
return this.tokens.get(place2.name) ?? [];
|
|
417
|
-
}
|
|
418
|
-
/** Get the first token for a place. Throws if no tokens. */
|
|
419
|
-
get(place2) {
|
|
420
|
-
const list = this.tokens.get(place2.name);
|
|
421
|
-
if (!list || list.length === 0) {
|
|
422
|
-
throw new Error(`No token for place: ${place2.name}`);
|
|
423
|
-
}
|
|
424
|
-
return list[0];
|
|
425
|
-
}
|
|
426
|
-
/** Get the first token's value for a place. Throws if no tokens. */
|
|
427
|
-
value(place2) {
|
|
428
|
-
return this.get(place2).value;
|
|
429
|
-
}
|
|
430
|
-
/** Get all token values for a place. */
|
|
431
|
-
values(place2) {
|
|
432
|
-
return this.getAll(place2).map((t) => t.value);
|
|
433
|
-
}
|
|
434
|
-
/** Get token count for a place. */
|
|
435
|
-
count(place2) {
|
|
436
|
-
return this.getAll(place2).length;
|
|
437
|
-
}
|
|
438
|
-
/** Check if any tokens exist for a place. */
|
|
439
|
-
has(place2) {
|
|
440
|
-
return this.count(place2) > 0;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
// src/core/transition.ts
|
|
445
|
-
var TRANSITION_KEY = /* @__PURE__ */ Symbol("Transition.internal");
|
|
446
|
-
var EMPTY_PLACE_ALIAS = /* @__PURE__ */ new Map();
|
|
447
|
-
var Transition = class {
|
|
448
|
-
name;
|
|
449
|
-
inputSpecs;
|
|
450
|
-
outputSpec;
|
|
451
|
-
inhibitors;
|
|
452
|
-
reads;
|
|
453
|
-
resets;
|
|
454
|
-
timing;
|
|
455
|
-
actionTimeout;
|
|
456
|
-
action;
|
|
457
|
-
priority;
|
|
458
|
-
/**
|
|
459
|
-
* ν-net join correlation: a subset of `inputSpecs` that must be correlated by
|
|
460
|
-
* name equality on firing (spec NU-020). `null` for ordinary transitions.
|
|
461
|
-
*/
|
|
462
|
-
matchSpec;
|
|
463
|
-
/**
|
|
464
|
-
* Per-transition **declared → actual** place correspondence (per
|
|
465
|
-
* **MOD-031**), keyed by the author-original declared place **name** →
|
|
466
|
-
* actual composed place. Empty for a hand-written or directly-composed
|
|
467
|
-
* ([MOD-025]) transition (identity). Populated by the subnet rewriter after
|
|
468
|
-
* instantiation ([MOD-010]) / port binding ([MOD-020]) so an action that
|
|
469
|
-
* hardcodes a declared place constant resolves to the composed place via
|
|
470
|
-
* {@link import('./transition-context.js').TransitionContext}. Consumed only
|
|
471
|
-
* by the action-facing context I/O — never by enablement, firing, the
|
|
472
|
-
* verifier, the exporter, or events (so [MOD-023] is unaffected).
|
|
473
|
-
*/
|
|
474
|
-
placeAlias;
|
|
475
|
-
_inputPlaces;
|
|
476
|
-
_readPlaces;
|
|
477
|
-
_outputPlaces;
|
|
478
|
-
/** @internal Use {@link Transition.builder} to create instances. */
|
|
479
|
-
constructor(key, name, inputSpecs, outputSpec, inhibitors, reads, resets, timing, action, priority, placeAlias = EMPTY_PLACE_ALIAS, matchSpec2 = null) {
|
|
480
|
-
if (key !== TRANSITION_KEY) throw new Error("Use Transition.builder() to create instances");
|
|
481
|
-
this.name = name;
|
|
482
|
-
this.inputSpecs = inputSpecs;
|
|
483
|
-
this.outputSpec = outputSpec;
|
|
484
|
-
this.inhibitors = inhibitors;
|
|
485
|
-
this.reads = reads;
|
|
486
|
-
this.resets = resets;
|
|
487
|
-
this.timing = timing;
|
|
488
|
-
this.actionTimeout = findTimeout(outputSpec);
|
|
489
|
-
this.action = action;
|
|
490
|
-
this.priority = priority;
|
|
491
|
-
this.placeAlias = placeAlias.size === 0 ? EMPTY_PLACE_ALIAS : placeAlias;
|
|
492
|
-
this.matchSpec = matchSpec2;
|
|
493
|
-
const inputPlaces = /* @__PURE__ */ new Set();
|
|
494
|
-
for (const spec of inputSpecs) {
|
|
495
|
-
inputPlaces.add(spec.place);
|
|
496
|
-
}
|
|
497
|
-
this._inputPlaces = inputPlaces;
|
|
498
|
-
const readPlaces = /* @__PURE__ */ new Set();
|
|
499
|
-
for (const r of reads) {
|
|
500
|
-
readPlaces.add(r.place);
|
|
501
|
-
}
|
|
502
|
-
this._readPlaces = readPlaces;
|
|
503
|
-
const outputPlaces = /* @__PURE__ */ new Set();
|
|
504
|
-
if (outputSpec !== null) {
|
|
505
|
-
for (const p of allPlaces(outputSpec)) {
|
|
506
|
-
outputPlaces.add(p);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
this._outputPlaces = outputPlaces;
|
|
510
|
-
}
|
|
511
|
-
/** Returns set of input places — consumed tokens. */
|
|
512
|
-
inputPlaces() {
|
|
513
|
-
return this._inputPlaces;
|
|
514
|
-
}
|
|
515
|
-
/** Returns set of read places — context tokens, not consumed. */
|
|
516
|
-
readPlaces() {
|
|
517
|
-
return this._readPlaces;
|
|
518
|
-
}
|
|
519
|
-
/** Returns set of output places — where tokens are produced. */
|
|
520
|
-
outputPlaces() {
|
|
521
|
-
return this._outputPlaces;
|
|
522
|
-
}
|
|
523
|
-
/** Returns true if this transition has an action timeout. */
|
|
524
|
-
hasActionTimeout() {
|
|
525
|
-
return this.actionTimeout !== null;
|
|
526
|
-
}
|
|
527
|
-
toString() {
|
|
528
|
-
return `Transition[${this.name}]`;
|
|
529
|
-
}
|
|
530
|
-
static builder(name) {
|
|
531
|
-
return new TransitionBuilder(name);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
var TransitionBuilder = class {
|
|
535
|
-
_name;
|
|
536
|
-
_inputSpecs = [];
|
|
537
|
-
_outputSpec = null;
|
|
538
|
-
_inhibitors = [];
|
|
539
|
-
_reads = [];
|
|
540
|
-
_resets = [];
|
|
541
|
-
_timing = immediate();
|
|
542
|
-
_action = passthrough();
|
|
543
|
-
_priority = 0;
|
|
544
|
-
_placeAlias = EMPTY_PLACE_ALIAS;
|
|
545
|
-
_matchSpec = null;
|
|
546
|
-
constructor(name) {
|
|
547
|
-
this._name = name;
|
|
548
|
-
}
|
|
549
|
-
/** Add input specifications with cardinality. */
|
|
550
|
-
inputs(...specs) {
|
|
551
|
-
this._inputSpecs.push(...specs);
|
|
552
|
-
return this;
|
|
553
|
-
}
|
|
554
|
-
/** Set the output specification (composite AND/XOR structure). */
|
|
555
|
-
outputs(spec) {
|
|
556
|
-
this._outputSpec = spec;
|
|
557
|
-
return this;
|
|
558
|
-
}
|
|
559
|
-
/** Add inhibitor arc. */
|
|
560
|
-
inhibitor(place2) {
|
|
561
|
-
this._inhibitors.push({ type: "inhibitor", place: place2 });
|
|
562
|
-
return this;
|
|
563
|
-
}
|
|
564
|
-
/** Add inhibitor arcs. */
|
|
565
|
-
inhibitors(...places) {
|
|
566
|
-
for (const p of places) {
|
|
567
|
-
this._inhibitors.push({ type: "inhibitor", place: p });
|
|
568
|
-
}
|
|
569
|
-
return this;
|
|
570
|
-
}
|
|
571
|
-
/** Add read arc. */
|
|
572
|
-
read(place2) {
|
|
573
|
-
this._reads.push({ type: "read", place: place2 });
|
|
574
|
-
return this;
|
|
575
|
-
}
|
|
576
|
-
/** Add read arcs. */
|
|
577
|
-
reads(...places) {
|
|
578
|
-
for (const p of places) {
|
|
579
|
-
this._reads.push({ type: "read", place: p });
|
|
580
|
-
}
|
|
581
|
-
return this;
|
|
582
|
-
}
|
|
583
|
-
/** Add reset arc. */
|
|
584
|
-
reset(place2) {
|
|
585
|
-
this._resets.push({ type: "reset", place: place2 });
|
|
586
|
-
return this;
|
|
587
|
-
}
|
|
588
|
-
/** Add reset arcs. */
|
|
589
|
-
resets(...places) {
|
|
590
|
-
for (const p of places) {
|
|
591
|
-
this._resets.push({ type: "reset", place: p });
|
|
592
|
-
}
|
|
593
|
-
return this;
|
|
594
|
-
}
|
|
595
|
-
/** Set timing specification. */
|
|
596
|
-
timing(timing) {
|
|
597
|
-
this._timing = timing;
|
|
598
|
-
return this;
|
|
599
|
-
}
|
|
600
|
-
/** Set the transition action. */
|
|
601
|
-
action(action) {
|
|
602
|
-
this._action = action;
|
|
603
|
-
return this;
|
|
604
|
-
}
|
|
605
|
-
/** Set the priority (higher fires first). */
|
|
606
|
-
priority(priority) {
|
|
607
|
-
this._priority = priority;
|
|
608
|
-
return this;
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Sets the ν-net join correlation spec: the named input places must be
|
|
612
|
-
* correlated by name equality on firing (spec NU-020). Every place referenced
|
|
613
|
-
* by the spec must also be declared as an input.
|
|
614
|
-
*/
|
|
615
|
-
match(spec) {
|
|
616
|
-
this._matchSpec = spec;
|
|
617
|
-
return this;
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Sets the per-transition declared→actual place correspondence (per
|
|
621
|
-
* **MOD-031**). Populated by the subnet rewriter during the compose-time
|
|
622
|
-
* rewrite; not normally called by hand-written nets, whose correspondence is
|
|
623
|
-
* the identity (empty map).
|
|
624
|
-
*/
|
|
625
|
-
placeAlias(alias) {
|
|
626
|
-
this._placeAlias = alias;
|
|
627
|
-
return this;
|
|
628
|
-
}
|
|
629
|
-
build() {
|
|
630
|
-
if (this._outputSpec !== null) {
|
|
631
|
-
const inputPlaceNames = new Set(this._inputSpecs.map((s) => s.place.name));
|
|
632
|
-
for (const fi of findForwardInputs(this._outputSpec)) {
|
|
633
|
-
if (!inputPlaceNames.has(fi.from.name)) {
|
|
634
|
-
throw new Error(
|
|
635
|
-
`Transition '${this._name}': ForwardInput references non-input place '${fi.from.name}'`
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (this._matchSpec !== null) {
|
|
641
|
-
const inputPlaceNames = new Set(this._inputSpecs.map((s) => s.place.name));
|
|
642
|
-
for (const k of this._matchSpec.keys) {
|
|
643
|
-
if (!inputPlaceNames.has(k.place.name)) {
|
|
644
|
-
throw new Error(
|
|
645
|
-
`Transition '${this._name}': MatchSpec correlates non-input place '${k.place.name}'`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
return new Transition(
|
|
651
|
-
TRANSITION_KEY,
|
|
652
|
-
this._name,
|
|
653
|
-
[...this._inputSpecs],
|
|
654
|
-
this._outputSpec,
|
|
655
|
-
[...this._inhibitors],
|
|
656
|
-
[...this._reads],
|
|
657
|
-
[...this._resets],
|
|
658
|
-
this._timing,
|
|
659
|
-
this._action,
|
|
660
|
-
this._priority,
|
|
661
|
-
this._placeAlias,
|
|
662
|
-
this._matchSpec
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
function findTimeout(out) {
|
|
667
|
-
if (out === null) return null;
|
|
668
|
-
switch (out.type) {
|
|
669
|
-
case "timeout":
|
|
670
|
-
return out;
|
|
671
|
-
case "and":
|
|
672
|
-
case "xor":
|
|
673
|
-
for (const child of out.children) {
|
|
674
|
-
const found = findTimeout(child);
|
|
675
|
-
if (found !== null) return found;
|
|
676
|
-
}
|
|
677
|
-
return null;
|
|
678
|
-
case "place":
|
|
679
|
-
case "forward-input":
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function findForwardInputs(out) {
|
|
684
|
-
switch (out.type) {
|
|
685
|
-
case "forward-input":
|
|
686
|
-
return [{ from: out.from, to: out.to }];
|
|
687
|
-
case "and":
|
|
688
|
-
case "xor":
|
|
689
|
-
return out.children.flatMap(findForwardInputs);
|
|
690
|
-
case "timeout":
|
|
691
|
-
return findForwardInputs(out.child);
|
|
692
|
-
case "place":
|
|
693
|
-
return [];
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// src/core/interface.ts
|
|
698
|
-
var INTERFACE_KEY = /* @__PURE__ */ Symbol("Interface.internal");
|
|
699
|
-
var Interface = class {
|
|
700
|
-
ports;
|
|
701
|
-
channels;
|
|
702
|
-
/** @internal Use {@link Interface.builder} to create instances. */
|
|
703
|
-
constructor(key, ports, channels) {
|
|
704
|
-
if (key !== INTERFACE_KEY) throw new Error("Use Interface.builder() to create instances");
|
|
705
|
-
this.ports = ports;
|
|
706
|
-
this.channels = channels;
|
|
707
|
-
}
|
|
708
|
-
/** Looks up a port by name. Returns undefined when absent. */
|
|
709
|
-
port(name) {
|
|
710
|
-
return this.ports.get(name);
|
|
711
|
-
}
|
|
712
|
-
/** Looks up a channel by name. Returns undefined when absent. */
|
|
713
|
-
channel(name) {
|
|
714
|
-
return this.channels.get(name);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Looks up a port by name and returns its underlying place, narrowed to
|
|
718
|
-
* `Place<T>`. Returns undefined when the port does not exist.
|
|
719
|
-
*
|
|
720
|
-
* Note: TypeScript erases generics at runtime, so unlike Java's
|
|
721
|
-
* `portPlaceAs(name, Class<T>)` this method cannot verify the token type at
|
|
722
|
-
* runtime — the caller is trusted to supply the correct `T`. Per **MOD-022**,
|
|
723
|
-
* type compatibility is enforced at compile time only in TypeScript.
|
|
724
|
-
*/
|
|
725
|
-
placeAs(name) {
|
|
726
|
-
const p = this.ports.get(name);
|
|
727
|
-
if (p === void 0) return void 0;
|
|
728
|
-
return p.place;
|
|
729
|
-
}
|
|
730
|
-
static builder() {
|
|
731
|
-
return new InterfaceBuilder();
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
var InterfaceBuilder = class {
|
|
735
|
-
_ports = /* @__PURE__ */ new Map();
|
|
736
|
-
_channels = /* @__PURE__ */ new Map();
|
|
737
|
-
/** Add a pre-built port (rejects duplicate names). */
|
|
738
|
-
port(port) {
|
|
739
|
-
if (this._ports.has(port.name)) {
|
|
740
|
-
throw new Error(`Duplicate port name: '${port.name}'`);
|
|
741
|
-
}
|
|
742
|
-
this._ports.set(port.name, port);
|
|
743
|
-
return this;
|
|
744
|
-
}
|
|
745
|
-
/** Add an input port (advisory direction). */
|
|
746
|
-
inputPort(name, place2) {
|
|
747
|
-
return this.port({ name, direction: "input", place: place2 });
|
|
748
|
-
}
|
|
749
|
-
/** Add an output port (advisory direction). */
|
|
750
|
-
outputPort(name, place2) {
|
|
751
|
-
return this.port({ name, direction: "output", place: place2 });
|
|
752
|
-
}
|
|
753
|
-
/** Add an in-out port (advisory direction). */
|
|
754
|
-
inoutPort(name, place2) {
|
|
755
|
-
return this.port({ name, direction: "inout", place: place2 });
|
|
756
|
-
}
|
|
757
|
-
channel(channelOrName, transition) {
|
|
758
|
-
const ch = typeof channelOrName === "string" ? { name: channelOrName, transition } : channelOrName;
|
|
759
|
-
if (this._channels.has(ch.name)) {
|
|
760
|
-
throw new Error(`Duplicate channel name: '${ch.name}'`);
|
|
761
|
-
}
|
|
762
|
-
this._channels.set(ch.name, ch);
|
|
763
|
-
return this;
|
|
764
|
-
}
|
|
765
|
-
/** @internal Bulk-add ports already validated by the caller. */
|
|
766
|
-
portsAll(ports) {
|
|
767
|
-
for (const p of ports) this.port(p);
|
|
768
|
-
return this;
|
|
769
|
-
}
|
|
770
|
-
/** @internal Bulk-add channels already validated by the caller. */
|
|
771
|
-
channelsAll(channels) {
|
|
772
|
-
for (const c of channels) this.channel(c);
|
|
773
|
-
return this;
|
|
774
|
-
}
|
|
775
|
-
build() {
|
|
776
|
-
return new Interface(
|
|
777
|
-
INTERFACE_KEY,
|
|
778
|
-
new Map(this._ports),
|
|
779
|
-
new Map(this._channels)
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
// src/core/instance.ts
|
|
785
|
-
var INSTANCE_KEY = /* @__PURE__ */ Symbol("Instance.internal");
|
|
786
|
-
var Instance = class {
|
|
787
|
-
prefix;
|
|
788
|
-
def;
|
|
789
|
-
renamedBody;
|
|
790
|
-
portHandles;
|
|
791
|
-
channelHandles;
|
|
792
|
-
params;
|
|
793
|
-
/**
|
|
794
|
-
* @internal Use {@link SubnetDef.instantiate} (or the internal factory
|
|
795
|
-
* {@link __createInstance}) to create instances.
|
|
796
|
-
*/
|
|
797
|
-
constructor(key, prefix, def, renamedBody, portHandles, channelHandles, params) {
|
|
798
|
-
if (key !== INSTANCE_KEY) {
|
|
799
|
-
throw new Error("Use SubnetDef.instantiate() to create Instance values");
|
|
800
|
-
}
|
|
801
|
-
this.prefix = prefix;
|
|
802
|
-
this.def = def;
|
|
803
|
-
this.renamedBody = renamedBody;
|
|
804
|
-
this.portHandles = portHandles;
|
|
805
|
-
this.channelHandles = channelHandles;
|
|
806
|
-
this.params = params;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Returns the renamed {@link Place} corresponding to the named port.
|
|
810
|
-
*
|
|
811
|
-
* The port name is the **original** (pre-prefix) name as declared in the
|
|
812
|
-
* subnet's `Interface`. Per **MOD-022**, TypeScript enforces token-type
|
|
813
|
-
* compatibility at compile time only; this method does not validate `T`
|
|
814
|
-
* at runtime. A missing name raises an `Error`.
|
|
815
|
-
*
|
|
816
|
-
* @throws when the port name is unknown
|
|
817
|
-
*/
|
|
818
|
-
port(name) {
|
|
819
|
-
const p = this.portHandles.get(name);
|
|
820
|
-
if (p === void 0) {
|
|
821
|
-
throw new Error(`No port named '${name}' in instance '${this.prefix}'`);
|
|
822
|
-
}
|
|
823
|
-
return p;
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Returns the renamed {@link Transition} corresponding to the named channel.
|
|
827
|
-
*
|
|
828
|
-
* @throws when the channel name is unknown
|
|
829
|
-
*/
|
|
830
|
-
channel(name) {
|
|
831
|
-
const t = this.channelHandles.get(name);
|
|
832
|
-
if (t === void 0) {
|
|
833
|
-
throw new Error(`No channel named '${name}' in instance '${this.prefix}'`);
|
|
834
|
-
}
|
|
835
|
-
return t;
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Returns the debug-UI descriptor for this instance per **MOD-041**.
|
|
839
|
-
*/
|
|
840
|
-
descriptor() {
|
|
841
|
-
const transitions = [];
|
|
842
|
-
for (const t of this.renamedBody.transitions) transitions.push(t.name);
|
|
843
|
-
const exposedPlaces = [];
|
|
844
|
-
for (const p of this.portHandles.values()) exposedPlaces.push(p.name);
|
|
845
|
-
return {
|
|
846
|
-
prefix: this.prefix,
|
|
847
|
-
defName: this.def.name,
|
|
848
|
-
transitions,
|
|
849
|
-
exposedPlaces,
|
|
850
|
-
params: this.params,
|
|
851
|
-
parentPrefix: null
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Produces a derived instance whose specified transitions (named by their
|
|
856
|
-
* **original**, pre-prefix names) carry the supplied actions per **MOD-030**.
|
|
857
|
-
*
|
|
858
|
-
* For each entry `[originalName, action]`, the renamed body is searched for
|
|
859
|
-
* the transition whose name equals `prefix + "/" + originalName`. The
|
|
860
|
-
* resulting instance shares the original `def`, `prefix`, `params`, and
|
|
861
|
-
* port/channel handle topology — only the renamed body is rebuilt with new
|
|
862
|
-
* actions. Per **MOD-030**, calling `bindActions` on one instance does NOT
|
|
863
|
-
* affect the actions held by other instances of the same `def`.
|
|
864
|
-
*
|
|
865
|
-
* Unrecognised original names raise an `Error` so typos surface eagerly.
|
|
866
|
-
*/
|
|
867
|
-
bindActions(actionsByOriginalName) {
|
|
868
|
-
const prefixedByName = /* @__PURE__ */ new Map();
|
|
869
|
-
const renamedNames = /* @__PURE__ */ new Set();
|
|
870
|
-
for (const t of this.renamedBody.transitions) {
|
|
871
|
-
renamedNames.add(t.name);
|
|
872
|
-
}
|
|
873
|
-
for (const originalName of Object.keys(actionsByOriginalName)) {
|
|
874
|
-
const prefixed = this.prefix + "/" + originalName;
|
|
875
|
-
if (!renamedNames.has(prefixed)) {
|
|
876
|
-
throw new Error(
|
|
877
|
-
`Instance.bindActions: no transition '${originalName}' (resolved as '${prefixed}') in instance '${this.prefix}' of subnet '${this.def.name}'`
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
prefixedByName.set(prefixed, actionsByOriginalName[originalName]);
|
|
881
|
-
}
|
|
882
|
-
const reboundBody = this.renamedBody.bindActionsWithResolver((name) => {
|
|
883
|
-
const action = prefixedByName.get(name);
|
|
884
|
-
if (action !== void 0) return action;
|
|
885
|
-
for (const t of this.renamedBody.transitions) {
|
|
886
|
-
if (t.name === name) return t.action;
|
|
887
|
-
}
|
|
888
|
-
throw new Error(`Instance.bindActions: resolver invoked with unknown name '${name}'`);
|
|
889
|
-
});
|
|
890
|
-
const reboundChannelHandles = /* @__PURE__ */ new Map();
|
|
891
|
-
if (this.channelHandles.size > 0) {
|
|
892
|
-
const byName = /* @__PURE__ */ new Map();
|
|
893
|
-
for (const t of reboundBody.transitions) {
|
|
894
|
-
byName.set(t.name, t);
|
|
895
|
-
}
|
|
896
|
-
for (const [name, oldT] of this.channelHandles) {
|
|
897
|
-
const refreshed = byName.get(oldT.name);
|
|
898
|
-
if (refreshed === void 0) {
|
|
899
|
-
throw new Error(
|
|
900
|
-
`Instance.bindActions: channel '${name}' transition '${oldT.name}' not found in rebound body`
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
reboundChannelHandles.set(name, refreshed);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
return __createInstance(
|
|
907
|
-
this.prefix,
|
|
908
|
-
this.def,
|
|
909
|
-
reboundBody,
|
|
910
|
-
this.portHandles,
|
|
911
|
-
reboundChannelHandles,
|
|
912
|
-
this.params
|
|
913
|
-
);
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
function __createInstance(prefix, def, renamedBody, portHandles, channelHandles, params) {
|
|
917
|
-
return new Instance(
|
|
918
|
-
INSTANCE_KEY,
|
|
919
|
-
prefix,
|
|
920
|
-
def,
|
|
921
|
-
renamedBody,
|
|
922
|
-
portHandles,
|
|
923
|
-
channelHandles,
|
|
924
|
-
params
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// src/core/internal/subnet-rewriter.ts
|
|
929
|
-
function renamePlace(orig, prefix) {
|
|
930
|
-
return place(prefix + "/" + orig.name);
|
|
931
|
-
}
|
|
932
|
-
function renameNet(body, prefix, placeRemap, transitionRemap) {
|
|
933
|
-
placeRemap.clear();
|
|
934
|
-
transitionRemap.clear();
|
|
935
|
-
for (const orig of body.places) {
|
|
936
|
-
placeRemap.set(orig.name, renamePlace(orig, prefix));
|
|
937
|
-
}
|
|
938
|
-
const builder = PetriNet.builder(prefix + "/" + body.name);
|
|
939
|
-
for (const renamedPlace of placeRemap.values()) {
|
|
940
|
-
builder.place(renamedPlace);
|
|
941
|
-
}
|
|
942
|
-
for (const t of body.transitions) {
|
|
943
|
-
const renamed = rewriteTransition(t, prefix, placeRemap);
|
|
944
|
-
transitionRemap.set(t.name, renamed);
|
|
945
|
-
builder.transition(renamed);
|
|
946
|
-
}
|
|
947
|
-
return builder.build();
|
|
948
|
-
}
|
|
949
|
-
function rewriteTransition(t, prefix, placeRemap) {
|
|
950
|
-
return rebuildWithName(t, prefix + "/" + t.name, placeRemap);
|
|
951
|
-
}
|
|
952
|
-
function substitutePlaces(t, remap) {
|
|
953
|
-
return rebuildWithName(t, t.name, remap);
|
|
954
|
-
}
|
|
955
|
-
function rebuildWithName(t, name, remap, normalizeInputs) {
|
|
956
|
-
const builder = Transition.builder(name).timing(t.timing).priority(t.priority).action(t.action);
|
|
957
|
-
const alias = buildPlaceAlias(t, remap);
|
|
958
|
-
if (alias.size > 0) {
|
|
959
|
-
builder.placeAlias(alias);
|
|
960
|
-
}
|
|
961
|
-
if (t.inputSpecs.length > 0) {
|
|
962
|
-
const rewrittenInputs = new Array(t.inputSpecs.length);
|
|
963
|
-
for (let i = 0; i < t.inputSpecs.length; i++) {
|
|
964
|
-
rewrittenInputs[i] = rewriteIn(t.inputSpecs[i], remap);
|
|
965
|
-
}
|
|
966
|
-
builder.inputs(...normalizeInputs !== void 0 ? normalizeInputs(rewrittenInputs) : rewrittenInputs);
|
|
967
|
-
}
|
|
968
|
-
if (t.outputSpec !== null) {
|
|
969
|
-
builder.outputs(rewriteOut(t.outputSpec, remap));
|
|
970
|
-
}
|
|
971
|
-
for (let i = 0; i < t.inhibitors.length; i++) {
|
|
972
|
-
builder.inhibitor(rewriteInhibitor(t.inhibitors[i], remap).place);
|
|
973
|
-
}
|
|
974
|
-
for (let i = 0; i < t.reads.length; i++) {
|
|
975
|
-
builder.read(rewriteRead(t.reads[i], remap).place);
|
|
976
|
-
}
|
|
977
|
-
for (let i = 0; i < t.resets.length; i++) {
|
|
978
|
-
builder.reset(rewriteReset(t.resets[i], remap).place);
|
|
979
|
-
}
|
|
980
|
-
if (t.matchSpec !== null) {
|
|
981
|
-
builder.match({
|
|
982
|
-
keys: t.matchSpec.keys.map((k) => ({
|
|
983
|
-
place: remap.get(k.place.name) ?? k.place,
|
|
984
|
-
key: k.key
|
|
985
|
-
}))
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
return builder.build();
|
|
989
|
-
}
|
|
990
|
-
function buildPlaceAlias(t, remap) {
|
|
991
|
-
const prev = t.placeAlias;
|
|
992
|
-
if (remap.size === 0 && prev.size === 0) {
|
|
993
|
-
return EMPTY_ALIAS2;
|
|
994
|
-
}
|
|
995
|
-
const alias = /* @__PURE__ */ new Map();
|
|
996
|
-
if (prev.size > 0) {
|
|
997
|
-
for (const [declaredName, prevActual] of prev) {
|
|
998
|
-
const replaced = remap.get(prevActual.name);
|
|
999
|
-
const finalActual = replaced !== void 0 ? replaced : prevActual;
|
|
1000
|
-
if (finalActual.name !== declaredName) {
|
|
1001
|
-
alias.set(declaredName, finalActual);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
return alias;
|
|
1005
|
-
}
|
|
1006
|
-
const record = (p) => {
|
|
1007
|
-
if (alias.has(p.name)) return;
|
|
1008
|
-
const replaced = remap.get(p.name);
|
|
1009
|
-
if (replaced !== void 0 && replaced.name !== p.name) {
|
|
1010
|
-
alias.set(p.name, replaced);
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
|
-
for (const spec of t.inputSpecs) record(spec.place);
|
|
1014
|
-
for (const rd of t.reads) record(rd.place);
|
|
1015
|
-
for (const inh of t.inhibitors) record(inh.place);
|
|
1016
|
-
for (const rs of t.resets) record(rs.place);
|
|
1017
|
-
if (t.outputSpec !== null) {
|
|
1018
|
-
for (const p of allPlaces(t.outputSpec)) record(p);
|
|
1019
|
-
}
|
|
1020
|
-
return alias;
|
|
1021
|
-
}
|
|
1022
|
-
var EMPTY_ALIAS2 = /* @__PURE__ */ new Map();
|
|
1023
|
-
function rewriteIn(spec, remap) {
|
|
1024
|
-
switch (spec.type) {
|
|
1025
|
-
case "one":
|
|
1026
|
-
return one(resolve(spec.place, remap));
|
|
1027
|
-
case "exactly":
|
|
1028
|
-
return exactly(spec.count, resolve(spec.place, remap));
|
|
1029
|
-
case "all":
|
|
1030
|
-
return all(resolve(spec.place, remap));
|
|
1031
|
-
case "at-least":
|
|
1032
|
-
return atLeast(spec.minimum, resolve(spec.place, remap));
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
function rewriteOut(out, remap) {
|
|
1036
|
-
switch (out.type) {
|
|
1037
|
-
case "place":
|
|
1038
|
-
return outPlace(resolve(out.place, remap));
|
|
1039
|
-
case "forward-input":
|
|
1040
|
-
return forwardInput(resolve(out.from, remap), resolve(out.to, remap));
|
|
1041
|
-
case "and": {
|
|
1042
|
-
const children = out.children;
|
|
1043
|
-
const rewritten = new Array(children.length);
|
|
1044
|
-
for (let i = 0; i < children.length; i++) {
|
|
1045
|
-
rewritten[i] = rewriteOut(children[i], remap);
|
|
1046
|
-
}
|
|
1047
|
-
return { type: "and", children: rewritten };
|
|
1048
|
-
}
|
|
1049
|
-
case "xor": {
|
|
1050
|
-
const children = out.children;
|
|
1051
|
-
const rewritten = new Array(children.length);
|
|
1052
|
-
for (let i = 0; i < children.length; i++) {
|
|
1053
|
-
rewritten[i] = rewriteOut(children[i], remap);
|
|
1054
|
-
}
|
|
1055
|
-
return { type: "xor", children: rewritten };
|
|
1056
|
-
}
|
|
1057
|
-
case "timeout":
|
|
1058
|
-
return timeout(out.afterMs, rewriteOut(out.child, remap));
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
function rewriteInhibitor(inh, remap) {
|
|
1062
|
-
return { type: "inhibitor", place: resolve(inh.place, remap) };
|
|
1063
|
-
}
|
|
1064
|
-
function rewriteRead(rd, remap) {
|
|
1065
|
-
return { type: "read", place: resolve(rd.place, remap) };
|
|
1066
|
-
}
|
|
1067
|
-
function rewriteReset(rs, remap) {
|
|
1068
|
-
return { type: "reset", place: resolve(rs.place, remap) };
|
|
1069
|
-
}
|
|
1070
|
-
function resolve(p, remap) {
|
|
1071
|
-
const replaced = remap.get(p.name);
|
|
1072
|
-
return replaced !== void 0 ? replaced : p;
|
|
1073
|
-
}
|
|
1074
|
-
function mergeTransitions(caller, instance, mergedName) {
|
|
1075
|
-
if (mergedName === void 0 || mergedName === null || mergedName.length === 0) {
|
|
1076
|
-
throw new Error("mergeTransitions: mergedName must be a non-empty string");
|
|
1077
|
-
}
|
|
1078
|
-
const mergedTiming = mergeTimings(caller.timing, instance.timing, mergedName);
|
|
1079
|
-
const mergedPriority = pickPriority(caller.priority, instance.priority);
|
|
1080
|
-
const mergedAction = composeActions(caller.action, instance.action);
|
|
1081
|
-
const mergedMatch = mergeMatchSpecs(caller.matchSpec, instance.matchSpec, mergedName);
|
|
1082
|
-
const mergedAlias = mergePlaceAlias(caller.placeAlias, instance.placeAlias, mergedName);
|
|
1083
|
-
const builder = Transition.builder(mergedName).timing(mergedTiming).priority(mergedPriority);
|
|
1084
|
-
if (mergedAction !== void 0) {
|
|
1085
|
-
builder.action(mergedAction);
|
|
1086
|
-
}
|
|
1087
|
-
rejectCrossSideKindConflicts(caller, instance, mergedName);
|
|
1088
|
-
const unionedInputs = normalizeInputArcs(
|
|
1089
|
-
[...caller.inputSpecs, ...instance.inputSpecs],
|
|
1090
|
-
() => `Channel composition '${mergedName}'`
|
|
1091
|
-
);
|
|
1092
|
-
if (unionedInputs.length > 0) {
|
|
1093
|
-
builder.inputs(...unionedInputs);
|
|
1094
|
-
}
|
|
1095
|
-
const mergedOutput = mergeOutputs(caller.outputSpec, instance.outputSpec);
|
|
1096
|
-
if (mergedOutput !== null) {
|
|
1097
|
-
builder.outputs(mergedOutput);
|
|
1098
|
-
}
|
|
1099
|
-
for (const inh of unionArcs(
|
|
1100
|
-
caller.inhibitors,
|
|
1101
|
-
instance.inhibitors,
|
|
1102
|
-
keyOfInhibitor
|
|
1103
|
-
)) {
|
|
1104
|
-
builder.inhibitor(inh.place);
|
|
1105
|
-
}
|
|
1106
|
-
for (const rd of unionArcs(caller.reads, instance.reads, keyOfRead)) {
|
|
1107
|
-
builder.read(rd.place);
|
|
1108
|
-
}
|
|
1109
|
-
for (const rs of unionArcs(caller.resets, instance.resets, keyOfReset)) {
|
|
1110
|
-
builder.reset(rs.place);
|
|
1111
|
-
}
|
|
1112
|
-
if (mergedMatch !== null) {
|
|
1113
|
-
builder.match(mergedMatch);
|
|
1114
|
-
}
|
|
1115
|
-
if (mergedAlias.size > 0) {
|
|
1116
|
-
builder.placeAlias(mergedAlias);
|
|
1117
|
-
}
|
|
1118
|
-
return builder.build();
|
|
1119
|
-
}
|
|
1120
|
-
function mergeMatchSpecs(caller, instance, channelName) {
|
|
1121
|
-
if (caller === null) return instance;
|
|
1122
|
-
if (instance === null) return caller;
|
|
1123
|
-
throw new Error(
|
|
1124
|
-
`Channel composition '${channelName}': both the caller-side and instance-side transition carry a \u03BD-net match \u2014 refusing to fuse two independent correlations into one transition (NU-060). Resolve explicitly by keeping the match on a single side.`
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
function mergePlaceAlias(caller, instance, channelName) {
|
|
1128
|
-
if (caller.size === 0) return instance;
|
|
1129
|
-
if (instance.size === 0) return caller;
|
|
1130
|
-
const merged = new Map(caller);
|
|
1131
|
-
for (const [declared, actual] of instance) {
|
|
1132
|
-
const existing = merged.get(declared);
|
|
1133
|
-
if (existing !== void 0 && existing.name !== actual.name) {
|
|
1134
|
-
throw new Error(
|
|
1135
|
-
`Channel composition '${channelName}': conflicting declared\u2192actual place alias for declared place '${declared}' \u2014 caller-side maps to '${existing.name}', instance-side to '${actual.name}' (MOD-031). Resolve explicitly.`
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
merged.set(declared, actual);
|
|
1139
|
-
}
|
|
1140
|
-
return merged;
|
|
1141
|
-
}
|
|
1142
|
-
function mergeTimings(caller, instance, channelName) {
|
|
1143
|
-
if (caller.type === "immediate" && instance.type === "immediate") {
|
|
1144
|
-
return { type: "immediate" };
|
|
1145
|
-
}
|
|
1146
|
-
if (caller.type === "immediate") return instance;
|
|
1147
|
-
if (instance.type === "immediate") return caller;
|
|
1148
|
-
if (timingsEqual(caller, instance)) return caller;
|
|
1149
|
-
throw new Error(
|
|
1150
|
-
`Channel composition '${channelName}': conflicting non-Immediate timings \u2014 caller-side ${describeTiming(caller)} vs instance-side ${describeTiming(instance)}. Resolve explicitly by aligning the timings on either side (MOD-021).`
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
function pickPriority(callerPriority, _instancePriority) {
|
|
1154
|
-
return callerPriority;
|
|
1155
|
-
}
|
|
1156
|
-
function composeActions(caller, instance) {
|
|
1157
|
-
const callerIsPassthrough = caller === void 0 || isPassthrough(caller);
|
|
1158
|
-
const instanceIsPassthrough = instance === void 0 || isPassthrough(instance);
|
|
1159
|
-
if (callerIsPassthrough && instanceIsPassthrough) return void 0;
|
|
1160
|
-
if (callerIsPassthrough) return instance;
|
|
1161
|
-
if (instanceIsPassthrough) return caller;
|
|
1162
|
-
return async (ctx) => {
|
|
1163
|
-
await caller(ctx);
|
|
1164
|
-
await instance(ctx);
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
function mergeOutputs(caller, instance) {
|
|
1168
|
-
if (caller === null && instance === null) return null;
|
|
1169
|
-
if (caller === null) return instance;
|
|
1170
|
-
if (instance === null) return caller;
|
|
1171
|
-
return and(caller, instance);
|
|
1172
|
-
}
|
|
1173
|
-
function unionArcs(caller, instance, keyOf) {
|
|
1174
|
-
if (caller.length === 0 && instance.length === 0) return [];
|
|
1175
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1176
|
-
for (let i2 = 0; i2 < caller.length; i2++) {
|
|
1177
|
-
const arc = caller[i2];
|
|
1178
|
-
const key = keyOf(arc);
|
|
1179
|
-
if (!seen.has(key)) seen.set(key, arc);
|
|
1180
|
-
}
|
|
1181
|
-
for (let i2 = 0; i2 < instance.length; i2++) {
|
|
1182
|
-
const arc = instance[i2];
|
|
1183
|
-
const key = keyOf(arc);
|
|
1184
|
-
if (!seen.has(key)) seen.set(key, arc);
|
|
1185
|
-
}
|
|
1186
|
-
const result = new Array(seen.size);
|
|
1187
|
-
let i = 0;
|
|
1188
|
-
for (const arc of seen.values()) result[i++] = arc;
|
|
1189
|
-
return result;
|
|
1190
|
-
}
|
|
1191
|
-
function timingsEqual(a, b) {
|
|
1192
|
-
if (a.type !== b.type) return false;
|
|
1193
|
-
switch (a.type) {
|
|
1194
|
-
case "immediate":
|
|
1195
|
-
return true;
|
|
1196
|
-
case "deadline":
|
|
1197
|
-
return a.byMs === b.byMs;
|
|
1198
|
-
case "delayed":
|
|
1199
|
-
return a.afterMs === b.afterMs;
|
|
1200
|
-
case "window": {
|
|
1201
|
-
const w = b;
|
|
1202
|
-
return a.earliestMs === w.earliestMs && a.latestMs === w.latestMs;
|
|
1203
|
-
}
|
|
1204
|
-
case "exact":
|
|
1205
|
-
return a.atMs === b.atMs;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
function describeTiming(t) {
|
|
1209
|
-
switch (t.type) {
|
|
1210
|
-
case "immediate":
|
|
1211
|
-
return "Immediate";
|
|
1212
|
-
case "deadline":
|
|
1213
|
-
return `Deadline(byMs=${t.byMs})`;
|
|
1214
|
-
case "delayed":
|
|
1215
|
-
return `Delayed(afterMs=${t.afterMs})`;
|
|
1216
|
-
case "window":
|
|
1217
|
-
return `Window(earliestMs=${t.earliestMs}, latestMs=${t.latestMs})`;
|
|
1218
|
-
case "exact":
|
|
1219
|
-
return `Exact(atMs=${t.atMs})`;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
function keyOfInhibitor(arc) {
|
|
1223
|
-
return `inh|${arc.place.name}`;
|
|
1224
|
-
}
|
|
1225
|
-
function keyOfRead(arc) {
|
|
1226
|
-
return `read|${arc.place.name}`;
|
|
1227
|
-
}
|
|
1228
|
-
function keyOfReset(arc) {
|
|
1229
|
-
return `reset|${arc.place.name}`;
|
|
1230
|
-
}
|
|
1231
|
-
function normalizeInputArcs(arcs, seamOf) {
|
|
1232
|
-
if (arcs.length < 2) return arcs;
|
|
1233
|
-
const byPlace = /* @__PURE__ */ new Map();
|
|
1234
|
-
let collided = false;
|
|
1235
|
-
for (let i2 = 0; i2 < arcs.length; i2++) {
|
|
1236
|
-
const arc = arcs[i2];
|
|
1237
|
-
const name = arc.place.name;
|
|
1238
|
-
const prior = byPlace.get(name);
|
|
1239
|
-
if (prior === void 0) {
|
|
1240
|
-
byPlace.set(name, arc);
|
|
1241
|
-
} else {
|
|
1242
|
-
collided = true;
|
|
1243
|
-
byPlace.set(name, mergeInPair(prior, arc, seamOf(name)));
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
if (!collided) return arcs;
|
|
1247
|
-
const result = new Array(byPlace.size);
|
|
1248
|
-
let i = 0;
|
|
1249
|
-
for (const arc of byPlace.values()) result[i++] = arc;
|
|
1250
|
-
return result;
|
|
1251
|
-
}
|
|
1252
|
-
function mergeInPair(a, b, seam) {
|
|
1253
|
-
if (a.type === "all" && b.type === "all") return a;
|
|
1254
|
-
if (a.type === "at-least" && b.type === "at-least") {
|
|
1255
|
-
return a.minimum >= b.minimum ? a : b;
|
|
1256
|
-
}
|
|
1257
|
-
const countA = summableCount(a);
|
|
1258
|
-
const countB = summableCount(b);
|
|
1259
|
-
if (countA !== -1 && countB !== -1) {
|
|
1260
|
-
return exactly(countA + countB, a.place);
|
|
1261
|
-
}
|
|
1262
|
-
throw new Error(
|
|
1263
|
-
`${seam}: input arcs ${describeIn(a)} and ${describeIn(b)} collide on place '${a.place.name}' and have no additive merge (MOD-021 rule (c)). Use a single arc with exactly(n) / atLeast(n).`
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
function summableCount(arc) {
|
|
1267
|
-
switch (arc.type) {
|
|
1268
|
-
case "one":
|
|
1269
|
-
return 1;
|
|
1270
|
-
case "exactly":
|
|
1271
|
-
return arc.count;
|
|
1272
|
-
default:
|
|
1273
|
-
return -1;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
function describeIn(arc) {
|
|
1277
|
-
switch (arc.type) {
|
|
1278
|
-
case "one":
|
|
1279
|
-
return "one()";
|
|
1280
|
-
case "exactly":
|
|
1281
|
-
return `exactly(${arc.count})`;
|
|
1282
|
-
case "all":
|
|
1283
|
-
return "all()";
|
|
1284
|
-
case "at-least":
|
|
1285
|
-
return `atLeast(${arc.minimum})`;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
function rejectCrossSideKindConflicts(caller, instance, mergedName) {
|
|
1289
|
-
const callerKinds = arcKindsByPlace(caller);
|
|
1290
|
-
if (callerKinds.size === 0) return;
|
|
1291
|
-
for (const [place2, instanceSet] of arcKindsByPlace(instance)) {
|
|
1292
|
-
const callerSet = callerKinds.get(place2);
|
|
1293
|
-
if (callerSet !== void 0 && !sameKindSet(callerSet, instanceSet)) {
|
|
1294
|
-
throw new Error(
|
|
1295
|
-
`Channel composition '${mergedName}': conflicting arc kinds on place '${place2}' \u2014 caller-side ${describeKinds(callerSet)} vs instance-side ${describeKinds(instanceSet)}. Different arc types on one place cannot be merged (MOD-021 rule (d)). Resolve explicitly.`
|
|
1296
|
-
);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
function arcKindsByPlace(t) {
|
|
1301
|
-
const kinds = /* @__PURE__ */ new Map();
|
|
1302
|
-
const add = (name, kind) => {
|
|
1303
|
-
let set = kinds.get(name);
|
|
1304
|
-
if (set === void 0) {
|
|
1305
|
-
set = /* @__PURE__ */ new Set();
|
|
1306
|
-
kinds.set(name, set);
|
|
1307
|
-
}
|
|
1308
|
-
set.add(kind);
|
|
1309
|
-
};
|
|
1310
|
-
for (const s of t.inputSpecs) add(s.place.name, "input");
|
|
1311
|
-
for (const a of t.inhibitors) add(a.place.name, "inhibitor");
|
|
1312
|
-
for (const a of t.reads) add(a.place.name, "read");
|
|
1313
|
-
for (const a of t.resets) add(a.place.name, "reset");
|
|
1314
|
-
return kinds;
|
|
1315
|
-
}
|
|
1316
|
-
function describeKinds(kinds) {
|
|
1317
|
-
return `[${[...kinds].sort().join(", ")}]`;
|
|
1318
|
-
}
|
|
1319
|
-
function sameKindSet(a, b) {
|
|
1320
|
-
if (a.size !== b.size) return false;
|
|
1321
|
-
for (const k of a) if (!b.has(k)) return false;
|
|
1322
|
-
return true;
|
|
1323
|
-
}
|
|
1324
|
-
function applyFusion(transitions, fusionMap, seamOf) {
|
|
1325
|
-
const normalizeInputs = (inputs) => normalizeInputArcs(inputs, seamOf);
|
|
1326
|
-
const rewritten = /* @__PURE__ */ new Set();
|
|
1327
|
-
for (const t of transitions) {
|
|
1328
|
-
rewritten.add(rebuildWithName(
|
|
1329
|
-
t,
|
|
1330
|
-
t.name,
|
|
1331
|
-
fusionMap,
|
|
1332
|
-
fusionTouchesInputs(t, fusionMap) ? normalizeInputs : void 0
|
|
1333
|
-
));
|
|
1334
|
-
}
|
|
1335
|
-
return rewritten;
|
|
1336
|
-
}
|
|
1337
|
-
function fusionTouchesInputs(t, fusionMap) {
|
|
1338
|
-
if (fusionMap.size === 0) return false;
|
|
1339
|
-
for (let i = 0; i < t.inputSpecs.length; i++) {
|
|
1340
|
-
if (fusionMap.has(t.inputSpecs[i].place.name)) return true;
|
|
1341
|
-
}
|
|
1342
|
-
return false;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// src/verification/verification-harness.ts
|
|
1346
|
-
function buildVerificationResult(syntheticNet, perProperty) {
|
|
1347
|
-
const frozen = new Map(perProperty);
|
|
1348
|
-
return {
|
|
1349
|
-
syntheticNet,
|
|
1350
|
-
perProperty: frozen,
|
|
1351
|
-
allProven() {
|
|
1352
|
-
for (const r of frozen.values()) {
|
|
1353
|
-
if (!isProven(r)) return false;
|
|
1354
|
-
}
|
|
1355
|
-
return true;
|
|
1356
|
-
},
|
|
1357
|
-
anyViolated() {
|
|
1358
|
-
for (const r of frozen.values()) {
|
|
1359
|
-
if (isViolated(r)) return true;
|
|
1360
|
-
}
|
|
1361
|
-
return false;
|
|
1362
|
-
}
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
function normaliseGenerators(generators) {
|
|
1366
|
-
if (generators instanceof Map) return new Map(generators);
|
|
1367
|
-
return new Map(Object.entries(generators));
|
|
1368
|
-
}
|
|
1369
|
-
function normaliseProperties(properties) {
|
|
1370
|
-
if (Array.isArray(properties)) return [...properties];
|
|
1371
|
-
return [...properties];
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// src/core/subnet-def.ts
|
|
1375
|
-
var SUBNET_DEF_KEY = /* @__PURE__ */ Symbol("SubnetDef.internal");
|
|
1376
|
-
var SubnetDef = class _SubnetDef {
|
|
1377
|
-
name;
|
|
1378
|
-
body;
|
|
1379
|
-
iface;
|
|
1380
|
-
/** @internal Use {@link SubnetDef.builder} or {@link SubnetDef.fromNet} to create instances. */
|
|
1381
|
-
constructor(key, name, body, iface) {
|
|
1382
|
-
if (key !== SUBNET_DEF_KEY) {
|
|
1383
|
-
throw new Error("Use SubnetDef.builder() or SubnetDef.fromNet() to create instances");
|
|
1384
|
-
}
|
|
1385
|
-
this.name = name;
|
|
1386
|
-
this.body = body;
|
|
1387
|
-
this.iface = iface;
|
|
1388
|
-
}
|
|
1389
|
-
/**
|
|
1390
|
-
* Produces a renamed module instance per **MOD-010**, **MOD-011**, **MOD-012**,
|
|
1391
|
-
* and **MOD-030**.
|
|
1392
|
-
*
|
|
1393
|
-
* The rename pass walks every place and transition of the body net,
|
|
1394
|
-
* substituting each name with `prefix + "/" + originalName`, and rebuilds
|
|
1395
|
-
* every arc with rewritten place references. Transition timing, priority,
|
|
1396
|
-
* and action are carried through by reference (action sharing per
|
|
1397
|
-
* [MOD-030]). The renamed body is itself a structurally valid `PetriNet`
|
|
1398
|
-
* per [CORE-040]; per-instance state isolation per [MOD-012] is a
|
|
1399
|
-
* structural consequence of distinct prefixed names.
|
|
1400
|
-
*
|
|
1401
|
-
* ## Prefix validation
|
|
1402
|
-
*
|
|
1403
|
-
* The `"/"` character is reserved as the prefix separator (per [MOD-010]).
|
|
1404
|
-
* User-supplied prefixes MUST NOT contain `"/"`; nested instantiation is
|
|
1405
|
-
* performed by the future `PetriNetBuilder.compose(...)` mechanism (per
|
|
1406
|
-
* [MOD-013]). A prefix containing `"/"` raises an `Error`.
|
|
1407
|
-
*
|
|
1408
|
-
* @param prefix the rename prefix (non-empty, must not contain `"/"`)
|
|
1409
|
-
* @param params the parameter value carried through to `Instance.params`
|
|
1410
|
-
* (may be omitted when `P` is `void`)
|
|
1411
|
-
* @throws when `prefix` is empty or contains `"/"`
|
|
1412
|
-
*/
|
|
1413
|
-
instantiate(prefix, params) {
|
|
1414
|
-
validatePrefix(prefix);
|
|
1415
|
-
const placeRemap = /* @__PURE__ */ new Map();
|
|
1416
|
-
const transitionRemap = /* @__PURE__ */ new Map();
|
|
1417
|
-
const renamedBody = renameNet(this.body, prefix, placeRemap, transitionRemap);
|
|
1418
|
-
const portHandles = /* @__PURE__ */ new Map();
|
|
1419
|
-
for (const port of this.iface.ports.values()) {
|
|
1420
|
-
const renamed = placeRemap.get(port.place.name);
|
|
1421
|
-
if (renamed === void 0) {
|
|
1422
|
-
throw new Error(
|
|
1423
|
-
`Port '${port.name}' references place '${port.place.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
portHandles.set(port.name, renamed);
|
|
1427
|
-
}
|
|
1428
|
-
const channelHandles = /* @__PURE__ */ new Map();
|
|
1429
|
-
for (const channel of this.iface.channels.values()) {
|
|
1430
|
-
const renamed = transitionRemap.get(channel.transition.name);
|
|
1431
|
-
if (renamed === void 0) {
|
|
1432
|
-
throw new Error(
|
|
1433
|
-
`Channel '${channel.name}' references transition '${channel.transition.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
1434
|
-
);
|
|
1435
|
-
}
|
|
1436
|
-
channelHandles.set(channel.name, renamed);
|
|
1437
|
-
}
|
|
1438
|
-
return __createInstance(
|
|
1439
|
-
prefix,
|
|
1440
|
-
this,
|
|
1441
|
-
renamedBody,
|
|
1442
|
-
portHandles,
|
|
1443
|
-
channelHandles,
|
|
1444
|
-
params
|
|
1445
|
-
);
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Verifies safety properties of this subnet definition **in isolation** per
|
|
1449
|
-
* **MOD-051**, by wrapping it in a synthetic enclosing net where each input
|
|
1450
|
-
* port is fed by an {@link EnvironmentPlace} (token-source per the harness
|
|
1451
|
-
* generator) and each output port is observed via a synthetic place. The
|
|
1452
|
-
* standard {@link SmtVerifier} (per [MOD-050]) is invoked once per property
|
|
1453
|
-
* declared in the harness; the resulting per-property outcomes are
|
|
1454
|
-
* aggregated into a {@link VerificationResult}.
|
|
1455
|
-
*
|
|
1456
|
-
* ## Synthetic-net construction
|
|
1457
|
-
*
|
|
1458
|
-
* The synthetic enclosing net is built by:
|
|
1459
|
-
* 1. Instantiating this `SubnetDef` with the prefix `"sut"` (system-under-test)
|
|
1460
|
-
* and `harness.params`.
|
|
1461
|
-
* 2. For each input or in-out port on the interface, looking up the
|
|
1462
|
-
* harness generator by port name, allocating a synthetic
|
|
1463
|
-
* {@link Place}`<unknown>` of the same conceptual token type as the
|
|
1464
|
-
* port, wrapping it in an {@link EnvironmentPlace}, and binding the
|
|
1465
|
-
* port to that synthetic place via
|
|
1466
|
-
* {@link import('./petri-net.js').PetriNetBuilder.compose}. The supplier
|
|
1467
|
-
* is invoked once at construction time to materialize the seed token —
|
|
1468
|
-
* its presence is what bounds the input behavior under analysis. **If
|
|
1469
|
-
* the harness map is missing a generator for a required input or in-out
|
|
1470
|
-
* port, an `Error` is thrown.**
|
|
1471
|
-
* 3. For each output or in-out port, allocating a synthetic observation
|
|
1472
|
-
* {@link Place}`<unknown>` and binding the port to it via the same
|
|
1473
|
-
* `compose(...)` call. The verifier inspects this place's reachability
|
|
1474
|
-
* / marking through the standard property APIs ({@link SmtProperty}).
|
|
1475
|
-
* 4. Building the resulting flat {@link PetriNet} per [MOD-023] (the
|
|
1476
|
-
* verifier is composition-unaware per [MOD-050]).
|
|
1477
|
-
*
|
|
1478
|
-
* ## Per-property invocation
|
|
1479
|
-
*
|
|
1480
|
-
* Each {@link SmtProperty} in the harness is verified independently against
|
|
1481
|
-
* the same synthetic net. The synthetic environment places are passed
|
|
1482
|
-
* through to the verifier so that places driven by the harness generators
|
|
1483
|
-
* are treated under the verifier's environment-analysis semantics rather
|
|
1484
|
-
* than as ordinary sink places.
|
|
1485
|
-
*
|
|
1486
|
-
* @param harness the verification harness — supplies parameters, input-port
|
|
1487
|
-
* token generators, and the property set
|
|
1488
|
-
* @returns a {@link VerificationResult} aggregating per-property
|
|
1489
|
-
* {@link SmtVerificationResult}s
|
|
1490
|
-
* @throws when an input or in-out port is missing a harness generator
|
|
1491
|
-
*/
|
|
1492
|
-
async verify(harness, environmentMode = alwaysAvailable()) {
|
|
1493
|
-
if (harness === null || harness === void 0) {
|
|
1494
|
-
throw new Error("SubnetDef.verify: harness must not be null/undefined");
|
|
1495
|
-
}
|
|
1496
|
-
const sut = this.instantiate("sut", harness.params);
|
|
1497
|
-
const generators = normaliseGenerators(harness.portInputGenerators);
|
|
1498
|
-
const properties = normaliseProperties(harness.properties);
|
|
1499
|
-
const envPlaces = [];
|
|
1500
|
-
const portMappings = /* @__PURE__ */ new Map();
|
|
1501
|
-
for (const port of this.iface.ports.values()) {
|
|
1502
|
-
const portName = port.name;
|
|
1503
|
-
switch (port.direction) {
|
|
1504
|
-
case "input": {
|
|
1505
|
-
const generator = generators.get(portName);
|
|
1506
|
-
if (generator === void 0) {
|
|
1507
|
-
throw new Error(
|
|
1508
|
-
`verify: harness is missing an input generator for port '${portName}' on subnet '${this.name}' (MOD-051)`
|
|
1509
|
-
);
|
|
1510
|
-
}
|
|
1511
|
-
const seed = generator();
|
|
1512
|
-
if (seed === null || seed === void 0) {
|
|
1513
|
-
throw new Error(
|
|
1514
|
-
`verify: input generator for port '${portName}' on subnet '${this.name}' produced null/undefined`
|
|
1515
|
-
);
|
|
1516
|
-
}
|
|
1517
|
-
const synth = place(`harness_in_${portName}`);
|
|
1518
|
-
envPlaces.push(environmentPlace(synth.name));
|
|
1519
|
-
portMappings.set(portName, synth);
|
|
1520
|
-
break;
|
|
1521
|
-
}
|
|
1522
|
-
case "output": {
|
|
1523
|
-
const synth = place(`harness_out_${portName}`);
|
|
1524
|
-
portMappings.set(portName, synth);
|
|
1525
|
-
break;
|
|
1526
|
-
}
|
|
1527
|
-
case "inout": {
|
|
1528
|
-
const generator = generators.get(portName);
|
|
1529
|
-
if (generator === void 0) {
|
|
1530
|
-
throw new Error(
|
|
1531
|
-
`verify: harness is missing an input generator for in-out port '${portName}' on subnet '${this.name}' (MOD-051)`
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1534
|
-
const seed = generator();
|
|
1535
|
-
if (seed === null || seed === void 0) {
|
|
1536
|
-
throw new Error(
|
|
1537
|
-
`verify: input generator for in-out port '${portName}' on subnet '${this.name}' produced null/undefined`
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
|
-
const synth = place(`harness_io_${portName}`);
|
|
1541
|
-
envPlaces.push(environmentPlace(synth.name));
|
|
1542
|
-
portMappings.set(portName, synth);
|
|
1543
|
-
break;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
const syntheticNet = PetriNet.builder("verify_" + this.name).compose(sut, portMappings).build();
|
|
1548
|
-
requireOutputProducingActions(syntheticNet);
|
|
1549
|
-
const perProperty = /* @__PURE__ */ new Map();
|
|
1550
|
-
for (const property of properties) {
|
|
1551
|
-
const verifier = SmtVerifier.forNet(syntheticNet).property(property);
|
|
1552
|
-
if (envPlaces.length > 0) {
|
|
1553
|
-
verifier.environmentPlaces(...envPlaces);
|
|
1554
|
-
verifier.environmentMode(environmentMode);
|
|
1555
|
-
}
|
|
1556
|
-
const result = await verifier.verify();
|
|
1557
|
-
perProperty.set(property, result);
|
|
1558
|
-
}
|
|
1559
|
-
return buildVerificationResult(syntheticNet, perProperty);
|
|
1560
|
-
}
|
|
1561
|
-
// ============================================================
|
|
1562
|
-
// Static factories
|
|
1563
|
-
// ============================================================
|
|
1564
|
-
static builder(name) {
|
|
1565
|
-
return new SubnetDefBuilder(name);
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* Retrofit utility per **MOD-014**: wraps an existing closed {@link PetriNet}
|
|
1569
|
-
* plus an {@link Interface} into an unparameterised `SubnetDef<void>`.
|
|
1570
|
-
*
|
|
1571
|
-
* Validation per **MOD-014** / **MOD-006** is enforced before the result is
|
|
1572
|
-
* constructed:
|
|
1573
|
-
* - Every port's underlying `Place` must be present in `net.places`.
|
|
1574
|
-
* - Every channel's underlying `Transition` must be present in `net.transitions`.
|
|
1575
|
-
* - Port and channel name uniqueness is re-validated defensively (the
|
|
1576
|
-
* `Interface` builder already enforces this; hand-built `Interface`
|
|
1577
|
-
* values bypass that path).
|
|
1578
|
-
*
|
|
1579
|
-
* The resulting subnet definition is unparameterised (parameter type is `void`).
|
|
1580
|
-
*
|
|
1581
|
-
* @throws when a port place is not in `net.places`, a channel transition is
|
|
1582
|
-
* not in `net.transitions`, or port/channel names are not unique.
|
|
1583
|
-
*/
|
|
1584
|
-
static fromNet(net, iface) {
|
|
1585
|
-
const bodyPlaces = net.places;
|
|
1586
|
-
const bodyTransitions = net.transitions;
|
|
1587
|
-
const seenPortNames = /* @__PURE__ */ new Set();
|
|
1588
|
-
for (const port of iface.ports.values()) {
|
|
1589
|
-
if (seenPortNames.has(port.name)) {
|
|
1590
|
-
throw new Error(
|
|
1591
|
-
`fromNet: duplicate port name '${port.name}' on interface for net '${net.name}' (MOD-006)`
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
1594
|
-
seenPortNames.add(port.name);
|
|
1595
|
-
if (!bodyPlaces.has(port.place)) {
|
|
1596
|
-
throw new Error(
|
|
1597
|
-
`fromNet: port '${port.name}' references place '${port.place.name}' which is not in net '${net.name}' (MOD-014/MOD-006)`
|
|
1598
|
-
);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
const seenChannelNames = /* @__PURE__ */ new Set();
|
|
1602
|
-
for (const channel of iface.channels.values()) {
|
|
1603
|
-
if (seenChannelNames.has(channel.name)) {
|
|
1604
|
-
throw new Error(
|
|
1605
|
-
`fromNet: duplicate channel name '${channel.name}' on interface for net '${net.name}' (MOD-006)`
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
seenChannelNames.add(channel.name);
|
|
1609
|
-
if (!bodyTransitions.has(channel.transition)) {
|
|
1610
|
-
throw new Error(
|
|
1611
|
-
`fromNet: channel '${channel.name}' references transition '${channel.transition.name}' which is not in net '${net.name}' (MOD-014/MOD-006)`
|
|
1612
|
-
);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
return new _SubnetDef(SUBNET_DEF_KEY, net.name, net, iface);
|
|
1616
|
-
}
|
|
1617
|
-
};
|
|
1618
|
-
var SubnetDefBuilder = class {
|
|
1619
|
-
_name;
|
|
1620
|
-
_bodyBuilder;
|
|
1621
|
-
_ports = [];
|
|
1622
|
-
_channels = [];
|
|
1623
|
-
constructor(name) {
|
|
1624
|
-
this._name = name;
|
|
1625
|
-
this._bodyBuilder = PetriNet.builder(name);
|
|
1626
|
-
}
|
|
1627
|
-
// -------- body construction (delegates to PetriNet.Builder) --------
|
|
1628
|
-
transition(transition) {
|
|
1629
|
-
this._bodyBuilder.transition(transition);
|
|
1630
|
-
return this;
|
|
1631
|
-
}
|
|
1632
|
-
transitions(...transitions) {
|
|
1633
|
-
this._bodyBuilder.transitions(...transitions);
|
|
1634
|
-
return this;
|
|
1635
|
-
}
|
|
1636
|
-
place(place2) {
|
|
1637
|
-
this._bodyBuilder.place(place2);
|
|
1638
|
-
return this;
|
|
1639
|
-
}
|
|
1640
|
-
// -------- interface declarations --------
|
|
1641
|
-
inputPort(name, place2) {
|
|
1642
|
-
this._ports.push({ name, direction: "input", place: place2 });
|
|
1643
|
-
return this;
|
|
1644
|
-
}
|
|
1645
|
-
outputPort(name, place2) {
|
|
1646
|
-
this._ports.push({ name, direction: "output", place: place2 });
|
|
1647
|
-
return this;
|
|
1648
|
-
}
|
|
1649
|
-
inoutPort(name, place2) {
|
|
1650
|
-
this._ports.push({ name, direction: "inout", place: place2 });
|
|
1651
|
-
return this;
|
|
1652
|
-
}
|
|
1653
|
-
channel(name, transition) {
|
|
1654
|
-
this._channels.push({ name, transition });
|
|
1655
|
-
return this;
|
|
1656
|
-
}
|
|
1657
|
-
// -------- build & validate (MOD-006) --------
|
|
1658
|
-
build() {
|
|
1659
|
-
const built = this._bodyBuilder.build();
|
|
1660
|
-
const bodyPlaces = built.places;
|
|
1661
|
-
const bodyTransitions = built.transitions;
|
|
1662
|
-
const portNames = /* @__PURE__ */ new Set();
|
|
1663
|
-
for (const port of this._ports) {
|
|
1664
|
-
if (portNames.has(port.name)) {
|
|
1665
|
-
throw new Error(`Subnet '${this._name}': duplicate port name '${port.name}'`);
|
|
1666
|
-
}
|
|
1667
|
-
portNames.add(port.name);
|
|
1668
|
-
if (!bodyPlaces.has(port.place)) {
|
|
1669
|
-
throw new Error(
|
|
1670
|
-
`Subnet '${this._name}': port '${port.name}' references place '${port.place.name}' which is not in the body`
|
|
1671
|
-
);
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
const channelNames = /* @__PURE__ */ new Set();
|
|
1675
|
-
for (const channel of this._channels) {
|
|
1676
|
-
if (channelNames.has(channel.name)) {
|
|
1677
|
-
throw new Error(`Subnet '${this._name}': duplicate channel name '${channel.name}'`);
|
|
1678
|
-
}
|
|
1679
|
-
channelNames.add(channel.name);
|
|
1680
|
-
if (!bodyTransitions.has(channel.transition)) {
|
|
1681
|
-
throw new Error(
|
|
1682
|
-
`Subnet '${this._name}': channel '${channel.name}' references transition '${channel.transition.name}' which is not in the body`
|
|
1683
|
-
);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
const ifaceBuilder = Interface.builder();
|
|
1687
|
-
ifaceBuilder.portsAll(this._ports);
|
|
1688
|
-
ifaceBuilder.channelsAll(this._channels);
|
|
1689
|
-
const iface = ifaceBuilder.build();
|
|
1690
|
-
return new SubnetDef(SUBNET_DEF_KEY, this._name, built, iface);
|
|
1691
|
-
}
|
|
1692
|
-
};
|
|
1693
|
-
function validatePrefix(prefix) {
|
|
1694
|
-
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
1695
|
-
throw new Error("SubnetDef.instantiate: prefix must be a non-empty string");
|
|
1696
|
-
}
|
|
1697
|
-
if (prefix.indexOf("/") >= 0) {
|
|
1698
|
-
throw new Error(
|
|
1699
|
-
`SubnetDef.instantiate: prefix must not contain '/' (reserved as the prefix separator per MOD-010); use compose(...) for nested instantiation. Got: '${prefix}'`
|
|
1700
|
-
);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// src/core/compose-bindings.ts
|
|
1705
|
-
var COMPOSE_BINDINGS_KEY = /* @__PURE__ */ Symbol("ComposeBindings.internal");
|
|
1706
|
-
var ComposeBindings = class {
|
|
1707
|
-
_portBindings = /* @__PURE__ */ new Map();
|
|
1708
|
-
_channelBindings = /* @__PURE__ */ new Map();
|
|
1709
|
-
/**
|
|
1710
|
-
* @internal Use {@link PetriNetBuilder.compose} — instances are produced
|
|
1711
|
-
* by the host builder and supplied to the caller's callback.
|
|
1712
|
-
*/
|
|
1713
|
-
constructor(key) {
|
|
1714
|
-
if (key !== COMPOSE_BINDINGS_KEY) {
|
|
1715
|
-
throw new Error(
|
|
1716
|
-
"Use PetriNetBuilder.compose(instance, b => ...) \u2014 ComposeBindings is not directly constructible"
|
|
1717
|
-
);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
/**
|
|
1721
|
-
* Binds the named interface port to the given caller place per **MOD-020**.
|
|
1722
|
-
*
|
|
1723
|
-
* The port name is the **original** (pre-prefix) name declared in the
|
|
1724
|
-
* subnet's `Interface`. The typed `<T>` parameter ensures the caller
|
|
1725
|
-
* place's token type matches the interface port's token type at compile
|
|
1726
|
-
* time per [MOD-022]; TypeScript does not validate the type at runtime
|
|
1727
|
-
* because generics are erased.
|
|
1728
|
-
*
|
|
1729
|
-
* @throws when `portName` is already bound on this builder
|
|
1730
|
-
*/
|
|
1731
|
-
bindPort(portName, callerPlace) {
|
|
1732
|
-
if (this._portBindings.has(portName)) {
|
|
1733
|
-
throw new Error(`Port '${portName}' is already bound`);
|
|
1734
|
-
}
|
|
1735
|
-
this._portBindings.set(portName, callerPlace);
|
|
1736
|
-
return this;
|
|
1737
|
-
}
|
|
1738
|
-
/**
|
|
1739
|
-
* Records a synchronous channel binding per **MOD-021**: at compose time,
|
|
1740
|
-
* the instance-side renamed channel transition is merged with
|
|
1741
|
-
* `callerTransition` into a single transition in the resulting flat net.
|
|
1742
|
-
*
|
|
1743
|
-
* **Status**: channel composition is implemented in task #13. Recording a
|
|
1744
|
-
* channel binding here is permitted, but `compose(...)` currently raises
|
|
1745
|
-
* an `Error` when any channel binding is present.
|
|
1746
|
-
*
|
|
1747
|
-
* @throws when `channelName` is already bound on this builder
|
|
1748
|
-
*/
|
|
1749
|
-
bindChannel(channelName, callerTransition) {
|
|
1750
|
-
if (this._channelBindings.has(channelName)) {
|
|
1751
|
-
throw new Error(`Channel '${channelName}' is already bound`);
|
|
1752
|
-
}
|
|
1753
|
-
this._channelBindings.set(channelName, callerTransition);
|
|
1754
|
-
return this;
|
|
1755
|
-
}
|
|
1756
|
-
/** Returns an unmodifiable view of the recorded port bindings. */
|
|
1757
|
-
portBindings() {
|
|
1758
|
-
return this._portBindings;
|
|
1759
|
-
}
|
|
1760
|
-
/** Returns an unmodifiable view of the recorded channel bindings. */
|
|
1761
|
-
channelBindings() {
|
|
1762
|
-
return this._channelBindings;
|
|
1763
|
-
}
|
|
1764
|
-
};
|
|
1765
|
-
function __createComposeBindings() {
|
|
1766
|
-
return new ComposeBindings(COMPOSE_BINDINGS_KEY);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// src/core/fusion-set.ts
|
|
1770
|
-
var FUSION_SET_KEY = /* @__PURE__ */ Symbol("FusionSet.internal");
|
|
1771
|
-
var FusionSet = class _FusionSet {
|
|
1772
|
-
name;
|
|
1773
|
-
members;
|
|
1774
|
-
/** @internal Use {@link FusionSet.builder} or {@link FusionSet.of} to create instances. */
|
|
1775
|
-
constructor(key, name, members) {
|
|
1776
|
-
if (key !== FUSION_SET_KEY) {
|
|
1777
|
-
throw new Error("Use FusionSet.builder() or FusionSet.of() to create instances");
|
|
1778
|
-
}
|
|
1779
|
-
this.name = name;
|
|
1780
|
-
this.members = members;
|
|
1781
|
-
}
|
|
1782
|
-
/**
|
|
1783
|
-
* Returns the canonical member — by convention, the first declared member.
|
|
1784
|
-
* The canonical place's identity survives into the resulting flat net.
|
|
1785
|
-
*/
|
|
1786
|
-
get canonical() {
|
|
1787
|
-
return this.members[0];
|
|
1788
|
-
}
|
|
1789
|
-
/**
|
|
1790
|
-
* Returns all members **except** the canonical member, in declaration order.
|
|
1791
|
-
* These are the places that get substituted away at
|
|
1792
|
-
* {@link import('./petri-net.js').PetriNetBuilder.build} time.
|
|
1793
|
-
*
|
|
1794
|
-
* For a single-member (degenerate) set, returns an empty array.
|
|
1795
|
-
*/
|
|
1796
|
-
nonCanonical() {
|
|
1797
|
-
if (this.members.length <= 1) return [];
|
|
1798
|
-
return this.members.slice(1);
|
|
1799
|
-
}
|
|
1800
|
-
toString() {
|
|
1801
|
-
return `FusionSet[${this.name}, canonical=${this.canonical.name}, members=${this.members.length}]`;
|
|
1802
|
-
}
|
|
1803
|
-
// ============================================================
|
|
1804
|
-
// Static factories
|
|
1805
|
-
// ============================================================
|
|
1806
|
-
/** Returns a fresh {@link FusionSetBuilder} with the given human-readable name. */
|
|
1807
|
-
static builder(name) {
|
|
1808
|
-
return new FusionSetBuilder(name);
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Convenience factory: builds a fusion set whose first member is `first`
|
|
1812
|
-
* (the canonical) and whose remaining members are `rest`, all sharing the
|
|
1813
|
-
* type `<T>`.
|
|
1814
|
-
*
|
|
1815
|
-
* The varargs form ensures static-type homogeneity at the call site (the
|
|
1816
|
-
* TypeScript compiler checks every `rest` entry is a `Place<T>`).
|
|
1817
|
-
*/
|
|
1818
|
-
static of(name, first, ...rest) {
|
|
1819
|
-
const members = [first];
|
|
1820
|
-
for (const p of rest) members.push(p);
|
|
1821
|
-
return new _FusionSet(FUSION_SET_KEY, name, Object.freeze(members));
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
var FusionSetBuilder = class {
|
|
1825
|
-
_name;
|
|
1826
|
-
_members = [];
|
|
1827
|
-
constructor(name) {
|
|
1828
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
1829
|
-
throw new Error("FusionSet.builder: name must be a non-empty string");
|
|
1830
|
-
}
|
|
1831
|
-
this._name = name;
|
|
1832
|
-
}
|
|
1833
|
-
/**
|
|
1834
|
-
* Appends a member to the fusion set. The first member becomes the canonical
|
|
1835
|
-
* place per the convention documented on {@link FusionSet}.
|
|
1836
|
-
*
|
|
1837
|
-
* The `<T>` parameter is for compile-time guidance only: callers writing
|
|
1838
|
-
* typed code at the same call site benefit from the compiler checking
|
|
1839
|
-
* `Place<T>` at each member.
|
|
1840
|
-
*/
|
|
1841
|
-
member(place2) {
|
|
1842
|
-
if (place2 === void 0 || place2 === null) {
|
|
1843
|
-
throw new Error(`FusionSet '${this._name}': member place must not be null`);
|
|
1844
|
-
}
|
|
1845
|
-
this._members.push(place2);
|
|
1846
|
-
return this;
|
|
1847
|
-
}
|
|
1848
|
-
/**
|
|
1849
|
-
* Builds the immutable {@link FusionSet}. Validates that the set has at
|
|
1850
|
-
* least one member; the empty set is rejected as malformed per **MOD-060**.
|
|
1851
|
-
* Single-member sets are accepted as a structurally degenerate no-op.
|
|
1852
|
-
*/
|
|
1853
|
-
build() {
|
|
1854
|
-
if (this._members.length === 0) {
|
|
1855
|
-
throw new Error(
|
|
1856
|
-
`FusionSet '${this._name}': must have at least one member (MOD-060)`
|
|
1857
|
-
);
|
|
1858
|
-
}
|
|
1859
|
-
return new FusionSet(FUSION_SET_KEY, this._name, Object.freeze(this._members.slice()));
|
|
1860
|
-
}
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
// src/core/petri-net.ts
|
|
1864
|
-
var PETRI_NET_KEY = /* @__PURE__ */ Symbol("PetriNet.internal");
|
|
1865
|
-
var PetriNet = class _PetriNet {
|
|
1866
|
-
name;
|
|
1867
|
-
places;
|
|
1868
|
-
transitions;
|
|
1869
|
-
/**
|
|
1870
|
-
* Subnet-membership metadata per **MOD-026**: maps each node name (place or
|
|
1871
|
-
* transition) contributed by exactly one directly-composed subnet to that
|
|
1872
|
-
* subnet's name. Shared places contributed by two or more subnets, and every
|
|
1873
|
-
* node of a net not built via {@link PetriNetBuilder.compose}`(SubnetDef)`,
|
|
1874
|
-
* are absent. Never null — an empty map when there is no metadata. The DOT
|
|
1875
|
-
* exporter renders `subgraph cluster_*` blocks from this map.
|
|
1876
|
-
*
|
|
1877
|
-
* V8 `Map` is insertion-ordered, preserving compose order so cluster
|
|
1878
|
-
* subgraphs render deterministically (cross-language byte-parity).
|
|
1879
|
-
*/
|
|
1880
|
-
subnetMembership;
|
|
1881
|
-
/** @internal Use {@link PetriNet.builder} to create instances. */
|
|
1882
|
-
constructor(key, name, places, transitions, subnetMembership = /* @__PURE__ */ new Map()) {
|
|
1883
|
-
if (key !== PETRI_NET_KEY) throw new Error("Use PetriNet.builder() to create instances");
|
|
1884
|
-
this.name = name;
|
|
1885
|
-
this.places = places;
|
|
1886
|
-
this.transitions = transitions;
|
|
1887
|
-
this.subnetMembership = subnetMembership;
|
|
1888
|
-
}
|
|
1889
|
-
/**
|
|
1890
|
-
* Creates a new PetriNet with actions bound to transitions by name.
|
|
1891
|
-
* Unbound transitions keep passthrough action.
|
|
1892
|
-
*/
|
|
1893
|
-
bindActions(actionBindings) {
|
|
1894
|
-
const bindings = actionBindings instanceof Map ? actionBindings : new Map(Object.entries(actionBindings));
|
|
1895
|
-
return this.bindActionsWithResolver(
|
|
1896
|
-
(name) => bindings.get(name) ?? passthrough()
|
|
1897
|
-
);
|
|
1898
|
-
}
|
|
1899
|
-
/**
|
|
1900
|
-
* Creates a new PetriNet with actions bound via a resolver function.
|
|
1901
|
-
*
|
|
1902
|
-
* The resolver is called once per transition with its name. Returning `null`
|
|
1903
|
-
* defers that transition — it keeps whatever action it already carries — which
|
|
1904
|
-
* is what makes staged binding (**MOD-024** AC7) work.
|
|
1905
|
-
*/
|
|
1906
|
-
bindActionsWithResolver(actionResolver) {
|
|
1907
|
-
const boundTransitions = /* @__PURE__ */ new Set();
|
|
1908
|
-
for (const t of this.transitions) {
|
|
1909
|
-
const action = actionResolver(t.name);
|
|
1910
|
-
if (action !== null && action !== t.action) {
|
|
1911
|
-
boundTransitions.add(rebuildWithAction(t, action));
|
|
1912
|
-
} else {
|
|
1913
|
-
boundTransitions.add(t);
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
return new _PetriNet(
|
|
1917
|
-
PETRI_NET_KEY,
|
|
1918
|
-
this.name,
|
|
1919
|
-
this.places,
|
|
1920
|
-
boundTransitions,
|
|
1921
|
-
this.subnetMembership
|
|
1922
|
-
);
|
|
1923
|
-
}
|
|
1924
|
-
static builder(name) {
|
|
1925
|
-
return new PetriNetBuilder(name);
|
|
1926
|
-
}
|
|
1927
|
-
};
|
|
1928
|
-
var PetriNetBuilder = class _PetriNetBuilder {
|
|
1929
|
-
_name;
|
|
1930
|
-
_places = /* @__PURE__ */ new Set();
|
|
1931
|
-
_transitions = /* @__PURE__ */ new Set();
|
|
1932
|
-
_fusionSets = [];
|
|
1933
|
-
// MOD-026: node name -> set of subnet names that contributed it via
|
|
1934
|
-
// compose(SubnetDef), in first-contribution order. Resolved to single-owner
|
|
1935
|
-
// membership at build(). V8 Map/Set iterate in insertion order, preserving
|
|
1936
|
-
// compose order for cross-language byte-parity.
|
|
1937
|
-
_subnetContributions = /* @__PURE__ */ new Map();
|
|
1938
|
-
constructor(name) {
|
|
1939
|
-
this._name = name;
|
|
1940
|
-
}
|
|
1941
|
-
/** Add an explicit place. */
|
|
1942
|
-
place(place2) {
|
|
1943
|
-
this._places.add(place2);
|
|
1944
|
-
return this;
|
|
1945
|
-
}
|
|
1946
|
-
/** Add explicit places. */
|
|
1947
|
-
places(...places) {
|
|
1948
|
-
for (const p of places) this._places.add(p);
|
|
1949
|
-
return this;
|
|
1950
|
-
}
|
|
1951
|
-
/** Add a transition (auto-collects places from arcs). */
|
|
1952
|
-
transition(transition) {
|
|
1953
|
-
this._transitions.add(transition);
|
|
1954
|
-
for (const spec of transition.inputSpecs) {
|
|
1955
|
-
this._places.add(spec.place);
|
|
1956
|
-
}
|
|
1957
|
-
for (const p of transition.outputPlaces()) {
|
|
1958
|
-
this._places.add(p);
|
|
1959
|
-
}
|
|
1960
|
-
for (const inh of transition.inhibitors) {
|
|
1961
|
-
this._places.add(inh.place);
|
|
1962
|
-
}
|
|
1963
|
-
for (const r of transition.reads) {
|
|
1964
|
-
this._places.add(r.place);
|
|
1965
|
-
}
|
|
1966
|
-
for (const r of transition.resets) {
|
|
1967
|
-
this._places.add(r.place);
|
|
1968
|
-
}
|
|
1969
|
-
return this;
|
|
1970
|
-
}
|
|
1971
|
-
/** Add transitions (auto-collects places from arcs). */
|
|
1972
|
-
transitions(...transitions) {
|
|
1973
|
-
for (const t of transitions) this.transition(t);
|
|
1974
|
-
return this;
|
|
1975
|
-
}
|
|
1976
|
-
compose(instanceOrDef, arg) {
|
|
1977
|
-
if (instanceOrDef instanceof SubnetDef) {
|
|
1978
|
-
return this.composeDirect(instanceOrDef);
|
|
1979
|
-
}
|
|
1980
|
-
const instance = instanceOrDef;
|
|
1981
|
-
if (arg === void 0) {
|
|
1982
|
-
return this.composeAuto(instance);
|
|
1983
|
-
}
|
|
1984
|
-
if (typeof arg === "function") {
|
|
1985
|
-
const bindings = __createComposeBindings();
|
|
1986
|
-
arg(bindings);
|
|
1987
|
-
return this.composeInternal(instance, bindings.portBindings(), bindings.channelBindings());
|
|
1988
|
-
}
|
|
1989
|
-
const portMappings = arg instanceof Map ? arg : new Map(Object.entries(arg));
|
|
1990
|
-
return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
|
|
240
|
+
/** Returns declared input places (consumed). */
|
|
241
|
+
inputPlaces() {
|
|
242
|
+
return this._inputPlaces;
|
|
1991
243
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
* **MOD-025** — **without instantiation**, and without the prefix-renaming
|
|
1995
|
-
* of {@link SubnetDef.instantiate}.
|
|
1996
|
-
*
|
|
1997
|
-
* Every body place and transition is added under its **original**
|
|
1998
|
-
* (un-prefixed) name. Places merge into this builder by name: a body place
|
|
1999
|
-
* whose name equals an enclosing-net place *is* that place in the composed
|
|
2000
|
-
* flat net. This is the mode for wiring a subnet in as a single shared copy.
|
|
2001
|
-
*
|
|
2002
|
-
* Direct composition is **order-independent**: composing the same set of
|
|
2003
|
-
* subnets in any order yields the same flat net, because merging is by
|
|
2004
|
-
* place name and not by a probe of the builder's place set at call time —
|
|
2005
|
-
* contrast the no-interface body-inference branch of {@link composeAuto}
|
|
2006
|
-
* (MOD-024), which is order-sensitive.
|
|
2007
|
-
*
|
|
2008
|
-
* For multiple *independent* copies — each with isolated per-instance state
|
|
2009
|
-
* per [MOD-012] — use {@link SubnetDef.instantiate} + the `compose(instance)`
|
|
2010
|
-
* overload instead.
|
|
2011
|
-
*
|
|
2012
|
-
* Rejections: a body transition whose name already exists in this builder
|
|
2013
|
-
* (use `instantiate(prefix)` for independent copies); a subnet whose
|
|
2014
|
-
* interface declares any channel (direct composition does not bind
|
|
2015
|
-
* channels — use `instantiate` + the channel-binding `compose` overload).
|
|
2016
|
-
*
|
|
2017
|
-
* Token-type conflicts on a same-named place cannot be detected: TS
|
|
2018
|
-
* {@link Place} equality is name-only at runtime (the documented carve-out,
|
|
2019
|
-
* same as MOD-024).
|
|
2020
|
-
*
|
|
2021
|
-
* @throws when the subnet declares channels, or a body transition name
|
|
2022
|
-
* collides with a transition already in this builder.
|
|
2023
|
-
*/
|
|
2024
|
-
composeDirect(def) {
|
|
2025
|
-
const iface = def.iface;
|
|
2026
|
-
if (iface.channels.size > 0) {
|
|
2027
|
-
const channelNames = [];
|
|
2028
|
-
for (const c of iface.channels.values()) channelNames.push(c.name);
|
|
2029
|
-
channelNames.sort();
|
|
244
|
+
requireInput(place2) {
|
|
245
|
+
if (!this.allowedInputs.has(place2.name)) {
|
|
2030
246
|
throw new Error(
|
|
2031
|
-
`
|
|
247
|
+
`Place '${place2.name}' not in declared inputs: [${[...this.allowedInputs].join(", ")}]`
|
|
2032
248
|
);
|
|
2033
249
|
}
|
|
2034
|
-
const body = def.body;
|
|
2035
|
-
const hostTransitionNames = /* @__PURE__ */ new Set();
|
|
2036
|
-
for (const t of this._transitions) hostTransitionNames.add(t.name);
|
|
2037
|
-
for (const t of body.transitions) {
|
|
2038
|
-
if (hostTransitionNames.has(t.name)) {
|
|
2039
|
-
throw new Error(
|
|
2040
|
-
`compose(SubnetDef): transition '${t.name}' from subnet '${def.name}' collides with a transition already in net '${this._name}'. Direct composition merges by name; for independent copies use def.instantiate(prefix) + compose(instance).`
|
|
2041
|
-
);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
const hostByName = /* @__PURE__ */ new Map();
|
|
2045
|
-
for (const p of this._places) hostByName.set(p.name, p);
|
|
2046
|
-
const subnetName = def.name.replace(/\//g, "_");
|
|
2047
|
-
const mergeMap = /* @__PURE__ */ new Map();
|
|
2048
|
-
for (const p of body.places) {
|
|
2049
|
-
const host = hostByName.get(p.name);
|
|
2050
|
-
this.place(host ?? p);
|
|
2051
|
-
if (host !== void 0) mergeMap.set(p.name, host);
|
|
2052
|
-
this.recordContribution(p.name, subnetName);
|
|
2053
|
-
}
|
|
2054
|
-
for (const t of body.transitions) {
|
|
2055
|
-
this.transition(substitutePlaces(t, mergeMap));
|
|
2056
|
-
this.recordContribution(t.name, subnetName);
|
|
2057
|
-
}
|
|
2058
|
-
return this;
|
|
2059
250
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
}
|
|
2067
|
-
owners.add(subnetName);
|
|
251
|
+
// ==================== Read Access (not consumed) ====================
|
|
252
|
+
/** Get read-only context value. Throws if place not declared as read. */
|
|
253
|
+
read(place2) {
|
|
254
|
+
const actual = this.resolve(place2);
|
|
255
|
+
this.requireRead(actual);
|
|
256
|
+
return this.rawInput.value(actual);
|
|
2068
257
|
}
|
|
2069
|
-
/**
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
* `spec/11-modular-composition.md` MOD-024). Body places that don't match
|
|
2082
|
-
* stay private under their prefixed names per [MOD-010].
|
|
2083
|
-
*
|
|
2084
|
-
* Channels are NOT auto-bound — transition identity is too delicate for
|
|
2085
|
-
* inference. If the subnet declares any channel, this overload throws.
|
|
2086
|
-
*/
|
|
2087
|
-
composeAuto(instance) {
|
|
2088
|
-
const iface = instance.def.iface;
|
|
2089
|
-
if (iface.channels.size > 0) {
|
|
2090
|
-
const channelNames = [];
|
|
2091
|
-
for (const c of iface.channels.values()) channelNames.push(c.name);
|
|
2092
|
-
channelNames.sort();
|
|
258
|
+
/** Get all read-only context values for a place. */
|
|
259
|
+
reads(place2) {
|
|
260
|
+
const actual = this.resolve(place2);
|
|
261
|
+
this.requireRead(actual);
|
|
262
|
+
return this.rawInput.values(actual);
|
|
263
|
+
}
|
|
264
|
+
/** Returns declared read places (context, not consumed). */
|
|
265
|
+
readPlaces() {
|
|
266
|
+
return this._readPlaces;
|
|
267
|
+
}
|
|
268
|
+
requireRead(place2) {
|
|
269
|
+
if (!this.allowedReads.has(place2.name)) {
|
|
2093
270
|
throw new Error(
|
|
2094
|
-
`
|
|
271
|
+
`Place '${place2.name}' not in declared reads: [${[...this.allowedReads].join(", ")}]`
|
|
2095
272
|
);
|
|
2096
273
|
}
|
|
2097
|
-
const hostByName = /* @__PURE__ */ new Map();
|
|
2098
|
-
for (const p of this._places) hostByName.set(p.name, p);
|
|
2099
|
-
if (iface.ports.size > 0) {
|
|
2100
|
-
const portMappings = /* @__PURE__ */ new Map();
|
|
2101
|
-
for (const port of iface.ports.values()) {
|
|
2102
|
-
const hostMatch = hostByName.get(port.place.name);
|
|
2103
|
-
portMappings.set(port.name, hostMatch ?? port.place);
|
|
2104
|
-
}
|
|
2105
|
-
return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
|
|
2106
|
-
}
|
|
2107
|
-
const mergeMap = /* @__PURE__ */ new Map();
|
|
2108
|
-
const prefix = instance.prefix + "/";
|
|
2109
|
-
for (const renamed of instance.renamedBody.places) {
|
|
2110
|
-
if (!renamed.name.startsWith(prefix)) continue;
|
|
2111
|
-
const originalName = renamed.name.substring(prefix.length);
|
|
2112
|
-
const hostMatch = hostByName.get(originalName);
|
|
2113
|
-
if (hostMatch !== void 0) {
|
|
2114
|
-
mergeMap.set(renamed.name, hostMatch);
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
return this.applyComposition(instance, mergeMap, /* @__PURE__ */ new Map());
|
|
2118
274
|
}
|
|
275
|
+
// ==================== Output Access ====================
|
|
2119
276
|
/**
|
|
2120
|
-
*
|
|
2121
|
-
* bindings, builds the place-substitution map, walks every renamed-body
|
|
2122
|
-
* transition through {@link substitutePlaces}, applies channel merges per
|
|
2123
|
-
* [MOD-021], and adds the resulting transitions to this builder.
|
|
2124
|
-
*
|
|
2125
|
-
* ## Channel-merge flow ([MOD-021])
|
|
277
|
+
* Add one or more output values to the same place in a single call.
|
|
2126
278
|
*
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
* added to the builder's transition set).
|
|
2130
|
-
* 2. For each channel binding, resolve the renamed instance-side
|
|
2131
|
-
* transition through `instance.channel(channelName)`, then look up its
|
|
2132
|
-
* rewritten counterpart in the working map by name.
|
|
2133
|
-
* 3. Replace the working-map entry with a {@link mergeTransitions} result
|
|
2134
|
-
* that fuses caller-side + instance-side; remove the rewritten
|
|
2135
|
-
* instance-side entry. Also replace (or add) the caller-side
|
|
2136
|
-
* transition in this builder's transition set with the same merged
|
|
2137
|
-
* result, indexed under the caller's name slot.
|
|
2138
|
-
* 4. Add the surviving (un-merged) entries to this builder.
|
|
279
|
+
* Validates the place once, then appends each value to the output
|
|
280
|
+
* collector. Calling with zero values is a no-op.
|
|
2139
281
|
*
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
2143
|
-
* Collecting first and merging second keeps the builder's transition set
|
|
2144
|
-
* finalized exactly once.
|
|
282
|
+
* @example
|
|
283
|
+
* ctx.output(outPlace, 'a', 'b', 'c');
|
|
284
|
+
* ctx.output(outPlace, ...someArray);
|
|
2145
285
|
*
|
|
2146
|
-
*
|
|
2147
|
-
* Transition reference) is also robust against a prior
|
|
2148
|
-
* {@link Instance.bindActions} call that may have rebuilt the renamed-body
|
|
2149
|
-
* transitions, breaking identity equality between the body and the
|
|
2150
|
-
* channel-handle map — but the prefixed names remain stable.
|
|
286
|
+
* @throws if place not declared as output.
|
|
2151
287
|
*/
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
for (const
|
|
2156
|
-
|
|
2157
|
-
if (port === void 0) {
|
|
2158
|
-
const knownPorts = [];
|
|
2159
|
-
for (const p of iface.ports.values()) knownPorts.push(p.name);
|
|
2160
|
-
throw new Error(
|
|
2161
|
-
`compose: no port named '${portName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known ports: [${knownPorts.join(", ")}]`
|
|
2162
|
-
);
|
|
2163
|
-
}
|
|
2164
|
-
const ifacePlace = instance.port(portName);
|
|
2165
|
-
mergeMap.set(ifacePlace.name, callerPlace);
|
|
288
|
+
output(place2, ...values) {
|
|
289
|
+
const actual = this.resolve(place2);
|
|
290
|
+
this.requireOutput(actual);
|
|
291
|
+
for (const value of values) {
|
|
292
|
+
this.writeTarget.add(actual, value);
|
|
2166
293
|
}
|
|
2167
|
-
return this
|
|
294
|
+
return this;
|
|
2168
295
|
}
|
|
2169
296
|
/**
|
|
2170
|
-
*
|
|
2171
|
-
* through `mergeMap`, applies channel merges per **MOD-021**, and adds
|
|
2172
|
-
* the surviving transitions to the builder.
|
|
297
|
+
* Add one or more pre-built output tokens to the same place in a single call.
|
|
2173
298
|
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2176
|
-
*
|
|
299
|
+
* Validates the place once, then appends each token. Calling with zero
|
|
300
|
+
* tokens is a no-op.
|
|
301
|
+
*
|
|
302
|
+
* @throws if place not declared as output.
|
|
2177
303
|
*/
|
|
2178
|
-
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2181
|
-
for (const
|
|
2182
|
-
|
|
2183
|
-
}
|
|
2184
|
-
for (const [channelName, callerTrans] of channelBindings) {
|
|
2185
|
-
let instanceRenamedChannel;
|
|
2186
|
-
try {
|
|
2187
|
-
instanceRenamedChannel = instance.channel(channelName);
|
|
2188
|
-
} catch (cause) {
|
|
2189
|
-
const knownChannels = [];
|
|
2190
|
-
for (const c of iface.channels.values()) knownChannels.push(c.name);
|
|
2191
|
-
const err = new Error(
|
|
2192
|
-
`compose: no channel named '${channelName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known channels: [${knownChannels.join(", ")}]`
|
|
2193
|
-
);
|
|
2194
|
-
err.cause = cause;
|
|
2195
|
-
throw err;
|
|
2196
|
-
}
|
|
2197
|
-
const rewrittenInstanceChannel = rewrittenByName.get(instanceRenamedChannel.name);
|
|
2198
|
-
if (rewrittenInstanceChannel === void 0) {
|
|
2199
|
-
throw new Error(
|
|
2200
|
-
`compose: channel '${channelName}' resolved to a transition '${instanceRenamedChannel.name}' that is not present in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
2201
|
-
);
|
|
2202
|
-
}
|
|
2203
|
-
const merged = mergeTransitions(callerTrans, rewrittenInstanceChannel, callerTrans.name);
|
|
2204
|
-
rewrittenByName.delete(instanceRenamedChannel.name);
|
|
2205
|
-
this._transitions.delete(callerTrans);
|
|
2206
|
-
this.transition(merged);
|
|
2207
|
-
}
|
|
2208
|
-
for (const rewritten of rewrittenByName.values()) {
|
|
2209
|
-
this.transition(rewritten);
|
|
304
|
+
outputToken(place2, ...tokens) {
|
|
305
|
+
const actual = this.resolve(place2);
|
|
306
|
+
this.requireOutput(actual);
|
|
307
|
+
for (const token of tokens) {
|
|
308
|
+
this.writeTarget.addToken(actual, token);
|
|
2210
309
|
}
|
|
2211
310
|
return this;
|
|
2212
311
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
if (s === void 0 || s === null) {
|
|
2223
|
-
throw new Error("fuse: fusion set must not be null");
|
|
2224
|
-
}
|
|
2225
|
-
this._fusionSets.push(s);
|
|
312
|
+
/** Returns declared output places. */
|
|
313
|
+
outputPlaces() {
|
|
314
|
+
return this._outputPlaces;
|
|
315
|
+
}
|
|
316
|
+
requireOutput(place2) {
|
|
317
|
+
if (!this.allowedOutputs.has(place2.name)) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Place '${place2.name}' not in declared outputs: [${[...this.allowedOutputs].join(", ")}]`
|
|
320
|
+
);
|
|
2226
321
|
}
|
|
2227
|
-
return this;
|
|
2228
322
|
}
|
|
2229
323
|
/**
|
|
2230
|
-
*
|
|
2231
|
-
* **MOD-061**) AFTER all transition/composition accumulation:
|
|
324
|
+
* Severs the action's output from the marking, for use when a firing times out.
|
|
2232
325
|
*
|
|
2233
|
-
*
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
*
|
|
2237
|
-
* Map<string, Place<unknown>> convention — TypeScript Place identity is
|
|
2238
|
-
* name-based per `runtime/compiled-net.ts`).
|
|
2239
|
-
* 3. Walk every transition through {@link applyFusion} to rewrite arc place
|
|
2240
|
-
* references; input arcs colliding on a canonical place merge per
|
|
2241
|
-
* [MOD-021] (additive where summable, rejected otherwise).
|
|
2242
|
-
* 4. Re-derive the place set from the rewritten transitions plus any
|
|
2243
|
-
* caller-declared standalone places, dropping non-canonical members.
|
|
2244
|
-
* Caller-declared standalone places that happen to be non-canonical
|
|
2245
|
-
* members are also dropped.
|
|
326
|
+
* After this call the action's `output(...)` writes are dropped, and the executor
|
|
327
|
+
* harvests a fresh collector the action holds no reference to. Anything the action
|
|
328
|
+
* wrote *before* the timeout is discarded with it: a partial result merged with the
|
|
329
|
+
* timeout branch would violate the transition's own output spec.
|
|
2246
330
|
*
|
|
2247
|
-
*
|
|
2248
|
-
*
|
|
2249
|
-
* unused.
|
|
331
|
+
* This is the only isolation available — libpetri does not own the promise the action
|
|
332
|
+
* runs on, so it cannot stop the work, only stop the result from landing.
|
|
2250
333
|
*
|
|
2251
|
-
* @
|
|
334
|
+
* @internal Executor machinery — must be called by the executor, never by an action.
|
|
2252
335
|
*/
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
return new PetriNet(
|
|
2257
|
-
PETRI_NET_KEY,
|
|
2258
|
-
this._name,
|
|
2259
|
-
this._places,
|
|
2260
|
-
this._transitions,
|
|
2261
|
-
membership
|
|
2262
|
-
);
|
|
2263
|
-
}
|
|
2264
|
-
return this.buildWithFusion(membership);
|
|
336
|
+
detachForTimeout() {
|
|
337
|
+
this.writeTarget.detach();
|
|
338
|
+
this._rawOutput = new TokenOutput();
|
|
2265
339
|
}
|
|
2266
340
|
/**
|
|
2267
|
-
*
|
|
2268
|
-
*
|
|
2269
|
-
*
|
|
2270
|
-
*
|
|
2271
|
-
*
|
|
2272
|
-
*
|
|
341
|
+
* Produces into the executor's harvest collector rather than the action's write target.
|
|
342
|
+
*
|
|
343
|
+
* Identical to {@link output} before {@link detachForTimeout}; after it, this is the
|
|
344
|
+
* only route that still reaches the marking. Used by the executor to deposit the
|
|
345
|
+
* timeout branch.
|
|
346
|
+
*
|
|
347
|
+
* @internal Executor machinery — must be called by the executor, never by an action.
|
|
2273
348
|
*/
|
|
2274
|
-
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
return resolved;
|
|
349
|
+
outputToHarvest(place2, value) {
|
|
350
|
+
const actual = this.resolve(place2);
|
|
351
|
+
this.requireOutput(actual);
|
|
352
|
+
this._rawOutput.add(actual, value);
|
|
353
|
+
return this;
|
|
2282
354
|
}
|
|
355
|
+
// ==================== ν-name minting (NU-010) ====================
|
|
2283
356
|
/**
|
|
2284
|
-
* @internal
|
|
2285
|
-
*
|
|
2286
|
-
*
|
|
2287
|
-
* keeps its own entry. Per **MOD-026**.
|
|
357
|
+
* @internal Installs the ν-name minter. Wired by the executor at firing time
|
|
358
|
+
* so names minted by {@link freshName} are monotonic across the run and
|
|
359
|
+
* instance-prefixed (spec NU-010, NU-030).
|
|
2288
360
|
*/
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
return membership;
|
|
2292
|
-
}
|
|
2293
|
-
const filtered = /* @__PURE__ */ new Map();
|
|
2294
|
-
for (const [key, value] of membership) {
|
|
2295
|
-
if (!nonCanonicalNames.has(key)) {
|
|
2296
|
-
filtered.set(key, value);
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
return filtered;
|
|
361
|
+
setFreshNameSupplier(supplier) {
|
|
362
|
+
this._freshNameSupplier = supplier;
|
|
2300
363
|
}
|
|
2301
364
|
/**
|
|
2302
|
-
*
|
|
2303
|
-
*
|
|
365
|
+
* Mints a fresh ν-name (the ν-binder primitive — spec NU-010).
|
|
366
|
+
*
|
|
367
|
+
* An action calls this on the fork side to create a correlation id, then
|
|
368
|
+
* writes it into the sibling output payloads; a later join correlates those
|
|
369
|
+
* siblings via a {@link import('./match-spec.js').MatchSpec}. Uses the
|
|
370
|
+
* executor-installed minter when present; otherwise falls back to a
|
|
371
|
+
* process-global counter prefixed by the transition name.
|
|
2304
372
|
*/
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
for (const p of this._places) {
|
|
2333
|
-
if (!nonCanonicalNames.has(p.name)) {
|
|
2334
|
-
rebuiltPlaces.add(p);
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
for (const t of rewrittenTransitions) {
|
|
2338
|
-
for (const spec of t.inputSpecs) rebuiltPlaces.add(spec.place);
|
|
2339
|
-
for (const p of t.outputPlaces()) rebuiltPlaces.add(p);
|
|
2340
|
-
for (const inh of t.inhibitors) rebuiltPlaces.add(inh.place);
|
|
2341
|
-
for (const r of t.reads) rebuiltPlaces.add(r.place);
|
|
2342
|
-
for (const r of t.resets) rebuiltPlaces.add(r.place);
|
|
2343
|
-
}
|
|
2344
|
-
return new PetriNet(
|
|
2345
|
-
PETRI_NET_KEY,
|
|
2346
|
-
this._name,
|
|
2347
|
-
rebuiltPlaces,
|
|
2348
|
-
rewrittenTransitions,
|
|
2349
|
-
_PetriNetBuilder.filterFusedMembership(membership, nonCanonicalNames)
|
|
2350
|
-
);
|
|
373
|
+
freshName() {
|
|
374
|
+
if (this._freshNameSupplier) return this._freshNameSupplier();
|
|
375
|
+
return nameId(`${this._transitionName}#${GLOBAL_FRESH_NAME_COUNTER++}`);
|
|
376
|
+
}
|
|
377
|
+
// ==================== Structure Info ====================
|
|
378
|
+
/** Returns the transition name. */
|
|
379
|
+
transitionName() {
|
|
380
|
+
return this._transitionName;
|
|
381
|
+
}
|
|
382
|
+
// ==================== Execution Context ====================
|
|
383
|
+
/** Retrieves an execution context object by key. */
|
|
384
|
+
executionContext(key) {
|
|
385
|
+
return this.executionCtx.get(key);
|
|
386
|
+
}
|
|
387
|
+
/** Checks if an execution context object of the given key is present. */
|
|
388
|
+
hasExecutionContext(key) {
|
|
389
|
+
return this.executionCtx.has(key);
|
|
390
|
+
}
|
|
391
|
+
// ==================== Logging ====================
|
|
392
|
+
/** Emits a structured log message into the event store. */
|
|
393
|
+
log(level, message, error) {
|
|
394
|
+
this._logFn?.(level, message, error);
|
|
395
|
+
}
|
|
396
|
+
// ==================== Internal ====================
|
|
397
|
+
/** @internal Used by BitmapNetExecutor to collect outputs after action completion. */
|
|
398
|
+
rawOutput() {
|
|
399
|
+
return this._rawOutput;
|
|
2351
400
|
}
|
|
2352
401
|
};
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
402
|
+
|
|
403
|
+
// src/core/token-input.ts
|
|
404
|
+
var TokenInput = class {
|
|
405
|
+
tokens = /* @__PURE__ */ new Map();
|
|
406
|
+
/** Add a token (used by executor when firing transition). */
|
|
407
|
+
add(place2, token) {
|
|
408
|
+
const existing = this.tokens.get(place2.name);
|
|
409
|
+
if (existing) {
|
|
410
|
+
existing.push(token);
|
|
411
|
+
} else {
|
|
412
|
+
this.tokens.set(place2.name, [token]);
|
|
413
|
+
}
|
|
414
|
+
return this;
|
|
2357
415
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
416
|
+
/** Get all tokens for a place. */
|
|
417
|
+
getAll(place2) {
|
|
418
|
+
return this.tokens.get(place2.name) ?? [];
|
|
2360
419
|
}
|
|
2361
|
-
|
|
2362
|
-
|
|
420
|
+
/** Get the first token for a place. Throws if no tokens. */
|
|
421
|
+
get(place2) {
|
|
422
|
+
const list = this.tokens.get(place2.name);
|
|
423
|
+
if (!list || list.length === 0) {
|
|
424
|
+
throw new Error(`No token for place: ${place2.name}`);
|
|
425
|
+
}
|
|
426
|
+
return list[0];
|
|
2363
427
|
}
|
|
2364
|
-
for
|
|
2365
|
-
|
|
428
|
+
/** Get the first token's value for a place. Throws if no tokens. */
|
|
429
|
+
value(place2) {
|
|
430
|
+
return this.get(place2).value;
|
|
2366
431
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
432
|
+
/** Get all token values for a place. */
|
|
433
|
+
values(place2) {
|
|
434
|
+
return this.getAll(place2).map((t) => t.value);
|
|
2369
435
|
}
|
|
2370
|
-
|
|
2371
|
-
|
|
436
|
+
/** Get token count for a place. */
|
|
437
|
+
count(place2) {
|
|
438
|
+
return this.getAll(place2).length;
|
|
2372
439
|
}
|
|
2373
|
-
if
|
|
2374
|
-
|
|
440
|
+
/** Check if any tokens exist for a place. */
|
|
441
|
+
has(place2) {
|
|
442
|
+
return this.count(place2) > 0;
|
|
2375
443
|
}
|
|
2376
|
-
|
|
2377
|
-
}
|
|
444
|
+
};
|
|
2378
445
|
|
|
2379
446
|
// src/core/subnet.ts
|
|
2380
447
|
function closedSubnet(net) {
|
|
@@ -2686,6 +753,35 @@ var CompiledNet = class _CompiledNet {
|
|
|
2686
753
|
return true;
|
|
2687
754
|
}
|
|
2688
755
|
};
|
|
756
|
+
function restartThresholds(compiled) {
|
|
757
|
+
const thresholds = new Float64Array(compiled.placeCount);
|
|
758
|
+
const requirer = new Int32Array(compiled.placeCount).fill(-1);
|
|
759
|
+
const drained = new Uint8Array(compiled.placeCount);
|
|
760
|
+
for (let tid = 0; tid < compiled.transitionCount; tid++) {
|
|
761
|
+
const t = compiled.transition(tid);
|
|
762
|
+
const requires = (pid) => {
|
|
763
|
+
requirer[pid] = requirer[pid] === -1 || requirer[pid] === tid ? tid : -2;
|
|
764
|
+
};
|
|
765
|
+
for (const spec of t.inputSpecs) {
|
|
766
|
+
const pid = compiled.placeId(spec.place);
|
|
767
|
+
thresholds[pid] = Math.max(thresholds[pid], requiredCount(spec));
|
|
768
|
+
requires(pid);
|
|
769
|
+
}
|
|
770
|
+
for (const arc of t.reads) {
|
|
771
|
+
const pid = compiled.placeId(arc.place);
|
|
772
|
+
thresholds[pid] = Math.max(thresholds[pid], 1);
|
|
773
|
+
requires(pid);
|
|
774
|
+
}
|
|
775
|
+
for (const arc of t.resets) drained[compiled.placeId(arc.place)] = 1;
|
|
776
|
+
if (t.matchSpec !== null) {
|
|
777
|
+
for (const key of t.matchSpec.keys) thresholds[compiled.placeId(key.place)] = Infinity;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
for (let pid = 0; pid < compiled.placeCount; pid++) {
|
|
781
|
+
if (requirer[pid] !== -2 && !drained[pid]) thresholds[pid] = 0;
|
|
782
|
+
}
|
|
783
|
+
return thresholds;
|
|
784
|
+
}
|
|
2689
785
|
function setBit(arr, bit) {
|
|
2690
786
|
arr[bit >>> WORD_SHIFT] |= 1 << (bit & BIT_MASK);
|
|
2691
787
|
}
|
|
@@ -3246,14 +1342,23 @@ var BitmapNetExecutor = class {
|
|
|
3246
1342
|
wakeUpResolve = null;
|
|
3247
1343
|
// Pre-allocated buffer for fireReadyTransitions() to avoid per-cycle allocation
|
|
3248
1344
|
readyBuffer = [];
|
|
3249
|
-
|
|
3250
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* Per transition, one bit: a firing left it disabled in the intermediate marking while it
|
|
1347
|
+
* was marked enabled (TIME-012), so the next dirty scan that finds it enabled restarts its
|
|
1348
|
+
* clock. Set at fire time, cleared at the end of every scan; `anyRestartPending` lets the
|
|
1349
|
+
* scan skip the clear when nothing was flagged.
|
|
1350
|
+
*/
|
|
1351
|
+
restartPendingWords;
|
|
1352
|
+
anyRestartPending = false;
|
|
1353
|
+
/** Per place, the count a firing must leave it at to have disabled nothing through it. */
|
|
1354
|
+
restartThresholds;
|
|
3251
1355
|
/**
|
|
3252
1356
|
* Undeclared place names already reported (CORE-072 AC4). Keyed by name — TS
|
|
3253
1357
|
* Place identity is name-based — so a hot loop warns once, not per token.
|
|
3254
1358
|
*/
|
|
3255
1359
|
warnedUnknownPlaces = /* @__PURE__ */ new Set();
|
|
3256
|
-
|
|
1360
|
+
/** Transitions already warned for writing several tokens to a place their spec names once (IO-016 AC4). */
|
|
1361
|
+
warnedMultiplicity = /* @__PURE__ */ new Set();
|
|
3257
1362
|
running = false;
|
|
3258
1363
|
draining = false;
|
|
3259
1364
|
closed = false;
|
|
@@ -3278,6 +1383,8 @@ var BitmapNetExecutor = class {
|
|
|
3278
1383
|
const dirtyWords = this.compiled.transitionCount + BIT_MASK >>> WORD_SHIFT;
|
|
3279
1384
|
this.dirtySet = new Uint32Array(dirtyWords);
|
|
3280
1385
|
this.dirtySnapBuffer = new Uint32Array(dirtyWords);
|
|
1386
|
+
this.restartPendingWords = new Uint32Array(dirtyWords);
|
|
1387
|
+
this.restartThresholds = restartThresholds(this.compiled);
|
|
3281
1388
|
this.enabledAtMs = new Float64Array(this.compiled.transitionCount);
|
|
3282
1389
|
this.enabledAtMs.fill(-Infinity);
|
|
3283
1390
|
this.inFlightFlags = new Uint8Array(this.compiled.transitionCount);
|
|
@@ -3302,12 +1409,6 @@ var BitmapNetExecutor = class {
|
|
|
3302
1409
|
this.allImmediate = allImm;
|
|
3303
1410
|
this.allSamePriority = samePrio;
|
|
3304
1411
|
this.eventStoreEnabled = this.eventStore.isEnabled();
|
|
3305
|
-
this.transitionInputPlaceNames = /* @__PURE__ */ new Map();
|
|
3306
|
-
for (const t of net.transitions) {
|
|
3307
|
-
const names = /* @__PURE__ */ new Set();
|
|
3308
|
-
for (const spec of t.inputSpecs) names.add(spec.place.name);
|
|
3309
|
-
this.transitionInputPlaceNames.set(t, names);
|
|
3310
|
-
}
|
|
3311
1412
|
for (const [place2, tokens] of initialTokens) {
|
|
3312
1413
|
if (tokens.length > 0 && this.compiled.tryPlaceId(place2) === void 0) {
|
|
3313
1414
|
this.warnUnknownPlace(place2, "");
|
|
@@ -3465,11 +1566,11 @@ var BitmapNetExecutor = class {
|
|
|
3465
1566
|
throw new Error(`Place ${envPlace.place.name} is not registered as an environment place`);
|
|
3466
1567
|
}
|
|
3467
1568
|
if (this.closed || this.draining) return false;
|
|
3468
|
-
return new Promise((
|
|
1569
|
+
return new Promise((resolve, reject) => {
|
|
3469
1570
|
this.externalQueue.push({
|
|
3470
1571
|
place: envPlace.place,
|
|
3471
1572
|
token,
|
|
3472
|
-
resolve
|
|
1573
|
+
resolve,
|
|
3473
1574
|
reject
|
|
3474
1575
|
});
|
|
3475
1576
|
this.wakeUp();
|
|
@@ -3542,7 +1643,7 @@ var BitmapNetExecutor = class {
|
|
|
3542
1643
|
this.enabledFlags[tid] = 0;
|
|
3543
1644
|
this.enabledTransitionCount--;
|
|
3544
1645
|
this.enabledAtMs[tid] = -Infinity;
|
|
3545
|
-
} else if (canNow && wasEnabled && this.
|
|
1646
|
+
} else if (canNow && wasEnabled && this.anyRestartPending && (this.restartPendingWords[w] & 1 << bit) !== 0) {
|
|
3546
1647
|
this.enabledAtMs[tid] = nowMs;
|
|
3547
1648
|
this.emitEvent({
|
|
3548
1649
|
type: "transition-clock-restarted",
|
|
@@ -3552,7 +1653,10 @@ var BitmapNetExecutor = class {
|
|
|
3552
1653
|
}
|
|
3553
1654
|
}
|
|
3554
1655
|
}
|
|
3555
|
-
this.
|
|
1656
|
+
if (this.anyRestartPending) {
|
|
1657
|
+
this.restartPendingWords.fill(0);
|
|
1658
|
+
this.anyRestartPending = false;
|
|
1659
|
+
}
|
|
3556
1660
|
}
|
|
3557
1661
|
/**
|
|
3558
1662
|
* Checks all enabled transitions with finite deadlines. If a transition has been
|
|
@@ -3606,14 +1710,26 @@ var BitmapNetExecutor = class {
|
|
|
3606
1710
|
}
|
|
3607
1711
|
return true;
|
|
3608
1712
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
1713
|
+
/**
|
|
1714
|
+
* Flags each other transition that firing `tid` has just disabled through `pid`
|
|
1715
|
+
* (TIME-012). Called from {@link updateBitmapAfterConsumption} on the intermediate marking
|
|
1716
|
+
* M - Pre(t) (inputs consumed and resets drained, outputs not yet deposited), and only for
|
|
1717
|
+
* a place the firing left below its restart threshold. Removing tokens never trips an
|
|
1718
|
+
* inhibitor arc, so the bit tests reject every other kind of neighbour cheaply.
|
|
1719
|
+
*/
|
|
1720
|
+
flagIntermediateDisablements(tid, pid) {
|
|
1721
|
+
const affected = this.compiled.affectedTransitions(pid);
|
|
1722
|
+
for (let j = 0; j < affected.length; j++) {
|
|
1723
|
+
const other = affected[j];
|
|
1724
|
+
if (other === tid || !this.enabledFlags[other] || this.inFlightFlags[other]) continue;
|
|
1725
|
+
const w = other >>> WORD_SHIFT;
|
|
1726
|
+
const mask = 1 << (other & BIT_MASK);
|
|
1727
|
+
if ((this.restartPendingWords[w] & mask) !== 0) continue;
|
|
1728
|
+
if (!this.canEnable(other, this.markingBitmap)) {
|
|
1729
|
+
this.restartPendingWords[w] |= mask;
|
|
1730
|
+
this.anyRestartPending = true;
|
|
1731
|
+
}
|
|
3615
1732
|
}
|
|
3616
|
-
return false;
|
|
3617
1733
|
}
|
|
3618
1734
|
// ======================== Firing ========================
|
|
3619
1735
|
fireReadyTransitions(nowMs) {
|
|
@@ -3777,7 +1893,6 @@ var BitmapNetExecutor = class {
|
|
|
3777
1893
|
}
|
|
3778
1894
|
for (const arc of t.resets) {
|
|
3779
1895
|
const removed = this.marking.removeAll(arc.place);
|
|
3780
|
-
this.pendingResetPlaces.add(arc.place.name);
|
|
3781
1896
|
for (const token of removed) {
|
|
3782
1897
|
consumed.push(token);
|
|
3783
1898
|
this.emitEvent({
|
|
@@ -3880,10 +1995,12 @@ var BitmapNetExecutor = class {
|
|
|
3880
1995
|
const pids = this.compiled.consumptionPlaceIds(tid);
|
|
3881
1996
|
for (const pid of pids) {
|
|
3882
1997
|
const place2 = this.compiled.place(pid);
|
|
3883
|
-
|
|
1998
|
+
const left = this.marking.tokenCount(place2);
|
|
1999
|
+
if (left === 0) {
|
|
3884
2000
|
clearBit(this.markingBitmap, pid);
|
|
3885
2001
|
}
|
|
3886
2002
|
this.markDirty(pid);
|
|
2003
|
+
if (left < this.restartThresholds[pid]) this.flagIntermediateDisablements(tid, pid);
|
|
3887
2004
|
}
|
|
3888
2005
|
}
|
|
3889
2006
|
// ======================== Completion Processing ========================
|
|
@@ -3913,7 +2030,9 @@ var BitmapNetExecutor = class {
|
|
|
3913
2030
|
try {
|
|
3914
2031
|
const outputs = flight.context.rawOutput();
|
|
3915
2032
|
if (t.outputSpec !== null) {
|
|
3916
|
-
|
|
2033
|
+
const produced2 = outputs.placesWithTokens();
|
|
2034
|
+
const claim = validateOutSpec(t.name, t.outputSpec, produced2);
|
|
2035
|
+
if (outputs.entries().length > produced2.size) this.warnMultiplicity(t.name, outputs, claim);
|
|
3917
2036
|
}
|
|
3918
2037
|
const produced = [];
|
|
3919
2038
|
for (const entry of outputs.entries()) {
|
|
@@ -4020,8 +2139,8 @@ var BitmapNetExecutor = class {
|
|
|
4020
2139
|
if (!this.closed) {
|
|
4021
2140
|
const timerMs = this.millisUntilNextTimedTransition();
|
|
4022
2141
|
if (timerMs === 0 && promises.length === 0) return;
|
|
4023
|
-
promises.push(new Promise((
|
|
4024
|
-
this.wakeUpResolve =
|
|
2142
|
+
promises.push(new Promise((resolve) => {
|
|
2143
|
+
this.wakeUpResolve = resolve;
|
|
4025
2144
|
}));
|
|
4026
2145
|
if (timerMs > 0 && timerMs < Infinity) {
|
|
4027
2146
|
promises.push(new Promise((r) => setTimeout(r, timerMs)));
|
|
@@ -4124,6 +2243,33 @@ var BitmapNetExecutor = class {
|
|
|
4124
2243
|
errorMessage: null
|
|
4125
2244
|
});
|
|
4126
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Reports, once per transition, a firing that wrote more than one token to a place
|
|
2248
|
+
* its output spec names once (IO-016 AC4), as the EVT-013 log-message event. The
|
|
2249
|
+
* tokens are deposited regardless: the diagnostic makes the under-approximation
|
|
2250
|
+
* every branch-enumerating analysis makes of this transition visible.
|
|
2251
|
+
*/
|
|
2252
|
+
warnMultiplicity(transitionName, outputs, claim) {
|
|
2253
|
+
if (this.warnedMultiplicity.has(transitionName)) return;
|
|
2254
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2255
|
+
for (const entry of outputs.entries()) {
|
|
2256
|
+
if (claim.has(entry.place.name)) counts.set(entry.place.name, (counts.get(entry.place.name) ?? 0) + 1);
|
|
2257
|
+
}
|
|
2258
|
+
const repeated = [];
|
|
2259
|
+
for (const [name, n] of counts) if (n > 1) repeated.push(`${name}: ${n}`);
|
|
2260
|
+
if (repeated.length === 0) return;
|
|
2261
|
+
this.warnedMultiplicity.add(transitionName);
|
|
2262
|
+
this.emitEvent({
|
|
2263
|
+
type: "log-message",
|
|
2264
|
+
timestamp: Date.now(),
|
|
2265
|
+
transitionName,
|
|
2266
|
+
logger: "libpetri.runtime",
|
|
2267
|
+
level: "WARN",
|
|
2268
|
+
message: `'${transitionName}': wrote more than one token to a place its output spec names once (${repeated.join(", ")}); branch-enumerating analyses model one token per named place, so this firing exceeds what they explore (IO-016)`,
|
|
2269
|
+
error: null,
|
|
2270
|
+
errorMessage: null
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
4127
2273
|
emitEvent(event) {
|
|
4128
2274
|
if (this.eventStoreEnabled) {
|
|
4129
2275
|
try {
|
|
@@ -4201,6 +2347,10 @@ var PrecompiledNet = class _PrecompiledNet {
|
|
|
4201
2347
|
simpleOutputPlaceId;
|
|
4202
2348
|
// ==================== Input Precomputation ====================
|
|
4203
2349
|
inputPlaceCount;
|
|
2350
|
+
/**
|
|
2351
|
+
* @deprecated Not read by the executor. Kept so the exported shape of `PrecompiledNet`
|
|
2352
|
+
* stays stable; scheduled for removal in the next major release.
|
|
2353
|
+
*/
|
|
4204
2354
|
inputPlaceMaskWords;
|
|
4205
2355
|
// ==================== Reverse Index ====================
|
|
4206
2356
|
placeToTransitions;
|
|
@@ -4517,6 +2667,8 @@ var PrecompiledNetExecutor = class {
|
|
|
4517
2667
|
* materializing a Marking has one to hand.
|
|
4518
2668
|
*/
|
|
4519
2669
|
unknownPlaceTokens = /* @__PURE__ */ new Map();
|
|
2670
|
+
/** Transitions already warned for writing several tokens to a place their spec names once (IO-016 AC4). */
|
|
2671
|
+
warnedMultiplicity = /* @__PURE__ */ new Set();
|
|
4520
2672
|
/** Monotonic source for ν-name minting (ctx.freshName(), NU-010). */
|
|
4521
2673
|
freshNameCounter = 0;
|
|
4522
2674
|
/**
|
|
@@ -4558,9 +2710,17 @@ var PrecompiledNetExecutor = class {
|
|
|
4558
2710
|
inFlightResolves;
|
|
4559
2711
|
inFlightErrors;
|
|
4560
2712
|
inFlightCount = 0;
|
|
4561
|
-
// ====================
|
|
4562
|
-
|
|
4563
|
-
|
|
2713
|
+
// ==================== Intermediate-Disablement Clock Restart ====================
|
|
2714
|
+
/**
|
|
2715
|
+
* Per transition, one bit: a firing left it disabled in the intermediate marking while it
|
|
2716
|
+
* was marked enabled (TIME-012), so the next dirty scan that finds it enabled restarts its
|
|
2717
|
+
* clock. Set at fire time, cleared at the end of every scan; `anyRestartPending` lets the
|
|
2718
|
+
* scan skip the clear when nothing was flagged.
|
|
2719
|
+
*/
|
|
2720
|
+
restartPendingWords;
|
|
2721
|
+
anyRestartPending = false;
|
|
2722
|
+
/** Per place, the count a firing must leave it at to have disabled nothing through it. */
|
|
2723
|
+
restartThresholds;
|
|
4564
2724
|
// ==================== Queues ====================
|
|
4565
2725
|
completionQueue = [];
|
|
4566
2726
|
externalQueue = [];
|
|
@@ -4628,7 +2788,8 @@ var PrecompiledNetExecutor = class {
|
|
|
4628
2788
|
this.inFlightStartMs = new Float64Array(tc);
|
|
4629
2789
|
this.inFlightResolves = new Array(tc).fill(null);
|
|
4630
2790
|
this.inFlightErrors = new Array(tc).fill(null);
|
|
4631
|
-
this.
|
|
2791
|
+
this.restartPendingWords = new Uint32Array(this.transitionWords);
|
|
2792
|
+
this.restartThresholds = restartThresholds(prog.compiled);
|
|
4632
2793
|
this.markingSnapBuffer = new Uint32Array(wc);
|
|
4633
2794
|
this.firingSnapBuffer = new Uint32Array(wc);
|
|
4634
2795
|
this.initMatchCaches();
|
|
@@ -4802,11 +2963,11 @@ var PrecompiledNetExecutor = class {
|
|
|
4802
2963
|
throw new Error(`Place ${envPlace.place.name} is not registered as an environment place`);
|
|
4803
2964
|
}
|
|
4804
2965
|
if (this.closed || this.draining) return false;
|
|
4805
|
-
return new Promise((
|
|
2966
|
+
return new Promise((resolve, reject) => {
|
|
4806
2967
|
this.externalQueue.push({
|
|
4807
2968
|
place: envPlace.place,
|
|
4808
2969
|
token,
|
|
4809
|
-
resolve
|
|
2970
|
+
resolve,
|
|
4810
2971
|
reject
|
|
4811
2972
|
});
|
|
4812
2973
|
this.wakeUp();
|
|
@@ -4881,7 +3042,7 @@ var PrecompiledNetExecutor = class {
|
|
|
4881
3042
|
this.enabledFlags[tid] = 0;
|
|
4882
3043
|
this.enabledTransitionCount--;
|
|
4883
3044
|
this.enabledAtMs[tid] = -Infinity;
|
|
4884
|
-
} else if (canNow && wasEnabled && this.
|
|
3045
|
+
} else if (canNow && wasEnabled && this.anyRestartPending && (this.restartPendingWords[w] & 1 << bit) !== 0) {
|
|
4885
3046
|
this.enabledAtMs[tid] = nowMs;
|
|
4886
3047
|
this.emitEvent({
|
|
4887
3048
|
type: "transition-clock-restarted",
|
|
@@ -4891,9 +3052,9 @@ var PrecompiledNetExecutor = class {
|
|
|
4891
3052
|
}
|
|
4892
3053
|
}
|
|
4893
3054
|
}
|
|
4894
|
-
if (this.
|
|
4895
|
-
this.
|
|
4896
|
-
this.
|
|
3055
|
+
if (this.anyRestartPending) {
|
|
3056
|
+
this.restartPendingWords.fill(0);
|
|
3057
|
+
this.anyRestartPending = false;
|
|
4897
3058
|
}
|
|
4898
3059
|
}
|
|
4899
3060
|
enforceDeadlines(nowMs) {
|
|
@@ -4955,13 +3116,25 @@ var PrecompiledNetExecutor = class {
|
|
|
4955
3116
|
}
|
|
4956
3117
|
return null;
|
|
4957
3118
|
}
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
3119
|
+
/**
|
|
3120
|
+
* Flags each other transition that firing `tid` has just disabled through `pid`
|
|
3121
|
+
* (TIME-012). Called from {@link updateBitmapAfterConsumption} on the intermediate marking,
|
|
3122
|
+
* only for a place the firing left below its restart threshold. Mirrors the bitmap
|
|
3123
|
+
* executor, which gives the rationale.
|
|
3124
|
+
*/
|
|
3125
|
+
flagIntermediateDisablements(tid, pid) {
|
|
3126
|
+
const affected = this.program.placeToTransitions[pid];
|
|
3127
|
+
for (let j = 0; j < affected.length; j++) {
|
|
3128
|
+
const other = affected[j];
|
|
3129
|
+
if (other === tid || !this.enabledFlags[other] || this.inFlightFlags[other]) continue;
|
|
3130
|
+
const w = other >>> WORD_SHIFT;
|
|
3131
|
+
const mask = 1 << (other & BIT_MASK);
|
|
3132
|
+
if ((this.restartPendingWords[w] & mask) !== 0) continue;
|
|
3133
|
+
if (!this.canEnable(other, this.markingBitmap)) {
|
|
3134
|
+
this.restartPendingWords[w] |= mask;
|
|
3135
|
+
this.anyRestartPending = true;
|
|
3136
|
+
}
|
|
4963
3137
|
}
|
|
4964
|
-
return false;
|
|
4965
3138
|
}
|
|
4966
3139
|
// ======================== Firing ========================
|
|
4967
3140
|
fireReadyTransitions(nowMs) {
|
|
@@ -5167,8 +3340,6 @@ var PrecompiledNetExecutor = class {
|
|
|
5167
3340
|
const pid = ops[pc++];
|
|
5168
3341
|
const place2 = prog.places[pid];
|
|
5169
3342
|
const tokens = this.tokenQueues[pid].splice(0);
|
|
5170
|
-
this.pendingResetWords[pid >>> WORD_SHIFT] |= 1 << (pid & BIT_MASK);
|
|
5171
|
-
this.hasPendingResets = true;
|
|
5172
3343
|
for (const token of tokens) {
|
|
5173
3344
|
consumed.push(token);
|
|
5174
3345
|
this.emitEvent({
|
|
@@ -5317,8 +3488,6 @@ var PrecompiledNetExecutor = class {
|
|
|
5317
3488
|
for (const arc of t.resets) {
|
|
5318
3489
|
const pid = prog.compiled.placeId(arc.place);
|
|
5319
3490
|
const tokens = this.tokenQueues[pid].splice(0);
|
|
5320
|
-
this.pendingResetWords[pid >>> WORD_SHIFT] |= 1 << (pid & BIT_MASK);
|
|
5321
|
-
this.hasPendingResets = true;
|
|
5322
3491
|
for (const token of tokens) {
|
|
5323
3492
|
consumed.push(token);
|
|
5324
3493
|
this.emitEvent({
|
|
@@ -5350,10 +3519,12 @@ var PrecompiledNetExecutor = class {
|
|
|
5350
3519
|
const pids = this.program.consumptionPlaceIds[tid];
|
|
5351
3520
|
for (let i = 0; i < pids.length; i++) {
|
|
5352
3521
|
const pid = pids[i];
|
|
5353
|
-
|
|
3522
|
+
const left = this.tokenQueues[pid].length;
|
|
3523
|
+
if (left === 0) {
|
|
5354
3524
|
this.clearMarkingBit(pid);
|
|
5355
3525
|
}
|
|
5356
3526
|
this.markDirty(pid);
|
|
3527
|
+
if (left < this.restartThresholds[pid]) this.flagIntermediateDisablements(tid, pid);
|
|
5357
3528
|
}
|
|
5358
3529
|
}
|
|
5359
3530
|
// ======================== Completion Processing ========================
|
|
@@ -5393,13 +3564,17 @@ var PrecompiledNetExecutor = class {
|
|
|
5393
3564
|
const simplePid = prog.simpleOutputPlaceId[tid];
|
|
5394
3565
|
if (simplePid >= 0) {
|
|
5395
3566
|
const produced2 = outputs.placesWithTokens();
|
|
5396
|
-
|
|
3567
|
+
const named = prog.places[simplePid].name;
|
|
3568
|
+
if (!produced2.has(named)) {
|
|
5397
3569
|
throw new OutViolationError(
|
|
5398
3570
|
`'${t.name}': output does not match the declared spec - produced {}, which no single branch of the spec claims exactly`
|
|
5399
3571
|
);
|
|
5400
3572
|
}
|
|
3573
|
+
if (outputs.entries().length > produced2.size) this.warnMultiplicity(t.name, outputs, /* @__PURE__ */ new Set([named]));
|
|
5401
3574
|
} else if (simplePid === -1) {
|
|
5402
|
-
|
|
3575
|
+
const produced2 = outputs.placesWithTokens();
|
|
3576
|
+
const claim = validateOutSpec(t.name, t.outputSpec, produced2);
|
|
3577
|
+
if (outputs.entries().length > produced2.size) this.warnMultiplicity(t.name, outputs, claim);
|
|
5403
3578
|
}
|
|
5404
3579
|
}
|
|
5405
3580
|
const produced = [];
|
|
@@ -5525,8 +3700,8 @@ var PrecompiledNetExecutor = class {
|
|
|
5525
3700
|
if (!this.closed) {
|
|
5526
3701
|
const timerMs = this.millisUntilNextTimedTransition();
|
|
5527
3702
|
if (timerMs === 0 && promises.length === 0) return;
|
|
5528
|
-
promises.push(new Promise((
|
|
5529
|
-
this.wakeUpResolve =
|
|
3703
|
+
promises.push(new Promise((resolve) => {
|
|
3704
|
+
this.wakeUpResolve = resolve;
|
|
5530
3705
|
}));
|
|
5531
3706
|
if (timerMs > 0 && timerMs < Infinity) {
|
|
5532
3707
|
promises.push(new Promise((r) => setTimeout(r, timerMs)));
|
|
@@ -5619,6 +3794,34 @@ var PrecompiledNetExecutor = class {
|
|
|
5619
3794
|
this.wakeUp();
|
|
5620
3795
|
}
|
|
5621
3796
|
// ======================== Event Emission ========================
|
|
3797
|
+
/**
|
|
3798
|
+
* Reports, once per transition, a firing that wrote more than one token to a place
|
|
3799
|
+
* its output spec names once (IO-016 AC4), as the EVT-013 log-message event. The
|
|
3800
|
+
* tokens are deposited regardless: the diagnostic makes the under-approximation
|
|
3801
|
+
* every branch-enumerating analysis makes of this transition visible. Mirrors the
|
|
3802
|
+
* bitmap executor word for word.
|
|
3803
|
+
*/
|
|
3804
|
+
warnMultiplicity(transitionName, outputs, claim) {
|
|
3805
|
+
if (this.warnedMultiplicity.has(transitionName)) return;
|
|
3806
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3807
|
+
for (const entry of outputs.entries()) {
|
|
3808
|
+
if (claim.has(entry.place.name)) counts.set(entry.place.name, (counts.get(entry.place.name) ?? 0) + 1);
|
|
3809
|
+
}
|
|
3810
|
+
const repeated = [];
|
|
3811
|
+
for (const [name, n] of counts) if (n > 1) repeated.push(`${name}: ${n}`);
|
|
3812
|
+
if (repeated.length === 0) return;
|
|
3813
|
+
this.warnedMultiplicity.add(transitionName);
|
|
3814
|
+
this.emitEvent({
|
|
3815
|
+
type: "log-message",
|
|
3816
|
+
timestamp: Date.now(),
|
|
3817
|
+
transitionName,
|
|
3818
|
+
logger: "libpetri.runtime",
|
|
3819
|
+
level: "WARN",
|
|
3820
|
+
message: `'${transitionName}': wrote more than one token to a place its output spec names once (${repeated.join(", ")}); branch-enumerating analyses model one token per named place, so this firing exceeds what they explore (IO-016)`,
|
|
3821
|
+
error: null,
|
|
3822
|
+
errorMessage: null
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
5622
3825
|
emitEvent(event) {
|
|
5623
3826
|
if (this.eventStoreEnabled) {
|
|
5624
3827
|
try {
|