@greenbytehq/cpn.js 0.1.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/LICENSE +9 -0
- package/README.md +206 -0
- package/dist/cjs/index.cjs +483 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.d.ts +159 -0
- package/dist/esm/index.js +432 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/engine.d.ts +62 -0
- package/dist/types/engine.d.ts.map +1 -0
- package/dist/types/examples.test.d.ts +2 -0
- package/dist/types/examples.test.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/multiset.d.ts +47 -0
- package/dist/types/multiset.d.ts.map +1 -0
- package/dist/types/net-types.d.ts +33 -0
- package/dist/types/net-types.d.ts.map +1 -0
- package/dist/types/semantics.test.d.ts +7 -0
- package/dist/types/semantics.test.d.ts.map +1 -0
- package/dist/types/sml.d.ts +7 -0
- package/dist/types/sml.d.ts.map +1 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types/types.d.ts.map +1 -0
- package/examples/lib.mjs +40 -0
- package/examples/producer-consumer.mjs +82 -0
- package/examples/sml-integration.mjs +72 -0
- package/examples/traffic-light.mjs +53 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Licensing Termsm
|
|
2
|
+
|
|
3
|
+
This project is dual-licensed under the MIT License and the GNU General Public License v3.0 (GPLv3). You may choose to use this project under the terms of either license.
|
|
4
|
+
|
|
5
|
+
* For the MIT License terms, see the `LICENSE-MIT` file.
|
|
6
|
+
* For the GPLv3 License terms, see the `LICENSE-GPL` file.
|
|
7
|
+
|
|
8
|
+
Please note: If you choose the MIT License, but utilize this project alongside its optional peer dependency `@sosml/interpreter`, your combined work will become subject to the copyleft terms of the GPLv3.
|
|
9
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @greenByteHQ/cpn-semantics
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@greenByteHQ/cpn-semantics)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Pure **Coloured Petri Net (CPN)** simulation engine. Computes enabled bindings, fires transitions, and manages multiset token flow — with no UI, no app-specific types, and no side effects.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @greenByteHQ/cpn-semantics
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Core Types
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import type { Place, Transition, Arc, NetLike, Marking, Binding, Multiset } from '@greenByteHQ/cpn-semantics';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`NetLike` is intentionally structural and map-based:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
type NetLike = {
|
|
24
|
+
places: ReadonlyMap<string, Place>;
|
|
25
|
+
transitions: ReadonlyMap<string, Transition>;
|
|
26
|
+
arcs: ReadonlyMap<string, Arc>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Marking = ReadonlyMap<string, Multiset>; // placeId -> token multiset
|
|
30
|
+
type Binding = ReadonlyMap<string, string>; // variable -> serialized token key
|
|
31
|
+
type Multiset = Map<string, number>; // token key -> count
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Multiset Operations
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { msAdd, msSubtract, msScale, msContains, msIsEmpty, msEquals, EMPTY_MULTISET } from '@greenByteHQ/cpn-semantics';
|
|
40
|
+
|
|
41
|
+
const a: Multiset = new Map([['red', 2], ['blue', 1]]);
|
|
42
|
+
const b: Multiset = new Map([['red', 1]]);
|
|
43
|
+
|
|
44
|
+
msAdd(a, b); // Map { red→3, blue→1 }
|
|
45
|
+
msSubtract(a, b); // Map { red→1, blue→1 }
|
|
46
|
+
msScale(a, 2); // Map { red→4, blue→2 }
|
|
47
|
+
msContains(a, b); // true
|
|
48
|
+
msIsEmpty(a); // false
|
|
49
|
+
msEquals(a, a); // true
|
|
50
|
+
EMPTY_MULTISET; // Map {}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Simulation API
|
|
56
|
+
|
|
57
|
+
### `findEnabledBindings`
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { findEnabledBindings } from '@greenByteHQ/cpn-semantics';
|
|
61
|
+
|
|
62
|
+
const bindings: Binding[] = findEnabledBindings(
|
|
63
|
+
net, // NetLike
|
|
64
|
+
marking, // Marking
|
|
65
|
+
transitionId, // string
|
|
66
|
+
evalExpr, // optional EvalExprFn
|
|
67
|
+
evalGuard, // optional EvalGuardFn
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Returns all variable bindings under which the given transition is enabled.
|
|
72
|
+
|
|
73
|
+
### `computeEnabledSet`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { computeEnabledSet } from '@greenByteHQ/cpn-semantics';
|
|
77
|
+
|
|
78
|
+
const enabled: ReadonlySet<string> =
|
|
79
|
+
computeEnabledSet(net, marking, evalExpr, evalGuard);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Returns all transition IDs that are currently fireable.
|
|
83
|
+
|
|
84
|
+
### `fire`
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { fire } from '@greenByteHQ/cpn-semantics';
|
|
88
|
+
|
|
89
|
+
const newMarking: Marking = await fire(
|
|
90
|
+
net, // NetLike
|
|
91
|
+
marking, // Marking (immutable — returns new copy)
|
|
92
|
+
transitionId, // string
|
|
93
|
+
binding, // Binding
|
|
94
|
+
evalExpr, // optional EvalExprFn
|
|
95
|
+
callbacks, // optional FireCallbacks
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Fires the given transition and returns the updated marking.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## SML Evaluation
|
|
104
|
+
|
|
105
|
+
SML expression and guard evaluation defaults to `@sosml/interpreter`.
|
|
106
|
+
`evalExprWithSosml` supports CPN multiset inscriptions such as `1\`x ++ 2\`(x + 1)` by evaluating the SML sub-expressions with SOSML.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { evalExprWithSosml, evalGuardWithSosml } from '@greenByteHQ/cpn-semantics';
|
|
110
|
+
|
|
111
|
+
await evalExprWithSosml('1`x ++ 2`(x + 1)', 'sml', new Map([['x', '3']]));
|
|
112
|
+
await evalGuardWithSosml('x + 1 = 4', 'sml', new Map([['x', '3']]));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Python or other non-SML languages are still supplied by the host app through `EvalExprFn` and `EvalGuardFn`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Fire Callbacks
|
|
120
|
+
|
|
121
|
+
Use callbacks to observe or customize firing. The semantics package does not
|
|
122
|
+
interpret application-specific transition classes or arc extensions; layer those
|
|
123
|
+
behaviors in callbacks.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import type { FireCallbacks } from '@greenByteHQ/cpn-semantics';
|
|
127
|
+
|
|
128
|
+
const callbacks: FireCallbacks = {
|
|
129
|
+
beforeFire: ({ transition, binding, marking, net }) => {
|
|
130
|
+
// called before input tokens are consumed
|
|
131
|
+
},
|
|
132
|
+
midFire: async ({ transition, binding, marking, markingAfterConsume, net }) => {
|
|
133
|
+
if (transition.id !== 'special-transition') return null;
|
|
134
|
+
|
|
135
|
+
// Return Map<placeId, Multiset> to own output-side token placement.
|
|
136
|
+
// null/undefined falls through to standard TP inscription evaluation.
|
|
137
|
+
return new Map([['p_out', new Map([['done', 1]])]]);
|
|
138
|
+
},
|
|
139
|
+
afterFire: ({ transition, binding, markingAfterConsume, markingAfterFire, net }) => {
|
|
140
|
+
// called after output tokens are added
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await fire(net, marking, transitionId, binding, evalExpr, callbacks);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Classical Examples
|
|
150
|
+
|
|
151
|
+
The package includes executable examples under `examples/`. They are intentionally
|
|
152
|
+
not exported from the package API; they show how to build and run nets with the
|
|
153
|
+
public primitives.
|
|
154
|
+
|
|
155
|
+
Run all examples:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pnpm --filter @greenByteHQ/cpn-semantics run examples
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Or build once and run an individual example:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm --filter @greenByteHQ/cpn-semantics run build
|
|
165
|
+
node packages/cpn-semantics/examples/traffic-light.mjs
|
|
166
|
+
node packages/cpn-semantics/examples/producer-consumer.mjs
|
|
167
|
+
node packages/cpn-semantics/examples/sml-integration.mjs
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Traffic Light
|
|
171
|
+
|
|
172
|
+
One coloured token represents the current light state. Firing `advance` cycles
|
|
173
|
+
`red -> green -> yellow -> red`.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
node packages/cpn-semantics/examples/traffic-light.mjs
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Bounded Producer-Consumer
|
|
180
|
+
|
|
181
|
+
This net models a bounded buffer with `slots`, `buffer`, and `consumed` places.
|
|
182
|
+
`produce` consumes one free slot and creates an `item`; `consume` consumes one
|
|
183
|
+
`item`, restores one slot, and records the consumed item.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
node packages/cpn-semantics/examples/producer-consumer.mjs
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### SML Guard + Expression
|
|
190
|
+
|
|
191
|
+
This net starts with integer tokens `2`, `3`, and `8`. The transition has an
|
|
192
|
+
SML guard `n mod 2 = 0`, so only even tokens are enabled. Its output inscription
|
|
193
|
+
is `1\`(n div 2)`, so firing computes the half of each even token.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
node packages/cpn-semantics/examples/sml-integration.mjs
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Each script prints JSON step output containing the fired transition, binding,
|
|
200
|
+
and marking snapshot after each firing.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT © kentis — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
EMPTY_MULTISET: () => EMPTY_MULTISET,
|
|
34
|
+
SmlEvaluationError: () => SmlEvaluationError,
|
|
35
|
+
computeEnabledSet: () => computeEnabledSet,
|
|
36
|
+
evalExprWithSosml: () => evalExprWithSosml,
|
|
37
|
+
evalGuardWithSosml: () => evalGuardWithSosml,
|
|
38
|
+
evalSmlExpression: () => evalSmlExpression,
|
|
39
|
+
evalSmlGuard: () => evalSmlGuard,
|
|
40
|
+
findEnabledBindings: () => findEnabledBindings,
|
|
41
|
+
fire: () => fire,
|
|
42
|
+
msAdd: () => msAdd,
|
|
43
|
+
msContains: () => msContains,
|
|
44
|
+
msEquals: () => msEquals,
|
|
45
|
+
msIsEmpty: () => msIsEmpty,
|
|
46
|
+
msScale: () => msScale,
|
|
47
|
+
msSubtract: () => msSubtract
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
|
|
51
|
+
// src/multiset.ts
|
|
52
|
+
var EMPTY_MULTISET = /* @__PURE__ */ new Map();
|
|
53
|
+
function msAdd(a, b) {
|
|
54
|
+
const result = new Map(a);
|
|
55
|
+
for (const [k, v] of b) {
|
|
56
|
+
result.set(k, (result.get(k) ?? 0) + v);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function msSubtract(a, b) {
|
|
61
|
+
const result = new Map(a);
|
|
62
|
+
for (const [k, v] of b) {
|
|
63
|
+
const have = result.get(k) ?? 0;
|
|
64
|
+
const count = have - v;
|
|
65
|
+
if (count < 0) {
|
|
66
|
+
throw new Error(`Multiset underflow for token "${k}": need ${v}, have ${have}`);
|
|
67
|
+
}
|
|
68
|
+
if (count === 0) {
|
|
69
|
+
result.delete(k);
|
|
70
|
+
} else {
|
|
71
|
+
result.set(k, count);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function msScale(ms, n) {
|
|
77
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
78
|
+
throw new RangeError(`msScale: n must be a non-negative integer, got ${n}`);
|
|
79
|
+
}
|
|
80
|
+
if (n === 0) return EMPTY_MULTISET;
|
|
81
|
+
const result = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const [k, v] of ms) {
|
|
83
|
+
result.set(k, v * n);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
function msContains(a, b) {
|
|
88
|
+
for (const [k, v] of b) {
|
|
89
|
+
if ((a.get(k) ?? 0) < v) return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function msIsEmpty(ms) {
|
|
94
|
+
return ms.size === 0;
|
|
95
|
+
}
|
|
96
|
+
function msEquals(a, b) {
|
|
97
|
+
if (a.size !== b.size) return false;
|
|
98
|
+
for (const [k, v] of a) {
|
|
99
|
+
if (b.get(k) !== v) return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/sml.ts
|
|
105
|
+
var import_interpreter = __toESM(require("@sosml/interpreter"), 1);
|
|
106
|
+
var { getFirstState, interpret } = import_interpreter.default;
|
|
107
|
+
var SmlEvaluationError = class extends Error {
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "SmlEvaluationError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var RESULT_NAME = "cpnResult";
|
|
114
|
+
function escapeSmlString(value) {
|
|
115
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
116
|
+
}
|
|
117
|
+
function tokenKeyToSmlLiteral(value) {
|
|
118
|
+
if (value === "true" || value === "false") return value;
|
|
119
|
+
if (value === "()") return "()";
|
|
120
|
+
if (/^-?\d+$/.test(value)) return value.replace(/^-/, "~");
|
|
121
|
+
if (/^\(.*\)$/.test(value)) {
|
|
122
|
+
return value.replace(/(^|[(,])-?\d+/g, (match) => match.replace("-", "~"));
|
|
123
|
+
}
|
|
124
|
+
return escapeSmlString(value);
|
|
125
|
+
}
|
|
126
|
+
function bindingPrelude(binding) {
|
|
127
|
+
return [...binding].map(([name, value]) => `val ${name} = ${tokenKeyToSmlLiteral(value)};`).join("\n");
|
|
128
|
+
}
|
|
129
|
+
function evaluateValue(expr, binding) {
|
|
130
|
+
const source = `${bindingPrelude(binding)}
|
|
131
|
+
val ${RESULT_NAME} = ${expr};`;
|
|
132
|
+
const result = interpret(source, getFirstState(), { allowSuccessorML: true, allowVector: true });
|
|
133
|
+
if (result.evaluationErrored) {
|
|
134
|
+
throw new SmlEvaluationError(String(result.error ?? "SML evaluation failed"));
|
|
135
|
+
}
|
|
136
|
+
const bindingValue = result.state.getDynamicValue(RESULT_NAME)?.[0];
|
|
137
|
+
if (!bindingValue) {
|
|
138
|
+
throw new SmlEvaluationError("SML evaluation did not produce a result");
|
|
139
|
+
}
|
|
140
|
+
return { value: bindingValue, state: result.state };
|
|
141
|
+
}
|
|
142
|
+
function unquoteSmlString(value) {
|
|
143
|
+
return value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
144
|
+
}
|
|
145
|
+
function valueToTokenKey(value, state) {
|
|
146
|
+
const typed = value;
|
|
147
|
+
switch (value.typeName()) {
|
|
148
|
+
case "Integer":
|
|
149
|
+
return String(typed.value);
|
|
150
|
+
case "BoolValue":
|
|
151
|
+
return String(typed.value);
|
|
152
|
+
case "StringValue":
|
|
153
|
+
return typeof typed.value === "string" ? typed.value : unquoteSmlString(value.toString(state));
|
|
154
|
+
case "RecordValue": {
|
|
155
|
+
const entries = typed.entries;
|
|
156
|
+
if (!entries || entries.size === 0) return "()";
|
|
157
|
+
const tupleItems = [];
|
|
158
|
+
for (let i = 1; i <= entries.size; i += 1) {
|
|
159
|
+
const item = entries.get(String(i));
|
|
160
|
+
if (!item) break;
|
|
161
|
+
tupleItems.push(valueToTokenKey(item, state));
|
|
162
|
+
}
|
|
163
|
+
if (tupleItems.length === entries.size && tupleItems.length > 0) {
|
|
164
|
+
return `(${tupleItems.join(",")})`;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return value.toString(state);
|
|
172
|
+
}
|
|
173
|
+
function valueToBoolean(value) {
|
|
174
|
+
if (value.typeName() !== "BoolValue") {
|
|
175
|
+
throw new SmlEvaluationError(`Guard must evaluate to bool, got ${value.typeName()}`);
|
|
176
|
+
}
|
|
177
|
+
return Boolean(value.value);
|
|
178
|
+
}
|
|
179
|
+
function addToken(result, key, count) {
|
|
180
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
181
|
+
throw new SmlEvaluationError(`Multiset count must be a non-negative integer, got ${count}`);
|
|
182
|
+
}
|
|
183
|
+
if (count === 0) return;
|
|
184
|
+
result.set(key, (result.get(key) ?? 0) + count);
|
|
185
|
+
}
|
|
186
|
+
function splitTopLevelUnion(expr) {
|
|
187
|
+
const parts = [];
|
|
188
|
+
let start = 0;
|
|
189
|
+
let depth = 0;
|
|
190
|
+
let inString = false;
|
|
191
|
+
let escaped = false;
|
|
192
|
+
for (let i = 0; i < expr.length; i += 1) {
|
|
193
|
+
const ch = expr[i];
|
|
194
|
+
if (inString) {
|
|
195
|
+
if (escaped) escaped = false;
|
|
196
|
+
else if (ch === "\\") escaped = true;
|
|
197
|
+
else if (ch === '"') inString = false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (ch === '"') {
|
|
201
|
+
inString = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
205
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth -= 1;
|
|
206
|
+
else if (ch === "+" && expr[i + 1] === "+" && depth === 0) {
|
|
207
|
+
parts.push(expr.slice(start, i).trim());
|
|
208
|
+
start = i + 2;
|
|
209
|
+
i += 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
parts.push(expr.slice(start).trim());
|
|
213
|
+
return parts.filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
function splitMultisetTerm(term) {
|
|
216
|
+
let depth = 0;
|
|
217
|
+
let inString = false;
|
|
218
|
+
let escaped = false;
|
|
219
|
+
for (let i = 0; i < term.length; i += 1) {
|
|
220
|
+
const ch = term[i];
|
|
221
|
+
if (inString) {
|
|
222
|
+
if (escaped) escaped = false;
|
|
223
|
+
else if (ch === "\\") escaped = true;
|
|
224
|
+
else if (ch === '"') inString = false;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (ch === '"') {
|
|
228
|
+
inString = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
232
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth -= 1;
|
|
233
|
+
else if (ch === "`" && depth === 0) {
|
|
234
|
+
return { countExpr: term.slice(0, i).trim(), valueExpr: term.slice(i + 1).trim() };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
async function evalSmlExpression(expr, binding) {
|
|
240
|
+
const trimmed = expr.trim();
|
|
241
|
+
if (!trimmed || trimmed === "empty") return /* @__PURE__ */ new Map();
|
|
242
|
+
const result = /* @__PURE__ */ new Map();
|
|
243
|
+
for (const part of splitTopLevelUnion(trimmed)) {
|
|
244
|
+
const term = splitMultisetTerm(part);
|
|
245
|
+
if (!term) {
|
|
246
|
+
const { value, state } = evaluateValue(part, binding);
|
|
247
|
+
addToken(result, valueToTokenKey(value, state), 1);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const countResult = evaluateValue(term.countExpr, binding);
|
|
251
|
+
if (countResult.value.typeName() !== "Integer") {
|
|
252
|
+
throw new SmlEvaluationError(`Multiset count must evaluate to int, got ${countResult.value.typeName()}`);
|
|
253
|
+
}
|
|
254
|
+
const valueResult = evaluateValue(term.valueExpr, binding);
|
|
255
|
+
addToken(
|
|
256
|
+
result,
|
|
257
|
+
valueToTokenKey(valueResult.value, valueResult.state),
|
|
258
|
+
Number(countResult.value.value)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
async function evalSmlGuard(expr, binding) {
|
|
264
|
+
if (!expr.trim()) return true;
|
|
265
|
+
return valueToBoolean(evaluateValue(expr, binding).value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/engine.ts
|
|
269
|
+
var evalExprWithSosml = async (expr, lang, binding) => {
|
|
270
|
+
if (lang !== "sml") {
|
|
271
|
+
throw new Error(`No ${lang} expression evaluator is configured in @greenByteHQ/cpn-semantics`);
|
|
272
|
+
}
|
|
273
|
+
return evalSmlExpression(expr, binding);
|
|
274
|
+
};
|
|
275
|
+
var evalGuardWithSosml = async (expr, lang, binding) => {
|
|
276
|
+
switch (lang) {
|
|
277
|
+
case "sml":
|
|
278
|
+
return evalSmlGuard(expr, binding);
|
|
279
|
+
case "python":
|
|
280
|
+
throw new Error(`No ${lang} guard evaluator is configured in @greenByteHQ/cpn-semantics`);
|
|
281
|
+
case "js":
|
|
282
|
+
throw new Error(`No ${lang} guard evaluator is configured in @greenByteHQ/cpn-semantics`);
|
|
283
|
+
default:
|
|
284
|
+
throw new Error(`Unknown language ${lang} in guard expression`);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
function extractVariables(expr) {
|
|
288
|
+
const KEYWORDS = /* @__PURE__ */ new Set([
|
|
289
|
+
"let",
|
|
290
|
+
"val",
|
|
291
|
+
"in",
|
|
292
|
+
"end",
|
|
293
|
+
"if",
|
|
294
|
+
"then",
|
|
295
|
+
"else",
|
|
296
|
+
"andalso",
|
|
297
|
+
"orelse",
|
|
298
|
+
"not",
|
|
299
|
+
"div",
|
|
300
|
+
"mod",
|
|
301
|
+
"true",
|
|
302
|
+
"false",
|
|
303
|
+
"empty"
|
|
304
|
+
]);
|
|
305
|
+
const matches = expr.match(/[a-zA-Z_][a-zA-Z0-9_']*/g) ?? [];
|
|
306
|
+
return [...new Set(matches)].filter((m) => !KEYWORDS.has(m));
|
|
307
|
+
}
|
|
308
|
+
function buildArcCandidates(arc, marking) {
|
|
309
|
+
const sourceMarking = marking.get(arc.sourceId) ?? EMPTY_MULTISET;
|
|
310
|
+
const tokenKeys = [...sourceMarking.keys()];
|
|
311
|
+
if (arc.inscription.trim() === "") {
|
|
312
|
+
return [/* @__PURE__ */ new Map()];
|
|
313
|
+
}
|
|
314
|
+
const vars = extractVariables(arc.inscription);
|
|
315
|
+
if (vars.length === 0) {
|
|
316
|
+
return [/* @__PURE__ */ new Map()];
|
|
317
|
+
}
|
|
318
|
+
if (vars.length === 1) {
|
|
319
|
+
return tokenKeys.map((key) => /* @__PURE__ */ new Map([[vars[0], key]]));
|
|
320
|
+
}
|
|
321
|
+
let candidates = [/* @__PURE__ */ new Map()];
|
|
322
|
+
for (const varName of vars) {
|
|
323
|
+
const next = [];
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
for (const key of tokenKeys) {
|
|
326
|
+
if (candidate.has(varName) && candidate.get(varName) !== key) continue;
|
|
327
|
+
const extended = new Map(candidate);
|
|
328
|
+
extended.set(varName, key);
|
|
329
|
+
next.push(extended);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
candidates = next;
|
|
333
|
+
}
|
|
334
|
+
return candidates;
|
|
335
|
+
}
|
|
336
|
+
function mergeBindings(perArcCandidates) {
|
|
337
|
+
let combined = [/* @__PURE__ */ new Map()];
|
|
338
|
+
for (const arcCandidates of perArcCandidates) {
|
|
339
|
+
const next = [];
|
|
340
|
+
for (const existing of combined) {
|
|
341
|
+
for (const arcCandidate of arcCandidates) {
|
|
342
|
+
let consistent = true;
|
|
343
|
+
for (const [k, v] of arcCandidate) {
|
|
344
|
+
if (existing.has(k) && existing.get(k) !== v) {
|
|
345
|
+
consistent = false;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!consistent) continue;
|
|
350
|
+
next.push(new Map([...existing, ...arcCandidate]));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
combined = next;
|
|
354
|
+
}
|
|
355
|
+
return combined;
|
|
356
|
+
}
|
|
357
|
+
async function findEnabledBindings(net, marking, transitionId, evalExpr = evalExprWithSosml, evalGuard = evalGuardWithSosml) {
|
|
358
|
+
const transition = net.transitions.get(transitionId);
|
|
359
|
+
if (!transition) return [];
|
|
360
|
+
const ptArcs = [...net.arcs.values()].filter(
|
|
361
|
+
(a) => a.kind === "PT" && a.targetId === transitionId
|
|
362
|
+
);
|
|
363
|
+
if (ptArcs.length === 0) {
|
|
364
|
+
const b = /* @__PURE__ */ new Map();
|
|
365
|
+
if (transition.guard.trim()) {
|
|
366
|
+
const lang = transition.guardLang ?? "sml" /* SML */;
|
|
367
|
+
const guardOk = await evalGuard(transition.guard, lang, b);
|
|
368
|
+
return guardOk ? [b] : [];
|
|
369
|
+
}
|
|
370
|
+
return [b];
|
|
371
|
+
}
|
|
372
|
+
const perArcCandidates = ptArcs.map((arc) => buildArcCandidates(arc, marking));
|
|
373
|
+
const candidateBindings = mergeBindings(perArcCandidates);
|
|
374
|
+
const enabled = [];
|
|
375
|
+
await Promise.all(
|
|
376
|
+
candidateBindings.map(async (b) => {
|
|
377
|
+
const arcChecks = ptArcs.map(async (arc) => {
|
|
378
|
+
const lang = arc.inscriptionLang ?? "sml" /* SML */;
|
|
379
|
+
const inscription = arc.inscription.trim();
|
|
380
|
+
if (!inscription) return true;
|
|
381
|
+
const weight = await evalExpr(inscription, lang, b);
|
|
382
|
+
const available = marking.get(arc.sourceId) ?? EMPTY_MULTISET;
|
|
383
|
+
return msContains(available, weight);
|
|
384
|
+
});
|
|
385
|
+
const arcResults = await Promise.all(arcChecks);
|
|
386
|
+
if (arcResults.some((ok) => !ok)) return;
|
|
387
|
+
if (transition.guard.trim()) {
|
|
388
|
+
const lang = transition.guardLang ?? "sml" /* SML */;
|
|
389
|
+
const guardOk = await evalGuard(transition.guard, lang, b);
|
|
390
|
+
if (!guardOk) return;
|
|
391
|
+
}
|
|
392
|
+
enabled.push(b);
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
return enabled;
|
|
396
|
+
}
|
|
397
|
+
async function fire(net, marking, transitionId, binding, evalExpr = evalExprWithSosml, callbacks = {}) {
|
|
398
|
+
const newMarking = new Map(marking);
|
|
399
|
+
const transition = net.transitions.get(transitionId);
|
|
400
|
+
if (!transition) return newMarking;
|
|
401
|
+
await callbacks.beforeFire?.({ transition, binding, marking, net });
|
|
402
|
+
for (const arc of net.arcs.values()) {
|
|
403
|
+
if (arc.kind !== "PT" || arc.targetId !== transitionId) continue;
|
|
404
|
+
if (arc.readArcGroupId) continue;
|
|
405
|
+
const inscription = arc.inscription.trim();
|
|
406
|
+
if (!inscription) continue;
|
|
407
|
+
const lang = arc.inscriptionLang ?? "sml" /* SML */;
|
|
408
|
+
const weight = await evalExpr(inscription, lang, binding);
|
|
409
|
+
const current = newMarking.get(arc.sourceId) ?? EMPTY_MULTISET;
|
|
410
|
+
newMarking.set(arc.sourceId, msSubtract(current, weight));
|
|
411
|
+
}
|
|
412
|
+
const markingAfterConsume = new Map(newMarking);
|
|
413
|
+
const customResult = await callbacks.midFire?.({
|
|
414
|
+
transition,
|
|
415
|
+
binding,
|
|
416
|
+
marking,
|
|
417
|
+
markingAfterConsume,
|
|
418
|
+
net
|
|
419
|
+
});
|
|
420
|
+
if (customResult !== null && customResult !== void 0) {
|
|
421
|
+
for (const [placeId, tokens] of customResult) {
|
|
422
|
+
const current = newMarking.get(placeId) ?? EMPTY_MULTISET;
|
|
423
|
+
newMarking.set(placeId, msAdd(current, tokens));
|
|
424
|
+
}
|
|
425
|
+
await callbacks.afterFire?.({
|
|
426
|
+
transition,
|
|
427
|
+
binding,
|
|
428
|
+
marking,
|
|
429
|
+
markingAfterConsume,
|
|
430
|
+
markingAfterFire: newMarking,
|
|
431
|
+
net
|
|
432
|
+
});
|
|
433
|
+
return newMarking;
|
|
434
|
+
}
|
|
435
|
+
for (const arc of net.arcs.values()) {
|
|
436
|
+
if (arc.kind !== "TP" || arc.sourceId !== transitionId) continue;
|
|
437
|
+
if (arc.readArcGroupId) continue;
|
|
438
|
+
const inscription = arc.inscription.trim();
|
|
439
|
+
if (!inscription) continue;
|
|
440
|
+
const lang = arc.inscriptionLang ?? "sml" /* SML */;
|
|
441
|
+
const weight = await evalExpr(inscription, lang, binding);
|
|
442
|
+
const current = newMarking.get(arc.targetId) ?? EMPTY_MULTISET;
|
|
443
|
+
newMarking.set(arc.targetId, msAdd(current, weight));
|
|
444
|
+
}
|
|
445
|
+
await callbacks.afterFire?.({
|
|
446
|
+
transition,
|
|
447
|
+
binding,
|
|
448
|
+
marking,
|
|
449
|
+
markingAfterConsume,
|
|
450
|
+
markingAfterFire: newMarking,
|
|
451
|
+
net
|
|
452
|
+
});
|
|
453
|
+
return newMarking;
|
|
454
|
+
}
|
|
455
|
+
async function computeEnabledSet(net, marking, evalExpr = evalExprWithSosml, evalGuard = evalGuardWithSosml) {
|
|
456
|
+
const enabled = /* @__PURE__ */ new Set();
|
|
457
|
+
await Promise.all(
|
|
458
|
+
[...net.transitions.keys()].map(async (tId) => {
|
|
459
|
+
const bindings = await findEnabledBindings(net, marking, tId, evalExpr, evalGuard);
|
|
460
|
+
if (bindings.length > 0) enabled.add(tId);
|
|
461
|
+
})
|
|
462
|
+
);
|
|
463
|
+
return enabled;
|
|
464
|
+
}
|
|
465
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
466
|
+
0 && (module.exports = {
|
|
467
|
+
EMPTY_MULTISET,
|
|
468
|
+
SmlEvaluationError,
|
|
469
|
+
computeEnabledSet,
|
|
470
|
+
evalExprWithSosml,
|
|
471
|
+
evalGuardWithSosml,
|
|
472
|
+
evalSmlExpression,
|
|
473
|
+
evalSmlGuard,
|
|
474
|
+
findEnabledBindings,
|
|
475
|
+
fire,
|
|
476
|
+
msAdd,
|
|
477
|
+
msContains,
|
|
478
|
+
msEquals,
|
|
479
|
+
msIsEmpty,
|
|
480
|
+
msScale,
|
|
481
|
+
msSubtract
|
|
482
|
+
});
|
|
483
|
+
//# sourceMappingURL=index.cjs.map
|