libpetri 2.3.2 → 2.5.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-B2CV5M3L.js → chunk-2GAZLDH3.js} +26 -8
- package/dist/chunk-2GAZLDH3.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/index.js +2 -2
- package/dist/doclet/resources/petrinet-diagrams.js +21 -21
- package/dist/dot-exporter-7YDFCBKC.js +8 -0
- package/dist/{event-store-I3QiQABh.d.ts → event-store-z30VMn0T.d.ts} +1 -1
- package/dist/export/index.d.ts +22 -2
- package/dist/export/index.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1085 -932
- package/dist/index.js.map +1 -1
- package/dist/{petri-net-CKZYPOsL.d.ts → petri-net-DXdpd0r5.d.ts} +66 -1
- package/dist/verification/index.d.ts +2 -2
- package/dist/viewer/index.d.ts +10 -4
- package/dist/viewer/index.js +76 -4
- package/dist/viewer/index.js.map +1 -1
- package/dist/viewer/viewer.iife.js +21 -21
- package/package.json +1 -1
- package/dist/chunk-B2CV5M3L.js.map +0 -1
- package/dist/dot-exporter-WJMCJEFK.js +0 -8
- /package/dist/{dot-exporter-WJMCJEFK.js.map → dot-exporter-7YDFCBKC.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -635,69 +635,235 @@ function findForwardInputs(out) {
|
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
-
// src/core/
|
|
639
|
-
var
|
|
640
|
-
var
|
|
641
|
-
|
|
642
|
-
|
|
638
|
+
// src/core/interface.ts
|
|
639
|
+
var INTERFACE_KEY = /* @__PURE__ */ Symbol("Interface.internal");
|
|
640
|
+
var Interface = class {
|
|
641
|
+
ports;
|
|
642
|
+
channels;
|
|
643
|
+
/** @internal Use {@link Interface.builder} to create instances. */
|
|
644
|
+
constructor(key, ports, channels) {
|
|
645
|
+
if (key !== INTERFACE_KEY) throw new Error("Use Interface.builder() to create instances");
|
|
646
|
+
this.ports = ports;
|
|
647
|
+
this.channels = channels;
|
|
648
|
+
}
|
|
649
|
+
/** Looks up a port by name. Returns undefined when absent. */
|
|
650
|
+
port(name) {
|
|
651
|
+
return this.ports.get(name);
|
|
652
|
+
}
|
|
653
|
+
/** Looks up a channel by name. Returns undefined when absent. */
|
|
654
|
+
channel(name) {
|
|
655
|
+
return this.channels.get(name);
|
|
656
|
+
}
|
|
643
657
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
658
|
+
* Looks up a port by name and returns its underlying place, narrowed to
|
|
659
|
+
* `Place<T>`. Returns undefined when the port does not exist.
|
|
660
|
+
*
|
|
661
|
+
* Note: TypeScript erases generics at runtime, so unlike Java's
|
|
662
|
+
* `portPlaceAs(name, Class<T>)` this method cannot verify the token type at
|
|
663
|
+
* runtime — the caller is trusted to supply the correct `T`. Per **MOD-022**,
|
|
664
|
+
* type compatibility is enforced at compile time only in TypeScript.
|
|
646
665
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
666
|
+
placeAs(name) {
|
|
667
|
+
const p = this.ports.get(name);
|
|
668
|
+
if (p === void 0) return void 0;
|
|
669
|
+
return p.place;
|
|
670
|
+
}
|
|
671
|
+
static builder() {
|
|
672
|
+
return new InterfaceBuilder();
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
var InterfaceBuilder = class {
|
|
676
|
+
_ports = /* @__PURE__ */ new Map();
|
|
677
|
+
_channels = /* @__PURE__ */ new Map();
|
|
678
|
+
/** Add a pre-built port (rejects duplicate names). */
|
|
679
|
+
port(port) {
|
|
680
|
+
if (this._ports.has(port.name)) {
|
|
681
|
+
throw new Error(`Duplicate port name: '${port.name}'`);
|
|
682
|
+
}
|
|
683
|
+
this._ports.set(port.name, port);
|
|
684
|
+
return this;
|
|
685
|
+
}
|
|
686
|
+
/** Add an input port (advisory direction). */
|
|
687
|
+
inputPort(name, place2) {
|
|
688
|
+
return this.port({ name, direction: "input", place: place2 });
|
|
689
|
+
}
|
|
690
|
+
/** Add an output port (advisory direction). */
|
|
691
|
+
outputPort(name, place2) {
|
|
692
|
+
return this.port({ name, direction: "output", place: place2 });
|
|
693
|
+
}
|
|
694
|
+
/** Add an in-out port (advisory direction). */
|
|
695
|
+
inoutPort(name, place2) {
|
|
696
|
+
return this.port({ name, direction: "inout", place: place2 });
|
|
697
|
+
}
|
|
698
|
+
channel(channelOrName, transition) {
|
|
699
|
+
const ch = typeof channelOrName === "string" ? { name: channelOrName, transition } : channelOrName;
|
|
700
|
+
if (this._channels.has(ch.name)) {
|
|
701
|
+
throw new Error(`Duplicate channel name: '${ch.name}'`);
|
|
702
|
+
}
|
|
703
|
+
this._channels.set(ch.name, ch);
|
|
704
|
+
return this;
|
|
705
|
+
}
|
|
706
|
+
/** @internal Bulk-add ports already validated by the caller. */
|
|
707
|
+
portsAll(ports) {
|
|
708
|
+
for (const p of ports) this.port(p);
|
|
709
|
+
return this;
|
|
710
|
+
}
|
|
711
|
+
/** @internal Bulk-add channels already validated by the caller. */
|
|
712
|
+
channelsAll(channels) {
|
|
713
|
+
for (const c of channels) this.channel(c);
|
|
714
|
+
return this;
|
|
715
|
+
}
|
|
716
|
+
build() {
|
|
717
|
+
return new Interface(
|
|
718
|
+
INTERFACE_KEY,
|
|
719
|
+
new Map(this._ports),
|
|
720
|
+
new Map(this._channels)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/core/instance.ts
|
|
726
|
+
var INSTANCE_KEY = /* @__PURE__ */ Symbol("Instance.internal");
|
|
727
|
+
var Instance = class {
|
|
728
|
+
prefix;
|
|
729
|
+
def;
|
|
730
|
+
renamedBody;
|
|
731
|
+
portHandles;
|
|
732
|
+
channelHandles;
|
|
733
|
+
params;
|
|
734
|
+
/**
|
|
735
|
+
* @internal Use {@link SubnetDef.instantiate} (or the internal factory
|
|
736
|
+
* {@link __createInstance}) to create instances.
|
|
737
|
+
*/
|
|
738
|
+
constructor(key, prefix, def, renamedBody, portHandles, channelHandles, params) {
|
|
739
|
+
if (key !== INSTANCE_KEY) {
|
|
740
|
+
throw new Error("Use SubnetDef.instantiate() to create Instance values");
|
|
652
741
|
}
|
|
742
|
+
this.prefix = prefix;
|
|
743
|
+
this.def = def;
|
|
744
|
+
this.renamedBody = renamedBody;
|
|
745
|
+
this.portHandles = portHandles;
|
|
746
|
+
this.channelHandles = channelHandles;
|
|
747
|
+
this.params = params;
|
|
653
748
|
}
|
|
654
749
|
/**
|
|
655
|
-
*
|
|
750
|
+
* Returns the renamed {@link Place} corresponding to the named port.
|
|
656
751
|
*
|
|
657
|
-
* The port name is the **original** (pre-prefix) name declared in the
|
|
658
|
-
* subnet's `Interface`.
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* because generics are erased.
|
|
752
|
+
* The port name is the **original** (pre-prefix) name as declared in the
|
|
753
|
+
* subnet's `Interface`. Per **MOD-022**, TypeScript enforces token-type
|
|
754
|
+
* compatibility at compile time only; this method does not validate `T`
|
|
755
|
+
* at runtime. A missing name raises an `Error`.
|
|
662
756
|
*
|
|
663
|
-
* @throws when
|
|
757
|
+
* @throws when the port name is unknown
|
|
664
758
|
*/
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
759
|
+
port(name) {
|
|
760
|
+
const p = this.portHandles.get(name);
|
|
761
|
+
if (p === void 0) {
|
|
762
|
+
throw new Error(`No port named '${name}' in instance '${this.prefix}'`);
|
|
668
763
|
}
|
|
669
|
-
|
|
670
|
-
return this;
|
|
764
|
+
return p;
|
|
671
765
|
}
|
|
672
766
|
/**
|
|
673
|
-
*
|
|
674
|
-
* the instance-side renamed channel transition is merged with
|
|
675
|
-
* `callerTransition` into a single transition in the resulting flat net.
|
|
676
|
-
*
|
|
677
|
-
* **Status**: channel composition is implemented in task #13. Recording a
|
|
678
|
-
* channel binding here is permitted, but `compose(...)` currently raises
|
|
679
|
-
* an `Error` when any channel binding is present.
|
|
767
|
+
* Returns the renamed {@link Transition} corresponding to the named channel.
|
|
680
768
|
*
|
|
681
|
-
* @throws when
|
|
769
|
+
* @throws when the channel name is unknown
|
|
682
770
|
*/
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
771
|
+
channel(name) {
|
|
772
|
+
const t = this.channelHandles.get(name);
|
|
773
|
+
if (t === void 0) {
|
|
774
|
+
throw new Error(`No channel named '${name}' in instance '${this.prefix}'`);
|
|
686
775
|
}
|
|
687
|
-
|
|
688
|
-
return this;
|
|
776
|
+
return t;
|
|
689
777
|
}
|
|
690
|
-
/**
|
|
691
|
-
|
|
692
|
-
|
|
778
|
+
/**
|
|
779
|
+
* Returns the debug-UI descriptor for this instance per **MOD-041**.
|
|
780
|
+
*/
|
|
781
|
+
descriptor() {
|
|
782
|
+
const transitions = [];
|
|
783
|
+
for (const t of this.renamedBody.transitions) transitions.push(t.name);
|
|
784
|
+
const exposedPlaces = [];
|
|
785
|
+
for (const p of this.portHandles.values()) exposedPlaces.push(p.name);
|
|
786
|
+
return {
|
|
787
|
+
prefix: this.prefix,
|
|
788
|
+
defName: this.def.name,
|
|
789
|
+
transitions,
|
|
790
|
+
exposedPlaces,
|
|
791
|
+
params: this.params,
|
|
792
|
+
parentPrefix: null
|
|
793
|
+
};
|
|
693
794
|
}
|
|
694
|
-
/**
|
|
695
|
-
|
|
696
|
-
|
|
795
|
+
/**
|
|
796
|
+
* Produces a derived instance whose specified transitions (named by their
|
|
797
|
+
* **original**, pre-prefix names) carry the supplied actions per **MOD-030**.
|
|
798
|
+
*
|
|
799
|
+
* For each entry `[originalName, action]`, the renamed body is searched for
|
|
800
|
+
* the transition whose name equals `prefix + "/" + originalName`. The
|
|
801
|
+
* resulting instance shares the original `def`, `prefix`, `params`, and
|
|
802
|
+
* port/channel handle topology — only the renamed body is rebuilt with new
|
|
803
|
+
* actions. Per **MOD-030**, calling `bindActions` on one instance does NOT
|
|
804
|
+
* affect the actions held by other instances of the same `def`.
|
|
805
|
+
*
|
|
806
|
+
* Unrecognised original names raise an `Error` so typos surface eagerly.
|
|
807
|
+
*/
|
|
808
|
+
bindActions(actionsByOriginalName) {
|
|
809
|
+
const prefixedByName = /* @__PURE__ */ new Map();
|
|
810
|
+
const renamedNames = /* @__PURE__ */ new Set();
|
|
811
|
+
for (const t of this.renamedBody.transitions) {
|
|
812
|
+
renamedNames.add(t.name);
|
|
813
|
+
}
|
|
814
|
+
for (const originalName of Object.keys(actionsByOriginalName)) {
|
|
815
|
+
const prefixed = this.prefix + "/" + originalName;
|
|
816
|
+
if (!renamedNames.has(prefixed)) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`Instance.bindActions: no transition '${originalName}' (resolved as '${prefixed}') in instance '${this.prefix}' of subnet '${this.def.name}'`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
prefixedByName.set(prefixed, actionsByOriginalName[originalName]);
|
|
822
|
+
}
|
|
823
|
+
const reboundBody = this.renamedBody.bindActionsWithResolver((name) => {
|
|
824
|
+
const action = prefixedByName.get(name);
|
|
825
|
+
if (action !== void 0) return action;
|
|
826
|
+
for (const t of this.renamedBody.transitions) {
|
|
827
|
+
if (t.name === name) return t.action;
|
|
828
|
+
}
|
|
829
|
+
throw new Error(`Instance.bindActions: resolver invoked with unknown name '${name}'`);
|
|
830
|
+
});
|
|
831
|
+
const reboundChannelHandles = /* @__PURE__ */ new Map();
|
|
832
|
+
if (this.channelHandles.size > 0) {
|
|
833
|
+
const byName = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const t of reboundBody.transitions) {
|
|
835
|
+
byName.set(t.name, t);
|
|
836
|
+
}
|
|
837
|
+
for (const [name, oldT] of this.channelHandles) {
|
|
838
|
+
const refreshed = byName.get(oldT.name);
|
|
839
|
+
if (refreshed === void 0) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
`Instance.bindActions: channel '${name}' transition '${oldT.name}' not found in rebound body`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
reboundChannelHandles.set(name, refreshed);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return __createInstance(
|
|
848
|
+
this.prefix,
|
|
849
|
+
this.def,
|
|
850
|
+
reboundBody,
|
|
851
|
+
this.portHandles,
|
|
852
|
+
reboundChannelHandles,
|
|
853
|
+
this.params
|
|
854
|
+
);
|
|
697
855
|
}
|
|
698
856
|
};
|
|
699
|
-
function
|
|
700
|
-
return new
|
|
857
|
+
function __createInstance(prefix, def, renamedBody, portHandles, channelHandles, params) {
|
|
858
|
+
return new Instance(
|
|
859
|
+
INSTANCE_KEY,
|
|
860
|
+
prefix,
|
|
861
|
+
def,
|
|
862
|
+
renamedBody,
|
|
863
|
+
portHandles,
|
|
864
|
+
channelHandles,
|
|
865
|
+
params
|
|
866
|
+
);
|
|
701
867
|
}
|
|
702
868
|
|
|
703
869
|
// src/core/internal/subnet-rewriter.ts
|
|
@@ -958,1043 +1124,1030 @@ function applyFusion(transitions, fusionMap) {
|
|
|
958
1124
|
return rewritten;
|
|
959
1125
|
}
|
|
960
1126
|
|
|
961
|
-
// src/
|
|
962
|
-
|
|
963
|
-
|
|
1127
|
+
// src/verification/verification-harness.ts
|
|
1128
|
+
function buildVerificationResult(syntheticNet, perProperty) {
|
|
1129
|
+
const frozen = new Map(perProperty);
|
|
1130
|
+
return {
|
|
1131
|
+
syntheticNet,
|
|
1132
|
+
perProperty: frozen,
|
|
1133
|
+
allProven() {
|
|
1134
|
+
for (const r of frozen.values()) {
|
|
1135
|
+
if (!isProven(r)) return false;
|
|
1136
|
+
}
|
|
1137
|
+
return true;
|
|
1138
|
+
},
|
|
1139
|
+
anyViolated() {
|
|
1140
|
+
for (const r of frozen.values()) {
|
|
1141
|
+
if (isViolated(r)) return true;
|
|
1142
|
+
}
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function normaliseGenerators(generators) {
|
|
1148
|
+
if (generators instanceof Map) return new Map(generators);
|
|
1149
|
+
return new Map(Object.entries(generators));
|
|
1150
|
+
}
|
|
1151
|
+
function normaliseProperties(properties) {
|
|
1152
|
+
if (Array.isArray(properties)) return [...properties];
|
|
1153
|
+
return [...properties];
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/core/subnet-def.ts
|
|
1157
|
+
var SUBNET_DEF_KEY = /* @__PURE__ */ Symbol("SubnetDef.internal");
|
|
1158
|
+
var SubnetDef = class _SubnetDef {
|
|
964
1159
|
name;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1160
|
+
body;
|
|
1161
|
+
iface;
|
|
1162
|
+
/** @internal Use {@link SubnetDef.builder} or {@link SubnetDef.fromNet} to create instances. */
|
|
1163
|
+
constructor(key, name, body, iface) {
|
|
1164
|
+
if (key !== SUBNET_DEF_KEY) {
|
|
1165
|
+
throw new Error("Use SubnetDef.builder() or SubnetDef.fromNet() to create instances");
|
|
970
1166
|
}
|
|
971
1167
|
this.name = name;
|
|
972
|
-
this.
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Returns the canonical member — by convention, the first declared member.
|
|
976
|
-
* The canonical place's identity survives into the resulting flat net.
|
|
977
|
-
*/
|
|
978
|
-
get canonical() {
|
|
979
|
-
return this.members[0];
|
|
1168
|
+
this.body = body;
|
|
1169
|
+
this.iface = iface;
|
|
980
1170
|
}
|
|
981
1171
|
/**
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
* {@link import('./petri-net.js').PetriNetBuilder.build} time.
|
|
1172
|
+
* Produces a renamed module instance per **MOD-010**, **MOD-011**, **MOD-012**,
|
|
1173
|
+
* and **MOD-030**.
|
|
985
1174
|
*
|
|
986
|
-
*
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
return `FusionSet[${this.name}, canonical=${this.canonical.name}, members=${this.members.length}]`;
|
|
994
|
-
}
|
|
995
|
-
// ============================================================
|
|
996
|
-
// Static factories
|
|
997
|
-
// ============================================================
|
|
998
|
-
/** Returns a fresh {@link FusionSetBuilder} with the given human-readable name. */
|
|
999
|
-
static builder(name) {
|
|
1000
|
-
return new FusionSetBuilder(name);
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Convenience factory: builds a fusion set whose first member is `first`
|
|
1004
|
-
* (the canonical) and whose remaining members are `rest`, all sharing the
|
|
1005
|
-
* type `<T>`.
|
|
1175
|
+
* The rename pass walks every place and transition of the body net,
|
|
1176
|
+
* substituting each name with `prefix + "/" + originalName`, and rebuilds
|
|
1177
|
+
* every arc with rewritten place references. Transition timing, priority,
|
|
1178
|
+
* and action are carried through by reference (action sharing per
|
|
1179
|
+
* [MOD-030]). The renamed body is itself a structurally valid `PetriNet`
|
|
1180
|
+
* per [CORE-040]; per-instance state isolation per [MOD-012] is a
|
|
1181
|
+
* structural consequence of distinct prefixed names.
|
|
1006
1182
|
*
|
|
1007
|
-
*
|
|
1008
|
-
* TypeScript compiler checks every `rest` entry is a `Place<T>`).
|
|
1009
|
-
*/
|
|
1010
|
-
static of(name, first, ...rest) {
|
|
1011
|
-
const members = [first];
|
|
1012
|
-
for (const p of rest) members.push(p);
|
|
1013
|
-
return new _FusionSet(FUSION_SET_KEY, name, Object.freeze(members));
|
|
1014
|
-
}
|
|
1015
|
-
};
|
|
1016
|
-
var FusionSetBuilder = class {
|
|
1017
|
-
_name;
|
|
1018
|
-
_members = [];
|
|
1019
|
-
constructor(name) {
|
|
1020
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
1021
|
-
throw new Error("FusionSet.builder: name must be a non-empty string");
|
|
1022
|
-
}
|
|
1023
|
-
this._name = name;
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* Appends a member to the fusion set. The first member becomes the canonical
|
|
1027
|
-
* place per the convention documented on {@link FusionSet}.
|
|
1183
|
+
* ## Prefix validation
|
|
1028
1184
|
*
|
|
1029
|
-
* The
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return this;
|
|
1039
|
-
}
|
|
1040
|
-
/**
|
|
1041
|
-
* Builds the immutable {@link FusionSet}. Validates that the set has at
|
|
1042
|
-
* least one member; the empty set is rejected as malformed per **MOD-060**.
|
|
1043
|
-
* Single-member sets are accepted as a structurally degenerate no-op.
|
|
1044
|
-
*/
|
|
1045
|
-
build() {
|
|
1046
|
-
if (this._members.length === 0) {
|
|
1047
|
-
throw new Error(
|
|
1048
|
-
`FusionSet '${this._name}': must have at least one member (MOD-060)`
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
return new FusionSet(FUSION_SET_KEY, this._name, Object.freeze(this._members.slice()));
|
|
1052
|
-
}
|
|
1053
|
-
};
|
|
1054
|
-
|
|
1055
|
-
// src/core/petri-net.ts
|
|
1056
|
-
var PETRI_NET_KEY = /* @__PURE__ */ Symbol("PetriNet.internal");
|
|
1057
|
-
var PetriNet = class _PetriNet {
|
|
1058
|
-
name;
|
|
1059
|
-
places;
|
|
1060
|
-
transitions;
|
|
1061
|
-
/** @internal Use {@link PetriNet.builder} to create instances. */
|
|
1062
|
-
constructor(key, name, places, transitions) {
|
|
1063
|
-
if (key !== PETRI_NET_KEY) throw new Error("Use PetriNet.builder() to create instances");
|
|
1064
|
-
this.name = name;
|
|
1065
|
-
this.places = places;
|
|
1066
|
-
this.transitions = transitions;
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Creates a new PetriNet with actions bound to transitions by name.
|
|
1070
|
-
* Unbound transitions keep passthrough action.
|
|
1071
|
-
*/
|
|
1072
|
-
bindActions(actionBindings) {
|
|
1073
|
-
const bindings = actionBindings instanceof Map ? actionBindings : new Map(Object.entries(actionBindings));
|
|
1074
|
-
return this.bindActionsWithResolver(
|
|
1075
|
-
(name) => bindings.get(name) ?? passthrough()
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* Creates a new PetriNet with actions bound via a resolver function.
|
|
1185
|
+
* The `"/"` character is reserved as the prefix separator (per [MOD-010]).
|
|
1186
|
+
* User-supplied prefixes MUST NOT contain `"/"`; nested instantiation is
|
|
1187
|
+
* performed by the future `PetriNetBuilder.compose(...)` mechanism (per
|
|
1188
|
+
* [MOD-013]). A prefix containing `"/"` raises an `Error`.
|
|
1189
|
+
*
|
|
1190
|
+
* @param prefix the rename prefix (non-empty, must not contain `"/"`)
|
|
1191
|
+
* @param params the parameter value carried through to `Instance.params`
|
|
1192
|
+
* (may be omitted when `P` is `void`)
|
|
1193
|
+
* @throws when `prefix` is empty or contains `"/"`
|
|
1080
1194
|
*/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1195
|
+
instantiate(prefix, params) {
|
|
1196
|
+
validatePrefix(prefix);
|
|
1197
|
+
const placeRemap = /* @__PURE__ */ new Map();
|
|
1198
|
+
const transitionRemap = /* @__PURE__ */ new Map();
|
|
1199
|
+
const renamedBody = renameNet(this.body, prefix, placeRemap, transitionRemap);
|
|
1200
|
+
const portHandles = /* @__PURE__ */ new Map();
|
|
1201
|
+
for (const port of this.iface.ports.values()) {
|
|
1202
|
+
const renamed = placeRemap.get(port.place.name);
|
|
1203
|
+
if (renamed === void 0) {
|
|
1204
|
+
throw new Error(
|
|
1205
|
+
`Port '${port.name}' references place '${port.place.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
1206
|
+
);
|
|
1089
1207
|
}
|
|
1208
|
+
portHandles.set(port.name, renamed);
|
|
1090
1209
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
_transitions = /* @__PURE__ */ new Set();
|
|
1101
|
-
_fusionSets = [];
|
|
1102
|
-
constructor(name) {
|
|
1103
|
-
this._name = name;
|
|
1104
|
-
}
|
|
1105
|
-
/** Add an explicit place. */
|
|
1106
|
-
place(place2) {
|
|
1107
|
-
this._places.add(place2);
|
|
1108
|
-
return this;
|
|
1109
|
-
}
|
|
1110
|
-
/** Add explicit places. */
|
|
1111
|
-
places(...places) {
|
|
1112
|
-
for (const p of places) this._places.add(p);
|
|
1113
|
-
return this;
|
|
1114
|
-
}
|
|
1115
|
-
/** Add a transition (auto-collects places from arcs). */
|
|
1116
|
-
transition(transition) {
|
|
1117
|
-
this._transitions.add(transition);
|
|
1118
|
-
for (const spec of transition.inputSpecs) {
|
|
1119
|
-
this._places.add(spec.place);
|
|
1120
|
-
}
|
|
1121
|
-
for (const p of transition.outputPlaces()) {
|
|
1122
|
-
this._places.add(p);
|
|
1123
|
-
}
|
|
1124
|
-
for (const inh of transition.inhibitors) {
|
|
1125
|
-
this._places.add(inh.place);
|
|
1126
|
-
}
|
|
1127
|
-
for (const r of transition.reads) {
|
|
1128
|
-
this._places.add(r.place);
|
|
1129
|
-
}
|
|
1130
|
-
for (const r of transition.resets) {
|
|
1131
|
-
this._places.add(r.place);
|
|
1132
|
-
}
|
|
1133
|
-
return this;
|
|
1134
|
-
}
|
|
1135
|
-
/** Add transitions (auto-collects places from arcs). */
|
|
1136
|
-
transitions(...transitions) {
|
|
1137
|
-
for (const t of transitions) this.transition(t);
|
|
1138
|
-
return this;
|
|
1139
|
-
}
|
|
1140
|
-
compose(instance, arg) {
|
|
1141
|
-
if (arg === void 0) {
|
|
1142
|
-
return this.composeAuto(instance);
|
|
1143
|
-
}
|
|
1144
|
-
if (typeof arg === "function") {
|
|
1145
|
-
const bindings = __createComposeBindings();
|
|
1146
|
-
arg(bindings);
|
|
1147
|
-
return this.composeInternal(instance, bindings.portBindings(), bindings.channelBindings());
|
|
1210
|
+
const channelHandles = /* @__PURE__ */ new Map();
|
|
1211
|
+
for (const channel of this.iface.channels.values()) {
|
|
1212
|
+
const renamed = transitionRemap.get(channel.transition.name);
|
|
1213
|
+
if (renamed === void 0) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`Channel '${channel.name}' references transition '${channel.transition.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
channelHandles.set(channel.name, renamed);
|
|
1148
1219
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1220
|
+
return __createInstance(
|
|
1221
|
+
prefix,
|
|
1222
|
+
this,
|
|
1223
|
+
renamedBody,
|
|
1224
|
+
portHandles,
|
|
1225
|
+
channelHandles,
|
|
1226
|
+
params
|
|
1227
|
+
);
|
|
1151
1228
|
}
|
|
1152
1229
|
/**
|
|
1153
|
-
*
|
|
1230
|
+
* Verifies safety properties of this subnet definition **in isolation** per
|
|
1231
|
+
* **MOD-051**, by wrapping it in a synthetic enclosing net where each input
|
|
1232
|
+
* port is fed by an {@link EnvironmentPlace} (token-source per the harness
|
|
1233
|
+
* generator) and each output port is observed via a synthetic place. The
|
|
1234
|
+
* standard {@link SmtVerifier} (per [MOD-050]) is invoked once per property
|
|
1235
|
+
* declared in the harness; the resulting per-property outcomes are
|
|
1236
|
+
* aggregated into a {@link VerificationResult}.
|
|
1154
1237
|
*
|
|
1155
|
-
*
|
|
1156
|
-
* Place the SubnetDef builder declared via `.inputPort(name, hostPlace)`
|
|
1157
|
-
* (or `outputPort` / `inoutPort`). If the host builder already declares
|
|
1158
|
-
* the equal place, the two merge; if not, the place arrives implicitly
|
|
1159
|
-
* via the rewritten transitions' arcs (same flow as explicit `bindPort`).
|
|
1238
|
+
* ## Synthetic-net construction
|
|
1160
1239
|
*
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1240
|
+
* The synthetic enclosing net is built by:
|
|
1241
|
+
* 1. Instantiating this `SubnetDef` with the prefix `"sut"` (system-under-test)
|
|
1242
|
+
* and `harness.params`.
|
|
1243
|
+
* 2. For each input or in-out port on the interface, looking up the
|
|
1244
|
+
* harness generator by port name, allocating a synthetic
|
|
1245
|
+
* {@link Place}`<unknown>` of the same conceptual token type as the
|
|
1246
|
+
* port, wrapping it in an {@link EnvironmentPlace}, and binding the
|
|
1247
|
+
* port to that synthetic place via
|
|
1248
|
+
* {@link import('./petri-net.js').PetriNetBuilder.compose}. The supplier
|
|
1249
|
+
* is invoked once at construction time to materialize the seed token —
|
|
1250
|
+
* its presence is what bounds the input behavior under analysis. **If
|
|
1251
|
+
* the harness map is missing a generator for a required input or in-out
|
|
1252
|
+
* port, an `Error` is thrown.**
|
|
1253
|
+
* 3. For each output or in-out port, allocating a synthetic observation
|
|
1254
|
+
* {@link Place}`<unknown>` and binding the port to it via the same
|
|
1255
|
+
* `compose(...)` call. The verifier inspects this place's reachability
|
|
1256
|
+
* / marking through the standard property APIs ({@link SmtProperty}).
|
|
1257
|
+
* 4. Building the resulting flat {@link PetriNet} per [MOD-023] (the
|
|
1258
|
+
* verifier is composition-unaware per [MOD-050]).
|
|
1166
1259
|
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1260
|
+
* ## Per-property invocation
|
|
1261
|
+
*
|
|
1262
|
+
* Each {@link SmtProperty} in the harness is verified independently against
|
|
1263
|
+
* the same synthetic net. The synthetic environment places are passed
|
|
1264
|
+
* through to the verifier so that places driven by the harness generators
|
|
1265
|
+
* are treated under the verifier's environment-analysis semantics rather
|
|
1266
|
+
* than as ordinary sink places.
|
|
1267
|
+
*
|
|
1268
|
+
* @param harness the verification harness — supplies parameters, input-port
|
|
1269
|
+
* token generators, and the property set
|
|
1270
|
+
* @returns a {@link VerificationResult} aggregating per-property
|
|
1271
|
+
* {@link SmtVerificationResult}s
|
|
1272
|
+
* @throws when an input or in-out port is missing a harness generator
|
|
1169
1273
|
*/
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const channelNames = [];
|
|
1174
|
-
for (const c of iface.channels.values()) channelNames.push(c.name);
|
|
1175
|
-
channelNames.sort();
|
|
1176
|
-
throw new Error(
|
|
1177
|
-
`compose(Instance): subnet '${instance.def.name}' (instance prefix '${instance.prefix}') declares channels [${channelNames.join(", ")}]; auto-compose does not bind channels. Use compose(instance, bind => bind.bindChannel(...)) with explicit channel bindings.`
|
|
1178
|
-
);
|
|
1274
|
+
async verify(harness) {
|
|
1275
|
+
if (harness === null || harness === void 0) {
|
|
1276
|
+
throw new Error("SubnetDef.verify: harness must not be null/undefined");
|
|
1179
1277
|
}
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1278
|
+
const sut = this.instantiate("sut", harness.params);
|
|
1279
|
+
const generators = normaliseGenerators(harness.portInputGenerators);
|
|
1280
|
+
const properties = normaliseProperties(harness.properties);
|
|
1281
|
+
const envPlaces = [];
|
|
1282
|
+
const portMappings = /* @__PURE__ */ new Map();
|
|
1283
|
+
for (const port of this.iface.ports.values()) {
|
|
1284
|
+
const portName = port.name;
|
|
1285
|
+
switch (port.direction) {
|
|
1286
|
+
case "input": {
|
|
1287
|
+
const generator = generators.get(portName);
|
|
1288
|
+
if (generator === void 0) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`verify: harness is missing an input generator for port '${portName}' on subnet '${this.name}' (MOD-051)`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
const seed = generator();
|
|
1294
|
+
if (seed === null || seed === void 0) {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`verify: input generator for port '${portName}' on subnet '${this.name}' produced null/undefined`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
const synth = place(`harness_in_${portName}`);
|
|
1300
|
+
envPlaces.push(environmentPlace(synth.name));
|
|
1301
|
+
portMappings.set(portName, synth);
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case "output": {
|
|
1305
|
+
const synth = place(`harness_out_${portName}`);
|
|
1306
|
+
portMappings.set(portName, synth);
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
case "inout": {
|
|
1310
|
+
const generator = generators.get(portName);
|
|
1311
|
+
if (generator === void 0) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`verify: harness is missing an input generator for in-out port '${portName}' on subnet '${this.name}' (MOD-051)`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
const seed = generator();
|
|
1317
|
+
if (seed === null || seed === void 0) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`verify: input generator for in-out port '${portName}' on subnet '${this.name}' produced null/undefined`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const synth = place(`harness_io_${portName}`);
|
|
1323
|
+
envPlaces.push(environmentPlace(synth.name));
|
|
1324
|
+
portMappings.set(portName, synth);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1187
1327
|
}
|
|
1188
|
-
return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
|
|
1189
1328
|
}
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1192
|
-
for (const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (hostMatch !== void 0) {
|
|
1197
|
-
mergeMap.set(renamed.name, hostMatch);
|
|
1329
|
+
const syntheticNet = PetriNet.builder("verify_" + this.name).compose(sut, portMappings).build();
|
|
1330
|
+
const perProperty = /* @__PURE__ */ new Map();
|
|
1331
|
+
for (const property of properties) {
|
|
1332
|
+
const verifier = SmtVerifier.forNet(syntheticNet).property(property);
|
|
1333
|
+
if (envPlaces.length > 0) {
|
|
1334
|
+
verifier.environmentPlaces(...envPlaces);
|
|
1198
1335
|
}
|
|
1336
|
+
const result = await verifier.verify();
|
|
1337
|
+
perProperty.set(property, result);
|
|
1199
1338
|
}
|
|
1200
|
-
return
|
|
1339
|
+
return buildVerificationResult(syntheticNet, perProperty);
|
|
1340
|
+
}
|
|
1341
|
+
// ============================================================
|
|
1342
|
+
// Static factories
|
|
1343
|
+
// ============================================================
|
|
1344
|
+
static builder(name) {
|
|
1345
|
+
return new SubnetDefBuilder(name);
|
|
1201
1346
|
}
|
|
1202
1347
|
/**
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
* transition through {@link substitutePlaces}, applies channel merges per
|
|
1206
|
-
* [MOD-021], and adds the resulting transitions to this builder.
|
|
1207
|
-
*
|
|
1208
|
-
* ## Channel-merge flow ([MOD-021])
|
|
1348
|
+
* Retrofit utility per **MOD-014**: wraps an existing closed {@link PetriNet}
|
|
1349
|
+
* plus an {@link Interface} into an unparameterised `SubnetDef<void>`.
|
|
1209
1350
|
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* that fuses caller-side + instance-side; remove the rewritten
|
|
1218
|
-
* instance-side entry. Also replace (or add) the caller-side
|
|
1219
|
-
* transition in this builder's transition set with the same merged
|
|
1220
|
-
* result, indexed under the caller's name slot.
|
|
1221
|
-
* 4. Add the surviving (un-merged) entries to this builder.
|
|
1351
|
+
* Validation per **MOD-014** / **MOD-006** is enforced before the result is
|
|
1352
|
+
* constructed:
|
|
1353
|
+
* - Every port's underlying `Place` must be present in `net.places`.
|
|
1354
|
+
* - Every channel's underlying `Transition` must be present in `net.transitions`.
|
|
1355
|
+
* - Port and channel name uniqueness is re-validated defensively (the
|
|
1356
|
+
* `Interface` builder already enforces this; hand-built `Interface`
|
|
1357
|
+
* values bypass that path).
|
|
1222
1358
|
*
|
|
1223
|
-
* The
|
|
1224
|
-
* builder eagerly would force a second "remove-then-replace" pass to
|
|
1225
|
-
* apply the channel merges, complicating the place-collection invariants.
|
|
1226
|
-
* Collecting first and merging second keeps the builder's transition set
|
|
1227
|
-
* finalized exactly once.
|
|
1359
|
+
* The resulting subnet definition is unparameterised (parameter type is `void`).
|
|
1228
1360
|
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
* {@link Instance.bindActions} call that may have rebuilt the renamed-body
|
|
1232
|
-
* transitions, breaking identity equality between the body and the
|
|
1233
|
-
* channel-handle map — but the prefixed names remain stable.
|
|
1361
|
+
* @throws when a port place is not in `net.places`, a channel transition is
|
|
1362
|
+
* not in `net.transitions`, or port/channel names are not unique.
|
|
1234
1363
|
*/
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
if (port
|
|
1241
|
-
const knownPorts = [];
|
|
1242
|
-
for (const p of iface.ports.values()) knownPorts.push(p.name);
|
|
1364
|
+
static fromNet(net, iface) {
|
|
1365
|
+
const bodyPlaces = net.places;
|
|
1366
|
+
const bodyTransitions = net.transitions;
|
|
1367
|
+
const seenPortNames = /* @__PURE__ */ new Set();
|
|
1368
|
+
for (const port of iface.ports.values()) {
|
|
1369
|
+
if (seenPortNames.has(port.name)) {
|
|
1243
1370
|
throw new Error(
|
|
1244
|
-
`
|
|
1371
|
+
`fromNet: duplicate port name '${port.name}' on interface for net '${net.name}' (MOD-006)`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
seenPortNames.add(port.name);
|
|
1375
|
+
if (!bodyPlaces.has(port.place)) {
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
`fromNet: port '${port.name}' references place '${port.place.name}' which is not in net '${net.name}' (MOD-014/MOD-006)`
|
|
1245
1378
|
);
|
|
1246
1379
|
}
|
|
1247
|
-
const ifacePlace = instance.port(portName);
|
|
1248
|
-
mergeMap.set(ifacePlace.name, callerPlace);
|
|
1249
|
-
}
|
|
1250
|
-
return this.applyComposition(instance, mergeMap, channelBindings);
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Shared post-mergeMap pipeline: rewrites renamed-body transitions
|
|
1254
|
-
* through `mergeMap`, applies channel merges per **MOD-021**, and adds
|
|
1255
|
-
* the surviving transitions to the builder.
|
|
1256
|
-
*
|
|
1257
|
-
* Used by both the explicit-binding path (`composeInternal`) and the
|
|
1258
|
-
* auto-compose path (`composeAuto` per **MOD-024**). The two paths differ
|
|
1259
|
-
* only in how the (renamed Place name → host Place) `mergeMap` is built.
|
|
1260
|
-
*/
|
|
1261
|
-
applyComposition(instance, mergeMap, channelBindings) {
|
|
1262
|
-
const iface = instance.def.iface;
|
|
1263
|
-
const rewrittenByName = /* @__PURE__ */ new Map();
|
|
1264
|
-
for (const t of instance.renamedBody.transitions) {
|
|
1265
|
-
rewrittenByName.set(t.name, substitutePlaces(t, mergeMap));
|
|
1266
1380
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
const knownChannels = [];
|
|
1273
|
-
for (const c of iface.channels.values()) knownChannels.push(c.name);
|
|
1274
|
-
const err = new Error(
|
|
1275
|
-
`compose: no channel named '${channelName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known channels: [${knownChannels.join(", ")}]`
|
|
1381
|
+
const seenChannelNames = /* @__PURE__ */ new Set();
|
|
1382
|
+
for (const channel of iface.channels.values()) {
|
|
1383
|
+
if (seenChannelNames.has(channel.name)) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`fromNet: duplicate channel name '${channel.name}' on interface for net '${net.name}' (MOD-006)`
|
|
1276
1386
|
);
|
|
1277
|
-
err.cause = cause;
|
|
1278
|
-
throw err;
|
|
1279
1387
|
}
|
|
1280
|
-
|
|
1281
|
-
if (
|
|
1388
|
+
seenChannelNames.add(channel.name);
|
|
1389
|
+
if (!bodyTransitions.has(channel.transition)) {
|
|
1282
1390
|
throw new Error(
|
|
1283
|
-
`
|
|
1391
|
+
`fromNet: channel '${channel.name}' references transition '${channel.transition.name}' which is not in net '${net.name}' (MOD-014/MOD-006)`
|
|
1284
1392
|
);
|
|
1285
1393
|
}
|
|
1286
|
-
const merged = mergeTransitions(callerTrans, rewrittenInstanceChannel, callerTrans.name);
|
|
1287
|
-
rewrittenByName.delete(instanceRenamedChannel.name);
|
|
1288
|
-
this._transitions.delete(callerTrans);
|
|
1289
|
-
this.transition(merged);
|
|
1290
|
-
}
|
|
1291
|
-
for (const rewritten of rewrittenByName.values()) {
|
|
1292
|
-
this.transition(rewritten);
|
|
1293
1394
|
}
|
|
1395
|
+
return new _SubnetDef(SUBNET_DEF_KEY, net.name, net, iface);
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
var SubnetDefBuilder = class {
|
|
1399
|
+
_name;
|
|
1400
|
+
_bodyBuilder;
|
|
1401
|
+
_ports = [];
|
|
1402
|
+
_channels = [];
|
|
1403
|
+
constructor(name) {
|
|
1404
|
+
this._name = name;
|
|
1405
|
+
this._bodyBuilder = PetriNet.builder(name);
|
|
1406
|
+
}
|
|
1407
|
+
// -------- body construction (delegates to PetriNet.Builder) --------
|
|
1408
|
+
transition(transition) {
|
|
1409
|
+
this._bodyBuilder.transition(transition);
|
|
1294
1410
|
return this;
|
|
1295
1411
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const declarer = args[0];
|
|
1299
|
-
const fb = FusionSet.builder(this._name + "-fusion");
|
|
1300
|
-
declarer(fb);
|
|
1301
|
-
this._fusionSets.push(fb.build());
|
|
1302
|
-
return this;
|
|
1303
|
-
}
|
|
1304
|
-
for (const s of args) {
|
|
1305
|
-
if (s === void 0 || s === null) {
|
|
1306
|
-
throw new Error("fuse: fusion set must not be null");
|
|
1307
|
-
}
|
|
1308
|
-
this._fusionSets.push(s);
|
|
1309
|
-
}
|
|
1412
|
+
transitions(...transitions) {
|
|
1413
|
+
this._bodyBuilder.transitions(...transitions);
|
|
1310
1414
|
return this;
|
|
1311
1415
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
*
|
|
1316
|
-
* 1. Detect overlapping fusion sets — a single place declared in two sets
|
|
1317
|
-
* is rejected with an `Error`.
|
|
1318
|
-
* 2. Build the `non-canonical → canonical` substitution map across all
|
|
1319
|
-
* sets, keyed by non-canonical place name (matching the rewriter's
|
|
1320
|
-
* Map<string, Place<unknown>> convention — TypeScript Place identity is
|
|
1321
|
-
* name-based per `runtime/compiled-net.ts`).
|
|
1322
|
-
* 3. Walk every transition through {@link applyFusion} to rewrite arc place
|
|
1323
|
-
* references.
|
|
1324
|
-
* 4. Re-derive the place set from the rewritten transitions plus any
|
|
1325
|
-
* caller-declared standalone places, dropping non-canonical members.
|
|
1326
|
-
* Caller-declared standalone places that happen to be non-canonical
|
|
1327
|
-
* members are also dropped.
|
|
1328
|
-
*
|
|
1329
|
-
* If no fusion sets were registered, the build is the trivial
|
|
1330
|
-
* `new PetriNet(...)` — the fusion machinery has no per-build cost when
|
|
1331
|
-
* unused.
|
|
1332
|
-
*
|
|
1333
|
-
* @throws when two fusion sets share a place
|
|
1334
|
-
*/
|
|
1335
|
-
build() {
|
|
1336
|
-
if (this._fusionSets.length === 0) {
|
|
1337
|
-
return new PetriNet(PETRI_NET_KEY, this._name, this._places, this._transitions);
|
|
1338
|
-
}
|
|
1339
|
-
return this.buildWithFusion();
|
|
1416
|
+
place(place2) {
|
|
1417
|
+
this._bodyBuilder.place(place2);
|
|
1418
|
+
return this;
|
|
1340
1419
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1420
|
+
// -------- interface declarations --------
|
|
1421
|
+
inputPort(name, place2) {
|
|
1422
|
+
this._ports.push({ name, direction: "input", place: place2 });
|
|
1423
|
+
return this;
|
|
1424
|
+
}
|
|
1425
|
+
outputPort(name, place2) {
|
|
1426
|
+
this._ports.push({ name, direction: "output", place: place2 });
|
|
1427
|
+
return this;
|
|
1428
|
+
}
|
|
1429
|
+
inoutPort(name, place2) {
|
|
1430
|
+
this._ports.push({ name, direction: "inout", place: place2 });
|
|
1431
|
+
return this;
|
|
1432
|
+
}
|
|
1433
|
+
channel(name, transition) {
|
|
1434
|
+
this._channels.push({ name, transition });
|
|
1435
|
+
return this;
|
|
1436
|
+
}
|
|
1437
|
+
// -------- build & validate (MOD-006) --------
|
|
1438
|
+
build() {
|
|
1439
|
+
const built = this._bodyBuilder.build();
|
|
1440
|
+
const bodyPlaces = built.places;
|
|
1441
|
+
const bodyTransitions = built.transitions;
|
|
1442
|
+
const portNames = /* @__PURE__ */ new Set();
|
|
1443
|
+
for (const port of this._ports) {
|
|
1444
|
+
if (portNames.has(port.name)) {
|
|
1445
|
+
throw new Error(`Subnet '${this._name}': duplicate port name '${port.name}'`);
|
|
1356
1446
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
for (const nc of set.nonCanonical()) {
|
|
1363
|
-
fusionMap.set(nc.name, canonical);
|
|
1364
|
-
nonCanonicalNames.add(nc.name);
|
|
1447
|
+
portNames.add(port.name);
|
|
1448
|
+
if (!bodyPlaces.has(port.place)) {
|
|
1449
|
+
throw new Error(
|
|
1450
|
+
`Subnet '${this._name}': port '${port.name}' references place '${port.place.name}' which is not in the body`
|
|
1451
|
+
);
|
|
1365
1452
|
}
|
|
1366
1453
|
}
|
|
1367
|
-
const
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1454
|
+
const channelNames = /* @__PURE__ */ new Set();
|
|
1455
|
+
for (const channel of this._channels) {
|
|
1456
|
+
if (channelNames.has(channel.name)) {
|
|
1457
|
+
throw new Error(`Subnet '${this._name}': duplicate channel name '${channel.name}'`);
|
|
1458
|
+
}
|
|
1459
|
+
channelNames.add(channel.name);
|
|
1460
|
+
if (!bodyTransitions.has(channel.transition)) {
|
|
1461
|
+
throw new Error(
|
|
1462
|
+
`Subnet '${this._name}': channel '${channel.name}' references transition '${channel.transition.name}' which is not in the body`
|
|
1463
|
+
);
|
|
1372
1464
|
}
|
|
1373
1465
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
for (const r of t.resets) rebuiltPlaces.add(r.place);
|
|
1380
|
-
}
|
|
1381
|
-
return new PetriNet(PETRI_NET_KEY, this._name, rebuiltPlaces, rewrittenTransitions);
|
|
1466
|
+
const ifaceBuilder = Interface.builder();
|
|
1467
|
+
ifaceBuilder.portsAll(this._ports);
|
|
1468
|
+
ifaceBuilder.channelsAll(this._channels);
|
|
1469
|
+
const iface = ifaceBuilder.build();
|
|
1470
|
+
return new SubnetDef(SUBNET_DEF_KEY, this._name, built, iface);
|
|
1382
1471
|
}
|
|
1383
1472
|
};
|
|
1384
|
-
function
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
builder.inputs(...t.inputSpecs);
|
|
1388
|
-
}
|
|
1389
|
-
if (t.outputSpec !== null) {
|
|
1390
|
-
builder.outputs(t.outputSpec);
|
|
1391
|
-
}
|
|
1392
|
-
for (const inh of t.inhibitors) {
|
|
1393
|
-
builder.inhibitor(inh.place);
|
|
1394
|
-
}
|
|
1395
|
-
for (const r of t.reads) {
|
|
1396
|
-
builder.read(r.place);
|
|
1473
|
+
function validatePrefix(prefix) {
|
|
1474
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
1475
|
+
throw new Error("SubnetDef.instantiate: prefix must be a non-empty string");
|
|
1397
1476
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1477
|
+
if (prefix.indexOf("/") >= 0) {
|
|
1478
|
+
throw new Error(
|
|
1479
|
+
`SubnetDef.instantiate: prefix must not contain '/' (reserved as the prefix separator per MOD-010); use compose(...) for nested instantiation. Got: '${prefix}'`
|
|
1480
|
+
);
|
|
1400
1481
|
}
|
|
1401
|
-
return builder.build();
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// src/core/subnet.ts
|
|
1405
|
-
function closedSubnet(net) {
|
|
1406
|
-
return { kind: "closed", net };
|
|
1407
|
-
}
|
|
1408
|
-
function openSubnet(def) {
|
|
1409
|
-
return { kind: "open", def };
|
|
1410
1482
|
}
|
|
1411
1483
|
|
|
1412
|
-
// src/core/
|
|
1413
|
-
var
|
|
1414
|
-
var
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
/**
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1484
|
+
// src/core/compose-bindings.ts
|
|
1485
|
+
var COMPOSE_BINDINGS_KEY = /* @__PURE__ */ Symbol("ComposeBindings.internal");
|
|
1486
|
+
var ComposeBindings = class {
|
|
1487
|
+
_portBindings = /* @__PURE__ */ new Map();
|
|
1488
|
+
_channelBindings = /* @__PURE__ */ new Map();
|
|
1489
|
+
/**
|
|
1490
|
+
* @internal Use {@link PetriNetBuilder.compose} — instances are produced
|
|
1491
|
+
* by the host builder and supplied to the caller's callback.
|
|
1492
|
+
*/
|
|
1493
|
+
constructor(key) {
|
|
1494
|
+
if (key !== COMPOSE_BINDINGS_KEY) {
|
|
1495
|
+
throw new Error(
|
|
1496
|
+
"Use PetriNetBuilder.compose(instance, b => ...) \u2014 ComposeBindings is not directly constructible"
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1426
1499
|
}
|
|
1427
|
-
/**
|
|
1428
|
-
|
|
1429
|
-
|
|
1500
|
+
/**
|
|
1501
|
+
* Binds the named interface port to the given caller place per **MOD-020**.
|
|
1502
|
+
*
|
|
1503
|
+
* The port name is the **original** (pre-prefix) name declared in the
|
|
1504
|
+
* subnet's `Interface`. The typed `<T>` parameter ensures the caller
|
|
1505
|
+
* place's token type matches the interface port's token type at compile
|
|
1506
|
+
* time per [MOD-022]; TypeScript does not validate the type at runtime
|
|
1507
|
+
* because generics are erased.
|
|
1508
|
+
*
|
|
1509
|
+
* @throws when `portName` is already bound on this builder
|
|
1510
|
+
*/
|
|
1511
|
+
bindPort(portName, callerPlace) {
|
|
1512
|
+
if (this._portBindings.has(portName)) {
|
|
1513
|
+
throw new Error(`Port '${portName}' is already bound`);
|
|
1514
|
+
}
|
|
1515
|
+
this._portBindings.set(portName, callerPlace);
|
|
1516
|
+
return this;
|
|
1430
1517
|
}
|
|
1431
1518
|
/**
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1519
|
+
* Records a synchronous channel binding per **MOD-021**: at compose time,
|
|
1520
|
+
* the instance-side renamed channel transition is merged with
|
|
1521
|
+
* `callerTransition` into a single transition in the resulting flat net.
|
|
1434
1522
|
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1523
|
+
* **Status**: channel composition is implemented in task #13. Recording a
|
|
1524
|
+
* channel binding here is permitted, but `compose(...)` currently raises
|
|
1525
|
+
* an `Error` when any channel binding is present.
|
|
1526
|
+
*
|
|
1527
|
+
* @throws when `channelName` is already bound on this builder
|
|
1439
1528
|
*/
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1529
|
+
bindChannel(channelName, callerTransition) {
|
|
1530
|
+
if (this._channelBindings.has(channelName)) {
|
|
1531
|
+
throw new Error(`Channel '${channelName}' is already bound`);
|
|
1532
|
+
}
|
|
1533
|
+
this._channelBindings.set(channelName, callerTransition);
|
|
1534
|
+
return this;
|
|
1444
1535
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1536
|
+
/** Returns an unmodifiable view of the recorded port bindings. */
|
|
1537
|
+
portBindings() {
|
|
1538
|
+
return this._portBindings;
|
|
1539
|
+
}
|
|
1540
|
+
/** Returns an unmodifiable view of the recorded channel bindings. */
|
|
1541
|
+
channelBindings() {
|
|
1542
|
+
return this._channelBindings;
|
|
1447
1543
|
}
|
|
1448
1544
|
};
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1545
|
+
function __createComposeBindings() {
|
|
1546
|
+
return new ComposeBindings(COMPOSE_BINDINGS_KEY);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/core/fusion-set.ts
|
|
1550
|
+
var FUSION_SET_KEY = /* @__PURE__ */ Symbol("FusionSet.internal");
|
|
1551
|
+
var FusionSet = class _FusionSet {
|
|
1552
|
+
name;
|
|
1553
|
+
members;
|
|
1554
|
+
/** @internal Use {@link FusionSet.builder} or {@link FusionSet.of} to create instances. */
|
|
1555
|
+
constructor(key, name, members) {
|
|
1556
|
+
if (key !== FUSION_SET_KEY) {
|
|
1557
|
+
throw new Error("Use FusionSet.builder() or FusionSet.of() to create instances");
|
|
1456
1558
|
}
|
|
1457
|
-
this.
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1460
|
-
/** Add an input port (advisory direction). */
|
|
1461
|
-
inputPort(name, place2) {
|
|
1462
|
-
return this.port({ name, direction: "input", place: place2 });
|
|
1559
|
+
this.name = name;
|
|
1560
|
+
this.members = members;
|
|
1463
1561
|
}
|
|
1464
|
-
/**
|
|
1465
|
-
|
|
1466
|
-
|
|
1562
|
+
/**
|
|
1563
|
+
* Returns the canonical member — by convention, the first declared member.
|
|
1564
|
+
* The canonical place's identity survives into the resulting flat net.
|
|
1565
|
+
*/
|
|
1566
|
+
get canonical() {
|
|
1567
|
+
return this.members[0];
|
|
1467
1568
|
}
|
|
1468
|
-
/**
|
|
1469
|
-
|
|
1470
|
-
|
|
1569
|
+
/**
|
|
1570
|
+
* Returns all members **except** the canonical member, in declaration order.
|
|
1571
|
+
* These are the places that get substituted away at
|
|
1572
|
+
* {@link import('./petri-net.js').PetriNetBuilder.build} time.
|
|
1573
|
+
*
|
|
1574
|
+
* For a single-member (degenerate) set, returns an empty array.
|
|
1575
|
+
*/
|
|
1576
|
+
nonCanonical() {
|
|
1577
|
+
if (this.members.length <= 1) return [];
|
|
1578
|
+
return this.members.slice(1);
|
|
1471
1579
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
if (this._channels.has(ch.name)) {
|
|
1475
|
-
throw new Error(`Duplicate channel name: '${ch.name}'`);
|
|
1476
|
-
}
|
|
1477
|
-
this._channels.set(ch.name, ch);
|
|
1478
|
-
return this;
|
|
1580
|
+
toString() {
|
|
1581
|
+
return `FusionSet[${this.name}, canonical=${this.canonical.name}, members=${this.members.length}]`;
|
|
1479
1582
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1583
|
+
// ============================================================
|
|
1584
|
+
// Static factories
|
|
1585
|
+
// ============================================================
|
|
1586
|
+
/** Returns a fresh {@link FusionSetBuilder} with the given human-readable name. */
|
|
1587
|
+
static builder(name) {
|
|
1588
|
+
return new FusionSetBuilder(name);
|
|
1484
1589
|
}
|
|
1485
|
-
/**
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1590
|
+
/**
|
|
1591
|
+
* Convenience factory: builds a fusion set whose first member is `first`
|
|
1592
|
+
* (the canonical) and whose remaining members are `rest`, all sharing the
|
|
1593
|
+
* type `<T>`.
|
|
1594
|
+
*
|
|
1595
|
+
* The varargs form ensures static-type homogeneity at the call site (the
|
|
1596
|
+
* TypeScript compiler checks every `rest` entry is a `Place<T>`).
|
|
1597
|
+
*/
|
|
1598
|
+
static of(name, first, ...rest) {
|
|
1599
|
+
const members = [first];
|
|
1600
|
+
for (const p of rest) members.push(p);
|
|
1601
|
+
return new _FusionSet(FUSION_SET_KEY, name, Object.freeze(members));
|
|
1489
1602
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
)
|
|
1603
|
+
};
|
|
1604
|
+
var FusionSetBuilder = class {
|
|
1605
|
+
_name;
|
|
1606
|
+
_members = [];
|
|
1607
|
+
constructor(name) {
|
|
1608
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1609
|
+
throw new Error("FusionSet.builder: name must be a non-empty string");
|
|
1610
|
+
}
|
|
1611
|
+
this._name = name;
|
|
1496
1612
|
}
|
|
1497
|
-
};
|
|
1498
|
-
|
|
1499
|
-
// src/core/instance.ts
|
|
1500
|
-
var INSTANCE_KEY = /* @__PURE__ */ Symbol("Instance.internal");
|
|
1501
|
-
var Instance = class {
|
|
1502
|
-
prefix;
|
|
1503
|
-
def;
|
|
1504
|
-
renamedBody;
|
|
1505
|
-
portHandles;
|
|
1506
|
-
channelHandles;
|
|
1507
|
-
params;
|
|
1508
1613
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
* {@link
|
|
1614
|
+
* Appends a member to the fusion set. The first member becomes the canonical
|
|
1615
|
+
* place per the convention documented on {@link FusionSet}.
|
|
1616
|
+
*
|
|
1617
|
+
* The `<T>` parameter is for compile-time guidance only: callers writing
|
|
1618
|
+
* typed code at the same call site benefit from the compiler checking
|
|
1619
|
+
* `Place<T>` at each member.
|
|
1511
1620
|
*/
|
|
1512
|
-
|
|
1513
|
-
if (
|
|
1514
|
-
throw new Error(
|
|
1621
|
+
member(place2) {
|
|
1622
|
+
if (place2 === void 0 || place2 === null) {
|
|
1623
|
+
throw new Error(`FusionSet '${this._name}': member place must not be null`);
|
|
1515
1624
|
}
|
|
1516
|
-
this.
|
|
1517
|
-
this
|
|
1518
|
-
this.renamedBody = renamedBody;
|
|
1519
|
-
this.portHandles = portHandles;
|
|
1520
|
-
this.channelHandles = channelHandles;
|
|
1521
|
-
this.params = params;
|
|
1625
|
+
this._members.push(place2);
|
|
1626
|
+
return this;
|
|
1522
1627
|
}
|
|
1523
1628
|
/**
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
1526
|
-
*
|
|
1527
|
-
* subnet's `Interface`. Per **MOD-022**, TypeScript enforces token-type
|
|
1528
|
-
* compatibility at compile time only; this method does not validate `T`
|
|
1529
|
-
* at runtime. A missing name raises an `Error`.
|
|
1530
|
-
*
|
|
1531
|
-
* @throws when the port name is unknown
|
|
1629
|
+
* Builds the immutable {@link FusionSet}. Validates that the set has at
|
|
1630
|
+
* least one member; the empty set is rejected as malformed per **MOD-060**.
|
|
1631
|
+
* Single-member sets are accepted as a structurally degenerate no-op.
|
|
1532
1632
|
*/
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1633
|
+
build() {
|
|
1634
|
+
if (this._members.length === 0) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`FusionSet '${this._name}': must have at least one member (MOD-060)`
|
|
1637
|
+
);
|
|
1537
1638
|
}
|
|
1538
|
-
return
|
|
1639
|
+
return new FusionSet(FUSION_SET_KEY, this._name, Object.freeze(this._members.slice()));
|
|
1539
1640
|
}
|
|
1641
|
+
};
|
|
1642
|
+
|
|
1643
|
+
// src/core/petri-net.ts
|
|
1644
|
+
var PETRI_NET_KEY = /* @__PURE__ */ Symbol("PetriNet.internal");
|
|
1645
|
+
var PetriNet = class _PetriNet {
|
|
1646
|
+
name;
|
|
1647
|
+
places;
|
|
1648
|
+
transitions;
|
|
1540
1649
|
/**
|
|
1541
|
-
*
|
|
1650
|
+
* Subnet-membership metadata per **MOD-026**: maps each node name (place or
|
|
1651
|
+
* transition) contributed by exactly one directly-composed subnet to that
|
|
1652
|
+
* subnet's name. Shared places contributed by two or more subnets, and every
|
|
1653
|
+
* node of a net not built via {@link PetriNetBuilder.compose}`(SubnetDef)`,
|
|
1654
|
+
* are absent. Never null — an empty map when there is no metadata. The DOT
|
|
1655
|
+
* exporter renders `subgraph cluster_*` blocks from this map.
|
|
1542
1656
|
*
|
|
1543
|
-
*
|
|
1657
|
+
* V8 `Map` is insertion-ordered, preserving compose order so cluster
|
|
1658
|
+
* subgraphs render deterministically (cross-language byte-parity).
|
|
1544
1659
|
*/
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1660
|
+
subnetMembership;
|
|
1661
|
+
/** @internal Use {@link PetriNet.builder} to create instances. */
|
|
1662
|
+
constructor(key, name, places, transitions, subnetMembership = /* @__PURE__ */ new Map()) {
|
|
1663
|
+
if (key !== PETRI_NET_KEY) throw new Error("Use PetriNet.builder() to create instances");
|
|
1664
|
+
this.name = name;
|
|
1665
|
+
this.places = places;
|
|
1666
|
+
this.transitions = transitions;
|
|
1667
|
+
this.subnetMembership = subnetMembership;
|
|
1551
1668
|
}
|
|
1552
1669
|
/**
|
|
1553
|
-
*
|
|
1670
|
+
* Creates a new PetriNet with actions bound to transitions by name.
|
|
1671
|
+
* Unbound transitions keep passthrough action.
|
|
1554
1672
|
*/
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
return {
|
|
1561
|
-
prefix: this.prefix,
|
|
1562
|
-
defName: this.def.name,
|
|
1563
|
-
transitions,
|
|
1564
|
-
exposedPlaces,
|
|
1565
|
-
params: this.params,
|
|
1566
|
-
parentPrefix: null
|
|
1567
|
-
};
|
|
1673
|
+
bindActions(actionBindings) {
|
|
1674
|
+
const bindings = actionBindings instanceof Map ? actionBindings : new Map(Object.entries(actionBindings));
|
|
1675
|
+
return this.bindActionsWithResolver(
|
|
1676
|
+
(name) => bindings.get(name) ?? passthrough()
|
|
1677
|
+
);
|
|
1568
1678
|
}
|
|
1569
1679
|
/**
|
|
1570
|
-
*
|
|
1571
|
-
* **original**, pre-prefix names) carry the supplied actions per **MOD-030**.
|
|
1572
|
-
*
|
|
1573
|
-
* For each entry `[originalName, action]`, the renamed body is searched for
|
|
1574
|
-
* the transition whose name equals `prefix + "/" + originalName`. The
|
|
1575
|
-
* resulting instance shares the original `def`, `prefix`, `params`, and
|
|
1576
|
-
* port/channel handle topology — only the renamed body is rebuilt with new
|
|
1577
|
-
* actions. Per **MOD-030**, calling `bindActions` on one instance does NOT
|
|
1578
|
-
* affect the actions held by other instances of the same `def`.
|
|
1579
|
-
*
|
|
1580
|
-
* Unrecognised original names raise an `Error` so typos surface eagerly.
|
|
1680
|
+
* Creates a new PetriNet with actions bound via a resolver function.
|
|
1581
1681
|
*/
|
|
1582
|
-
|
|
1583
|
-
const
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (!renamedNames.has(prefixed)) {
|
|
1591
|
-
throw new Error(
|
|
1592
|
-
`Instance.bindActions: no transition '${originalName}' (resolved as '${prefixed}') in instance '${this.prefix}' of subnet '${this.def.name}'`
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
prefixedByName.set(prefixed, actionsByOriginalName[originalName]);
|
|
1596
|
-
}
|
|
1597
|
-
const reboundBody = this.renamedBody.bindActionsWithResolver((name) => {
|
|
1598
|
-
const action = prefixedByName.get(name);
|
|
1599
|
-
if (action !== void 0) return action;
|
|
1600
|
-
for (const t of this.renamedBody.transitions) {
|
|
1601
|
-
if (t.name === name) return t.action;
|
|
1602
|
-
}
|
|
1603
|
-
throw new Error(`Instance.bindActions: resolver invoked with unknown name '${name}'`);
|
|
1604
|
-
});
|
|
1605
|
-
const reboundChannelHandles = /* @__PURE__ */ new Map();
|
|
1606
|
-
if (this.channelHandles.size > 0) {
|
|
1607
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1608
|
-
for (const t of reboundBody.transitions) {
|
|
1609
|
-
byName.set(t.name, t);
|
|
1610
|
-
}
|
|
1611
|
-
for (const [name, oldT] of this.channelHandles) {
|
|
1612
|
-
const refreshed = byName.get(oldT.name);
|
|
1613
|
-
if (refreshed === void 0) {
|
|
1614
|
-
throw new Error(
|
|
1615
|
-
`Instance.bindActions: channel '${name}' transition '${oldT.name}' not found in rebound body`
|
|
1616
|
-
);
|
|
1617
|
-
}
|
|
1618
|
-
reboundChannelHandles.set(name, refreshed);
|
|
1682
|
+
bindActionsWithResolver(actionResolver) {
|
|
1683
|
+
const boundTransitions = /* @__PURE__ */ new Set();
|
|
1684
|
+
for (const t of this.transitions) {
|
|
1685
|
+
const action = actionResolver(t.name);
|
|
1686
|
+
if (action !== null && action !== t.action) {
|
|
1687
|
+
boundTransitions.add(rebuildWithAction(t, action));
|
|
1688
|
+
} else {
|
|
1689
|
+
boundTransitions.add(t);
|
|
1619
1690
|
}
|
|
1620
1691
|
}
|
|
1621
|
-
return
|
|
1622
|
-
|
|
1623
|
-
this.
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
this.params
|
|
1692
|
+
return new _PetriNet(
|
|
1693
|
+
PETRI_NET_KEY,
|
|
1694
|
+
this.name,
|
|
1695
|
+
this.places,
|
|
1696
|
+
boundTransitions,
|
|
1697
|
+
this.subnetMembership
|
|
1628
1698
|
);
|
|
1629
1699
|
}
|
|
1700
|
+
static builder(name) {
|
|
1701
|
+
return new PetriNetBuilder(name);
|
|
1702
|
+
}
|
|
1630
1703
|
};
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
return false;
|
|
1704
|
+
var PetriNetBuilder = class _PetriNetBuilder {
|
|
1705
|
+
_name;
|
|
1706
|
+
_places = /* @__PURE__ */ new Set();
|
|
1707
|
+
_transitions = /* @__PURE__ */ new Set();
|
|
1708
|
+
_fusionSets = [];
|
|
1709
|
+
// MOD-026: node name -> set of subnet names that contributed it via
|
|
1710
|
+
// compose(SubnetDef), in first-contribution order. Resolved to single-owner
|
|
1711
|
+
// membership at build(). V8 Map/Set iterate in insertion order, preserving
|
|
1712
|
+
// compose order for cross-language byte-parity.
|
|
1713
|
+
_subnetContributions = /* @__PURE__ */ new Map();
|
|
1714
|
+
constructor(name) {
|
|
1715
|
+
this._name = name;
|
|
1716
|
+
}
|
|
1717
|
+
/** Add an explicit place. */
|
|
1718
|
+
place(place2) {
|
|
1719
|
+
this._places.add(place2);
|
|
1720
|
+
return this;
|
|
1721
|
+
}
|
|
1722
|
+
/** Add explicit places. */
|
|
1723
|
+
places(...places) {
|
|
1724
|
+
for (const p of places) this._places.add(p);
|
|
1725
|
+
return this;
|
|
1726
|
+
}
|
|
1727
|
+
/** Add a transition (auto-collects places from arcs). */
|
|
1728
|
+
transition(transition) {
|
|
1729
|
+
this._transitions.add(transition);
|
|
1730
|
+
for (const spec of transition.inputSpecs) {
|
|
1731
|
+
this._places.add(spec.place);
|
|
1660
1732
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1733
|
+
for (const p of transition.outputPlaces()) {
|
|
1734
|
+
this._places.add(p);
|
|
1735
|
+
}
|
|
1736
|
+
for (const inh of transition.inhibitors) {
|
|
1737
|
+
this._places.add(inh.place);
|
|
1738
|
+
}
|
|
1739
|
+
for (const r of transition.reads) {
|
|
1740
|
+
this._places.add(r.place);
|
|
1741
|
+
}
|
|
1742
|
+
for (const r of transition.resets) {
|
|
1743
|
+
this._places.add(r.place);
|
|
1744
|
+
}
|
|
1745
|
+
return this;
|
|
1746
|
+
}
|
|
1747
|
+
/** Add transitions (auto-collects places from arcs). */
|
|
1748
|
+
transitions(...transitions) {
|
|
1749
|
+
for (const t of transitions) this.transition(t);
|
|
1750
|
+
return this;
|
|
1751
|
+
}
|
|
1752
|
+
compose(instanceOrDef, arg) {
|
|
1753
|
+
if (instanceOrDef instanceof SubnetDef) {
|
|
1754
|
+
return this.composeDirect(instanceOrDef);
|
|
1682
1755
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1756
|
+
const instance = instanceOrDef;
|
|
1757
|
+
if (arg === void 0) {
|
|
1758
|
+
return this.composeAuto(instance);
|
|
1759
|
+
}
|
|
1760
|
+
if (typeof arg === "function") {
|
|
1761
|
+
const bindings = __createComposeBindings();
|
|
1762
|
+
arg(bindings);
|
|
1763
|
+
return this.composeInternal(instance, bindings.portBindings(), bindings.channelBindings());
|
|
1764
|
+
}
|
|
1765
|
+
const portMappings = arg instanceof Map ? arg : new Map(Object.entries(arg));
|
|
1766
|
+
return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
|
|
1686
1767
|
}
|
|
1687
1768
|
/**
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1769
|
+
* Composes a subnet {@link SubnetDef} **directly** into this builder per
|
|
1770
|
+
* **MOD-025** — **without instantiation**, and without the prefix-renaming
|
|
1771
|
+
* of {@link SubnetDef.instantiate}.
|
|
1690
1772
|
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
* [MOD-030]). The renamed body is itself a structurally valid `PetriNet`
|
|
1696
|
-
* per [CORE-040]; per-instance state isolation per [MOD-012] is a
|
|
1697
|
-
* structural consequence of distinct prefixed names.
|
|
1773
|
+
* Every body place and transition is added under its **original**
|
|
1774
|
+
* (un-prefixed) name. Places merge into this builder by name: a body place
|
|
1775
|
+
* whose name equals an enclosing-net place *is* that place in the composed
|
|
1776
|
+
* flat net. This is the mode for wiring a subnet in as a single shared copy.
|
|
1698
1777
|
*
|
|
1699
|
-
*
|
|
1778
|
+
* Direct composition is **order-independent**: composing the same set of
|
|
1779
|
+
* subnets in any order yields the same flat net, because merging is by
|
|
1780
|
+
* place name and not by a probe of the builder's place set at call time —
|
|
1781
|
+
* contrast the no-interface body-inference branch of {@link composeAuto}
|
|
1782
|
+
* (MOD-024), which is order-sensitive.
|
|
1700
1783
|
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
* [MOD-013]). A prefix containing `"/"` raises an `Error`.
|
|
1784
|
+
* For multiple *independent* copies — each with isolated per-instance state
|
|
1785
|
+
* per [MOD-012] — use {@link SubnetDef.instantiate} + the `compose(instance)`
|
|
1786
|
+
* overload instead.
|
|
1705
1787
|
*
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1788
|
+
* Rejections: a body transition whose name already exists in this builder
|
|
1789
|
+
* (use `instantiate(prefix)` for independent copies); a subnet whose
|
|
1790
|
+
* interface declares any channel (direct composition does not bind
|
|
1791
|
+
* channels — use `instantiate` + the channel-binding `compose` overload).
|
|
1792
|
+
*
|
|
1793
|
+
* Token-type conflicts on a same-named place cannot be detected: TS
|
|
1794
|
+
* {@link Place} equality is name-only at runtime (the documented carve-out,
|
|
1795
|
+
* same as MOD-024).
|
|
1796
|
+
*
|
|
1797
|
+
* @throws when the subnet declares channels, or a body transition name
|
|
1798
|
+
* collides with a transition already in this builder.
|
|
1710
1799
|
*/
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
throw new Error(
|
|
1721
|
-
`Port '${port.name}' references place '${port.place.name}' that was not found in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
1722
|
-
);
|
|
1723
|
-
}
|
|
1724
|
-
portHandles.set(port.name, renamed);
|
|
1800
|
+
composeDirect(def) {
|
|
1801
|
+
const iface = def.iface;
|
|
1802
|
+
if (iface.channels.size > 0) {
|
|
1803
|
+
const channelNames = [];
|
|
1804
|
+
for (const c of iface.channels.values()) channelNames.push(c.name);
|
|
1805
|
+
channelNames.sort();
|
|
1806
|
+
throw new Error(
|
|
1807
|
+
`compose(SubnetDef): subnet '${def.name}' declares channels [${channelNames.join(", ")}]; direct composition does not bind channels. Use def.instantiate(prefix) + compose(instance, bind => bind.bindChannel(...)).`
|
|
1808
|
+
);
|
|
1725
1809
|
}
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1810
|
+
const body = def.body;
|
|
1811
|
+
const hostTransitionNames = /* @__PURE__ */ new Set();
|
|
1812
|
+
for (const t of this._transitions) hostTransitionNames.add(t.name);
|
|
1813
|
+
for (const t of body.transitions) {
|
|
1814
|
+
if (hostTransitionNames.has(t.name)) {
|
|
1730
1815
|
throw new Error(
|
|
1731
|
-
`
|
|
1816
|
+
`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).`
|
|
1732
1817
|
);
|
|
1733
1818
|
}
|
|
1734
|
-
channelHandles.set(channel.name, renamed);
|
|
1735
1819
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1820
|
+
const hostByName = /* @__PURE__ */ new Map();
|
|
1821
|
+
for (const p of this._places) hostByName.set(p.name, p);
|
|
1822
|
+
const subnetName = def.name.replace(/\//g, "_");
|
|
1823
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
1824
|
+
for (const p of body.places) {
|
|
1825
|
+
const host = hostByName.get(p.name);
|
|
1826
|
+
this.place(host ?? p);
|
|
1827
|
+
if (host !== void 0) mergeMap.set(p.name, host);
|
|
1828
|
+
this.recordContribution(p.name, subnetName);
|
|
1829
|
+
}
|
|
1830
|
+
for (const t of body.transitions) {
|
|
1831
|
+
this.transition(substitutePlaces(t, mergeMap));
|
|
1832
|
+
this.recordContribution(t.name, subnetName);
|
|
1833
|
+
}
|
|
1834
|
+
return this;
|
|
1835
|
+
}
|
|
1836
|
+
/** @internal MOD-026: records a node as contributed by the named subnet. */
|
|
1837
|
+
recordContribution(nodeName, subnetName) {
|
|
1838
|
+
let owners = this._subnetContributions.get(nodeName);
|
|
1839
|
+
if (owners === void 0) {
|
|
1840
|
+
owners = /* @__PURE__ */ new Set();
|
|
1841
|
+
this._subnetContributions.set(nodeName, owners);
|
|
1842
|
+
}
|
|
1843
|
+
owners.add(subnetName);
|
|
1744
1844
|
}
|
|
1745
1845
|
/**
|
|
1746
|
-
*
|
|
1747
|
-
* **MOD-051**, by wrapping it in a synthetic enclosing net where each input
|
|
1748
|
-
* port is fed by an {@link EnvironmentPlace} (token-source per the harness
|
|
1749
|
-
* generator) and each output port is observed via a synthetic place. The
|
|
1750
|
-
* standard {@link SmtVerifier} (per [MOD-050]) is invoked once per property
|
|
1751
|
-
* declared in the harness; the resulting per-property outcomes are
|
|
1752
|
-
* aggregated into a {@link VerificationResult}.
|
|
1753
|
-
*
|
|
1754
|
-
* ## Synthetic-net construction
|
|
1755
|
-
*
|
|
1756
|
-
* The synthetic enclosing net is built by:
|
|
1757
|
-
* 1. Instantiating this `SubnetDef` with the prefix `"sut"` (system-under-test)
|
|
1758
|
-
* and `harness.params`.
|
|
1759
|
-
* 2. For each input or in-out port on the interface, looking up the
|
|
1760
|
-
* harness generator by port name, allocating a synthetic
|
|
1761
|
-
* {@link Place}`<unknown>` of the same conceptual token type as the
|
|
1762
|
-
* port, wrapping it in an {@link EnvironmentPlace}, and binding the
|
|
1763
|
-
* port to that synthetic place via
|
|
1764
|
-
* {@link import('./petri-net.js').PetriNetBuilder.compose}. The supplier
|
|
1765
|
-
* is invoked once at construction time to materialize the seed token —
|
|
1766
|
-
* its presence is what bounds the input behavior under analysis. **If
|
|
1767
|
-
* the harness map is missing a generator for a required input or in-out
|
|
1768
|
-
* port, an `Error` is thrown.**
|
|
1769
|
-
* 3. For each output or in-out port, allocating a synthetic observation
|
|
1770
|
-
* {@link Place}`<unknown>` and binding the port to it via the same
|
|
1771
|
-
* `compose(...)` call. The verifier inspects this place's reachability
|
|
1772
|
-
* / marking through the standard property APIs ({@link SmtProperty}).
|
|
1773
|
-
* 4. Building the resulting flat {@link PetriNet} per [MOD-023] (the
|
|
1774
|
-
* verifier is composition-unaware per [MOD-050]).
|
|
1846
|
+
* Identity-default auto-compose per **MOD-024**.
|
|
1775
1847
|
*
|
|
1776
|
-
*
|
|
1848
|
+
* Each declared interface port auto-binds to its own `port.place` — the
|
|
1849
|
+
* Place the SubnetDef builder declared via `.inputPort(name, hostPlace)`
|
|
1850
|
+
* (or `outputPort` / `inoutPort`). If the host builder already declares
|
|
1851
|
+
* the equal place, the two merge; if not, the place arrives implicitly
|
|
1852
|
+
* via the rewritten transitions' arcs (same flow as explicit `bindPort`).
|
|
1777
1853
|
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1854
|
+
* If the subnet declares no interface ports at all, body places are
|
|
1855
|
+
* checked against this builder's place set **by name** (matching the
|
|
1856
|
+
* existing TS Place equality semantics; see [CORE-002] note in
|
|
1857
|
+
* `spec/11-modular-composition.md` MOD-024). Body places that don't match
|
|
1858
|
+
* stay private under their prefixed names per [MOD-010].
|
|
1783
1859
|
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
* @returns a {@link VerificationResult} aggregating per-property
|
|
1787
|
-
* {@link SmtVerificationResult}s
|
|
1788
|
-
* @throws when an input or in-out port is missing a harness generator
|
|
1860
|
+
* Channels are NOT auto-bound — transition identity is too delicate for
|
|
1861
|
+
* inference. If the subnet declares any channel, this overload throws.
|
|
1789
1862
|
*/
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1863
|
+
composeAuto(instance) {
|
|
1864
|
+
const iface = instance.def.iface;
|
|
1865
|
+
if (iface.channels.size > 0) {
|
|
1866
|
+
const channelNames = [];
|
|
1867
|
+
for (const c of iface.channels.values()) channelNames.push(c.name);
|
|
1868
|
+
channelNames.sort();
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
`compose(Instance): subnet '${instance.def.name}' (instance prefix '${instance.prefix}') declares channels [${channelNames.join(", ")}]; auto-compose does not bind channels. Use compose(instance, bind => bind.bindChannel(...)) with explicit channel bindings.`
|
|
1871
|
+
);
|
|
1793
1872
|
}
|
|
1794
|
-
const
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
switch (port.direction) {
|
|
1802
|
-
case "input": {
|
|
1803
|
-
const generator = generators.get(portName);
|
|
1804
|
-
if (generator === void 0) {
|
|
1805
|
-
throw new Error(
|
|
1806
|
-
`verify: harness is missing an input generator for port '${portName}' on subnet '${this.name}' (MOD-051)`
|
|
1807
|
-
);
|
|
1808
|
-
}
|
|
1809
|
-
const seed = generator();
|
|
1810
|
-
if (seed === null || seed === void 0) {
|
|
1811
|
-
throw new Error(
|
|
1812
|
-
`verify: input generator for port '${portName}' on subnet '${this.name}' produced null/undefined`
|
|
1813
|
-
);
|
|
1814
|
-
}
|
|
1815
|
-
const synth = place(`harness_in_${portName}`);
|
|
1816
|
-
envPlaces.push(environmentPlace(synth.name));
|
|
1817
|
-
portMappings.set(portName, synth);
|
|
1818
|
-
break;
|
|
1819
|
-
}
|
|
1820
|
-
case "output": {
|
|
1821
|
-
const synth = place(`harness_out_${portName}`);
|
|
1822
|
-
portMappings.set(portName, synth);
|
|
1823
|
-
break;
|
|
1824
|
-
}
|
|
1825
|
-
case "inout": {
|
|
1826
|
-
const generator = generators.get(portName);
|
|
1827
|
-
if (generator === void 0) {
|
|
1828
|
-
throw new Error(
|
|
1829
|
-
`verify: harness is missing an input generator for in-out port '${portName}' on subnet '${this.name}' (MOD-051)`
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
const seed = generator();
|
|
1833
|
-
if (seed === null || seed === void 0) {
|
|
1834
|
-
throw new Error(
|
|
1835
|
-
`verify: input generator for in-out port '${portName}' on subnet '${this.name}' produced null/undefined`
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
|
-
const synth = place(`harness_io_${portName}`);
|
|
1839
|
-
envPlaces.push(environmentPlace(synth.name));
|
|
1840
|
-
portMappings.set(portName, synth);
|
|
1841
|
-
break;
|
|
1842
|
-
}
|
|
1873
|
+
const hostByName = /* @__PURE__ */ new Map();
|
|
1874
|
+
for (const p of this._places) hostByName.set(p.name, p);
|
|
1875
|
+
if (iface.ports.size > 0) {
|
|
1876
|
+
const portMappings = /* @__PURE__ */ new Map();
|
|
1877
|
+
for (const port of iface.ports.values()) {
|
|
1878
|
+
const hostMatch = hostByName.get(port.place.name);
|
|
1879
|
+
portMappings.set(port.name, hostMatch ?? port.place);
|
|
1843
1880
|
}
|
|
1881
|
+
return this.composeInternal(instance, portMappings, /* @__PURE__ */ new Map());
|
|
1844
1882
|
}
|
|
1845
|
-
const
|
|
1846
|
-
const
|
|
1847
|
-
for (const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1883
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
1884
|
+
const prefix = instance.prefix + "/";
|
|
1885
|
+
for (const renamed of instance.renamedBody.places) {
|
|
1886
|
+
if (!renamed.name.startsWith(prefix)) continue;
|
|
1887
|
+
const originalName = renamed.name.substring(prefix.length);
|
|
1888
|
+
const hostMatch = hostByName.get(originalName);
|
|
1889
|
+
if (hostMatch !== void 0) {
|
|
1890
|
+
mergeMap.set(renamed.name, hostMatch);
|
|
1851
1891
|
}
|
|
1852
|
-
const result = await verifier.verify();
|
|
1853
|
-
perProperty.set(property, result);
|
|
1854
1892
|
}
|
|
1855
|
-
return
|
|
1856
|
-
}
|
|
1857
|
-
// ============================================================
|
|
1858
|
-
// Static factories
|
|
1859
|
-
// ============================================================
|
|
1860
|
-
static builder(name) {
|
|
1861
|
-
return new SubnetDefBuilder(name);
|
|
1893
|
+
return this.applyComposition(instance, mergeMap, /* @__PURE__ */ new Map());
|
|
1862
1894
|
}
|
|
1863
1895
|
/**
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1896
|
+
* @internal Shared compose implementation: validates port and channel
|
|
1897
|
+
* bindings, builds the place-substitution map, walks every renamed-body
|
|
1898
|
+
* transition through {@link substitutePlaces}, applies channel merges per
|
|
1899
|
+
* [MOD-021], and adds the resulting transitions to this builder.
|
|
1866
1900
|
*
|
|
1867
|
-
*
|
|
1868
|
-
* constructed:
|
|
1869
|
-
* - Every port's underlying `Place` must be present in `net.places`.
|
|
1870
|
-
* - Every channel's underlying `Transition` must be present in `net.transitions`.
|
|
1871
|
-
* - Port and channel name uniqueness is re-validated defensively (the
|
|
1872
|
-
* `Interface` builder already enforces this; hand-built `Interface`
|
|
1873
|
-
* values bypass that path).
|
|
1901
|
+
* ## Channel-merge flow ([MOD-021])
|
|
1874
1902
|
*
|
|
1875
|
-
*
|
|
1903
|
+
* 1. Collect rewritten instance transitions into a working `Map<string,
|
|
1904
|
+
* Transition>` keyed by prefixed transition name (deferred — not yet
|
|
1905
|
+
* added to the builder's transition set).
|
|
1906
|
+
* 2. For each channel binding, resolve the renamed instance-side
|
|
1907
|
+
* transition through `instance.channel(channelName)`, then look up its
|
|
1908
|
+
* rewritten counterpart in the working map by name.
|
|
1909
|
+
* 3. Replace the working-map entry with a {@link mergeTransitions} result
|
|
1910
|
+
* that fuses caller-side + instance-side; remove the rewritten
|
|
1911
|
+
* instance-side entry. Also replace (or add) the caller-side
|
|
1912
|
+
* transition in this builder's transition set with the same merged
|
|
1913
|
+
* result, indexed under the caller's name slot.
|
|
1914
|
+
* 4. Add the surviving (un-merged) entries to this builder.
|
|
1876
1915
|
*
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1916
|
+
* The deferral matters: writing the rewritten instance transitions to the
|
|
1917
|
+
* builder eagerly would force a second "remove-then-replace" pass to
|
|
1918
|
+
* apply the channel merges, complicating the place-collection invariants.
|
|
1919
|
+
* Collecting first and merging second keeps the builder's transition set
|
|
1920
|
+
* finalized exactly once.
|
|
1921
|
+
*
|
|
1922
|
+
* Keying the working map by prefixed transition name (rather than by
|
|
1923
|
+
* Transition reference) is also robust against a prior
|
|
1924
|
+
* {@link Instance.bindActions} call that may have rebuilt the renamed-body
|
|
1925
|
+
* transitions, breaking identity equality between the body and the
|
|
1926
|
+
* channel-handle map — but the prefixed names remain stable.
|
|
1879
1927
|
*/
|
|
1880
|
-
|
|
1881
|
-
const
|
|
1882
|
-
const
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1885
|
-
if (
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
);
|
|
1889
|
-
}
|
|
1890
|
-
seenPortNames.add(port.name);
|
|
1891
|
-
if (!bodyPlaces.has(port.place)) {
|
|
1928
|
+
composeInternal(instance, portMappings, channelBindings) {
|
|
1929
|
+
const iface = instance.def.iface;
|
|
1930
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
1931
|
+
for (const [portName, callerPlace] of portMappings) {
|
|
1932
|
+
const port = iface.port(portName);
|
|
1933
|
+
if (port === void 0) {
|
|
1934
|
+
const knownPorts = [];
|
|
1935
|
+
for (const p of iface.ports.values()) knownPorts.push(p.name);
|
|
1892
1936
|
throw new Error(
|
|
1893
|
-
`
|
|
1937
|
+
`compose: no port named '${portName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known ports: [${knownPorts.join(", ")}]`
|
|
1894
1938
|
);
|
|
1895
1939
|
}
|
|
1940
|
+
const ifacePlace = instance.port(portName);
|
|
1941
|
+
mergeMap.set(ifacePlace.name, callerPlace);
|
|
1896
1942
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1943
|
+
return this.applyComposition(instance, mergeMap, channelBindings);
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Shared post-mergeMap pipeline: rewrites renamed-body transitions
|
|
1947
|
+
* through `mergeMap`, applies channel merges per **MOD-021**, and adds
|
|
1948
|
+
* the surviving transitions to the builder.
|
|
1949
|
+
*
|
|
1950
|
+
* Used by both the explicit-binding path (`composeInternal`) and the
|
|
1951
|
+
* auto-compose path (`composeAuto` per **MOD-024**). The two paths differ
|
|
1952
|
+
* only in how the (renamed Place name → host Place) `mergeMap` is built.
|
|
1953
|
+
*/
|
|
1954
|
+
applyComposition(instance, mergeMap, channelBindings) {
|
|
1955
|
+
const iface = instance.def.iface;
|
|
1956
|
+
const rewrittenByName = /* @__PURE__ */ new Map();
|
|
1957
|
+
for (const t of instance.renamedBody.transitions) {
|
|
1958
|
+
rewrittenByName.set(t.name, substitutePlaces(t, mergeMap));
|
|
1959
|
+
}
|
|
1960
|
+
for (const [channelName, callerTrans] of channelBindings) {
|
|
1961
|
+
let instanceRenamedChannel;
|
|
1962
|
+
try {
|
|
1963
|
+
instanceRenamedChannel = instance.channel(channelName);
|
|
1964
|
+
} catch (cause) {
|
|
1965
|
+
const knownChannels = [];
|
|
1966
|
+
for (const c of iface.channels.values()) knownChannels.push(c.name);
|
|
1967
|
+
const err = new Error(
|
|
1968
|
+
`compose: no channel named '${channelName}' on subnet '${instance.def.name}' (instance prefix '${instance.prefix}'). Known channels: [${knownChannels.join(", ")}]`
|
|
1902
1969
|
);
|
|
1970
|
+
err.cause = cause;
|
|
1971
|
+
throw err;
|
|
1903
1972
|
}
|
|
1904
|
-
|
|
1905
|
-
if (
|
|
1973
|
+
const rewrittenInstanceChannel = rewrittenByName.get(instanceRenamedChannel.name);
|
|
1974
|
+
if (rewrittenInstanceChannel === void 0) {
|
|
1906
1975
|
throw new Error(
|
|
1907
|
-
`
|
|
1976
|
+
`compose: channel '${channelName}' resolved to a transition '${instanceRenamedChannel.name}' that is not present in the renamed body. This indicates a SubnetDef invariant violation.`
|
|
1908
1977
|
);
|
|
1909
1978
|
}
|
|
1979
|
+
const merged = mergeTransitions(callerTrans, rewrittenInstanceChannel, callerTrans.name);
|
|
1980
|
+
rewrittenByName.delete(instanceRenamedChannel.name);
|
|
1981
|
+
this._transitions.delete(callerTrans);
|
|
1982
|
+
this.transition(merged);
|
|
1983
|
+
}
|
|
1984
|
+
for (const rewritten of rewrittenByName.values()) {
|
|
1985
|
+
this.transition(rewritten);
|
|
1910
1986
|
}
|
|
1911
|
-
return new _SubnetDef(SUBNET_DEF_KEY, net.name, net, iface);
|
|
1912
|
-
}
|
|
1913
|
-
};
|
|
1914
|
-
var SubnetDefBuilder = class {
|
|
1915
|
-
_name;
|
|
1916
|
-
_bodyBuilder;
|
|
1917
|
-
_ports = [];
|
|
1918
|
-
_channels = [];
|
|
1919
|
-
constructor(name) {
|
|
1920
|
-
this._name = name;
|
|
1921
|
-
this._bodyBuilder = PetriNet.builder(name);
|
|
1922
|
-
}
|
|
1923
|
-
// -------- body construction (delegates to PetriNet.Builder) --------
|
|
1924
|
-
transition(transition) {
|
|
1925
|
-
this._bodyBuilder.transition(transition);
|
|
1926
|
-
return this;
|
|
1927
|
-
}
|
|
1928
|
-
transitions(...transitions) {
|
|
1929
|
-
this._bodyBuilder.transitions(...transitions);
|
|
1930
|
-
return this;
|
|
1931
|
-
}
|
|
1932
|
-
place(place2) {
|
|
1933
|
-
this._bodyBuilder.place(place2);
|
|
1934
|
-
return this;
|
|
1935
|
-
}
|
|
1936
|
-
// -------- interface declarations --------
|
|
1937
|
-
inputPort(name, place2) {
|
|
1938
|
-
this._ports.push({ name, direction: "input", place: place2 });
|
|
1939
1987
|
return this;
|
|
1940
1988
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1989
|
+
fuse(...args) {
|
|
1990
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
1991
|
+
const declarer = args[0];
|
|
1992
|
+
const fb = FusionSet.builder(this._name + "-fusion");
|
|
1993
|
+
declarer(fb);
|
|
1994
|
+
this._fusionSets.push(fb.build());
|
|
1995
|
+
return this;
|
|
1996
|
+
}
|
|
1997
|
+
for (const s of args) {
|
|
1998
|
+
if (s === void 0 || s === null) {
|
|
1999
|
+
throw new Error("fuse: fusion set must not be null");
|
|
2000
|
+
}
|
|
2001
|
+
this._fusionSets.push(s);
|
|
2002
|
+
}
|
|
1943
2003
|
return this;
|
|
1944
2004
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2005
|
+
/**
|
|
2006
|
+
* Builds the immutable {@link PetriNet}, applying fusion resolution (per
|
|
2007
|
+
* **MOD-061**) AFTER all transition/composition accumulation:
|
|
2008
|
+
*
|
|
2009
|
+
* 1. Detect overlapping fusion sets — a single place declared in two sets
|
|
2010
|
+
* is rejected with an `Error`.
|
|
2011
|
+
* 2. Build the `non-canonical → canonical` substitution map across all
|
|
2012
|
+
* sets, keyed by non-canonical place name (matching the rewriter's
|
|
2013
|
+
* Map<string, Place<unknown>> convention — TypeScript Place identity is
|
|
2014
|
+
* name-based per `runtime/compiled-net.ts`).
|
|
2015
|
+
* 3. Walk every transition through {@link applyFusion} to rewrite arc place
|
|
2016
|
+
* references.
|
|
2017
|
+
* 4. Re-derive the place set from the rewritten transitions plus any
|
|
2018
|
+
* caller-declared standalone places, dropping non-canonical members.
|
|
2019
|
+
* Caller-declared standalone places that happen to be non-canonical
|
|
2020
|
+
* members are also dropped.
|
|
2021
|
+
*
|
|
2022
|
+
* If no fusion sets were registered, the build is the trivial
|
|
2023
|
+
* `new PetriNet(...)` — the fusion machinery has no per-build cost when
|
|
2024
|
+
* unused.
|
|
2025
|
+
*
|
|
2026
|
+
* @throws when two fusion sets share a place
|
|
2027
|
+
*/
|
|
2028
|
+
build() {
|
|
2029
|
+
const membership = this.resolveSubnetMembership();
|
|
2030
|
+
if (this._fusionSets.length === 0) {
|
|
2031
|
+
return new PetriNet(
|
|
2032
|
+
PETRI_NET_KEY,
|
|
2033
|
+
this._name,
|
|
2034
|
+
this._places,
|
|
2035
|
+
this._transitions,
|
|
2036
|
+
membership
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
return this.buildWithFusion(membership);
|
|
1948
2040
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2041
|
+
/**
|
|
2042
|
+
* @internal Resolves the per-compose contributions recorded by
|
|
2043
|
+
* `composeDirect` into the final node-name → subnet-name membership map per
|
|
2044
|
+
* **MOD-026**. A node contributed by exactly one subnet maps to that subnet;
|
|
2045
|
+
* a place contributed by two or more subnets is a shared rendezvous place
|
|
2046
|
+
* and is omitted (it renders top-level, outside any cluster). Returns an
|
|
2047
|
+
* empty map — the common case — when no subnet was composed directly.
|
|
2048
|
+
*/
|
|
2049
|
+
resolveSubnetMembership() {
|
|
2050
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
2051
|
+
for (const [nodeName, owners] of this._subnetContributions) {
|
|
2052
|
+
if (owners.size === 1) {
|
|
2053
|
+
resolved.set(nodeName, owners.values().next().value);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return resolved;
|
|
1952
2057
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2058
|
+
/**
|
|
2059
|
+
* @internal Drops membership entries for places removed by fusion: a
|
|
2060
|
+
* non-canonical fused member no longer exists in the net, so its
|
|
2061
|
+
* `node-name → subnet` entry would dangle. The surviving canonical place
|
|
2062
|
+
* keeps its own entry. Per **MOD-026**.
|
|
2063
|
+
*/
|
|
2064
|
+
static filterFusedMembership(membership, nonCanonicalNames) {
|
|
2065
|
+
if (membership.size === 0 || nonCanonicalNames.size === 0) {
|
|
2066
|
+
return membership;
|
|
2067
|
+
}
|
|
2068
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
2069
|
+
for (const [key, value] of membership) {
|
|
2070
|
+
if (!nonCanonicalNames.has(key)) {
|
|
2071
|
+
filtered.set(key, value);
|
|
1962
2072
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2073
|
+
}
|
|
2074
|
+
return filtered;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* @internal Fusion-resolution pass per **MOD-061**. Split out from
|
|
2078
|
+
* {@link build} so the no-fusion fast path stays trivial.
|
|
2079
|
+
*/
|
|
2080
|
+
buildWithFusion(membership) {
|
|
2081
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
2082
|
+
for (const set of this._fusionSets) {
|
|
2083
|
+
for (const member of set.members) {
|
|
2084
|
+
const prior = ownership.get(member.name);
|
|
2085
|
+
if (prior !== void 0 && prior !== set) {
|
|
2086
|
+
throw new Error(
|
|
2087
|
+
`Fusion overlap: place '${member.name}' appears in two fusion sets ('${prior.name}' and '${set.name}'). A place may appear in at most one fusion set (MOD-060).`
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
ownership.set(member.name, set);
|
|
1968
2091
|
}
|
|
1969
2092
|
}
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2093
|
+
const fusionMap = /* @__PURE__ */ new Map();
|
|
2094
|
+
const nonCanonicalNames = /* @__PURE__ */ new Set();
|
|
2095
|
+
for (const set of this._fusionSets) {
|
|
2096
|
+
const canonical = set.canonical;
|
|
2097
|
+
for (const nc of set.nonCanonical()) {
|
|
2098
|
+
fusionMap.set(nc.name, canonical);
|
|
2099
|
+
nonCanonicalNames.add(nc.name);
|
|
1974
2100
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2101
|
+
}
|
|
2102
|
+
const rewrittenTransitions = applyFusion(this._transitions, fusionMap);
|
|
2103
|
+
const rebuiltPlaces = /* @__PURE__ */ new Set();
|
|
2104
|
+
for (const p of this._places) {
|
|
2105
|
+
if (!nonCanonicalNames.has(p.name)) {
|
|
2106
|
+
rebuiltPlaces.add(p);
|
|
1980
2107
|
}
|
|
1981
2108
|
}
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2109
|
+
for (const t of rewrittenTransitions) {
|
|
2110
|
+
for (const spec of t.inputSpecs) rebuiltPlaces.add(spec.place);
|
|
2111
|
+
for (const p of t.outputPlaces()) rebuiltPlaces.add(p);
|
|
2112
|
+
for (const inh of t.inhibitors) rebuiltPlaces.add(inh.place);
|
|
2113
|
+
for (const r of t.reads) rebuiltPlaces.add(r.place);
|
|
2114
|
+
for (const r of t.resets) rebuiltPlaces.add(r.place);
|
|
2115
|
+
}
|
|
2116
|
+
return new PetriNet(
|
|
2117
|
+
PETRI_NET_KEY,
|
|
2118
|
+
this._name,
|
|
2119
|
+
rebuiltPlaces,
|
|
2120
|
+
rewrittenTransitions,
|
|
2121
|
+
_PetriNetBuilder.filterFusedMembership(membership, nonCanonicalNames)
|
|
2122
|
+
);
|
|
1987
2123
|
}
|
|
1988
2124
|
};
|
|
1989
|
-
function
|
|
1990
|
-
|
|
1991
|
-
|
|
2125
|
+
function rebuildWithAction(t, action) {
|
|
2126
|
+
const builder = Transition.builder(t.name).timing(t.timing).priority(t.priority).action(action);
|
|
2127
|
+
if (t.inputSpecs.length > 0) {
|
|
2128
|
+
builder.inputs(...t.inputSpecs);
|
|
1992
2129
|
}
|
|
1993
|
-
if (
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2130
|
+
if (t.outputSpec !== null) {
|
|
2131
|
+
builder.outputs(t.outputSpec);
|
|
2132
|
+
}
|
|
2133
|
+
for (const inh of t.inhibitors) {
|
|
2134
|
+
builder.inhibitor(inh.place);
|
|
2135
|
+
}
|
|
2136
|
+
for (const r of t.reads) {
|
|
2137
|
+
builder.read(r.place);
|
|
1997
2138
|
}
|
|
2139
|
+
for (const r of t.resets) {
|
|
2140
|
+
builder.reset(r.place);
|
|
2141
|
+
}
|
|
2142
|
+
return builder.build();
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/core/subnet.ts
|
|
2146
|
+
function closedSubnet(net) {
|
|
2147
|
+
return { kind: "closed", net };
|
|
2148
|
+
}
|
|
2149
|
+
function openSubnet(def) {
|
|
2150
|
+
return { kind: "open", def };
|
|
1998
2151
|
}
|
|
1999
2152
|
|
|
2000
2153
|
// src/runtime/marking.ts
|