@fairfox/polly 0.67.0 → 0.70.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/src/client/index.js +17 -20
- package/dist/src/client/index.js.map +4 -4
- package/dist/src/mesh.js +85 -11
- package/dist/src/mesh.js.map +7 -6
- package/dist/src/peer.js +80 -6
- package/dist/src/peer.js.map +7 -6
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/shared/lib/crdt-specialised.d.ts +6 -0
- package/dist/src/shared/lib/crdt-state.d.ts +8 -1
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +98 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +0 -4
- package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
- package/dist/tools/test/src/e2e-mesh/index.js +1089 -0
- package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
- package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +70 -0
- package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
- package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
- package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +361 -22
- package/dist/tools/verify/src/cli.js.map +6 -6
- package/dist/tools/verify/src/config.d.ts +26 -1
- package/dist/tools/verify/src/config.js +9 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +30 -0
- package/dist/tools/visualize/src/cli.js +43 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +11 -8
- package/LICENSE +0 -21
- package/README.md +0 -362
|
@@ -798,6 +798,8 @@ var init_tla = __esm(() => {
|
|
|
798
798
|
tabSymmetryEnabled = false;
|
|
799
799
|
tabCount = 0;
|
|
800
800
|
moduleName = "UserApp";
|
|
801
|
+
currentConfig;
|
|
802
|
+
meshSignalDocs = new Map;
|
|
801
803
|
constructor(options) {
|
|
802
804
|
this.options = options;
|
|
803
805
|
}
|
|
@@ -811,6 +813,19 @@ var init_tla = __esm(() => {
|
|
|
811
813
|
if (moduleName) {
|
|
812
814
|
this.moduleName = moduleName;
|
|
813
815
|
}
|
|
816
|
+
this.currentConfig = config;
|
|
817
|
+
this.meshSignalDocs.clear();
|
|
818
|
+
if (this.hasMeshConfig(config)) {
|
|
819
|
+
const declaredDocs = new Set(Object.keys(config.mesh ?? {}));
|
|
820
|
+
const sigs = analysis.meshOrPeerSignals;
|
|
821
|
+
if (sigs) {
|
|
822
|
+
for (const s of sigs) {
|
|
823
|
+
if (declaredDocs.has(s.key)) {
|
|
824
|
+
this.meshSignalDocs.set(s.variableName, s.key);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
814
829
|
this.validateInputs(config, analysis);
|
|
815
830
|
this.extractInvariantsIfEnabled();
|
|
816
831
|
this.generateTemporalPropertiesIfEnabled(analysis);
|
|
@@ -963,7 +978,7 @@ var init_tla = __esm(() => {
|
|
|
963
978
|
this.addMessageTypes(config, analysis);
|
|
964
979
|
this.addTabSymmetry(config);
|
|
965
980
|
this.addStateType(config, analysis);
|
|
966
|
-
this.addVariables();
|
|
981
|
+
this.addVariables(config);
|
|
967
982
|
if (this.temporalProperties.length > 0) {
|
|
968
983
|
this.addDeliveredTracking();
|
|
969
984
|
}
|
|
@@ -1181,6 +1196,74 @@ var init_tla = __esm(() => {
|
|
|
1181
1196
|
this.indent--;
|
|
1182
1197
|
this.line("]");
|
|
1183
1198
|
this.line("");
|
|
1199
|
+
this.addMeshTypes(config);
|
|
1200
|
+
}
|
|
1201
|
+
addMeshTypes(config) {
|
|
1202
|
+
const mesh = config.mesh;
|
|
1203
|
+
if (!mesh || Object.keys(mesh).length === 0)
|
|
1204
|
+
return;
|
|
1205
|
+
const docIds = Object.keys(mesh).sort();
|
|
1206
|
+
this.line("\\* polly#117: declared $meshState documents");
|
|
1207
|
+
this.line(`MeshDocs == {${docIds.map((d) => `"${d}"`).join(", ")}}`);
|
|
1208
|
+
this.line("");
|
|
1209
|
+
for (const docId of docIds) {
|
|
1210
|
+
const fields = mesh[docId];
|
|
1211
|
+
if (!fields)
|
|
1212
|
+
continue;
|
|
1213
|
+
const fieldLines = [];
|
|
1214
|
+
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
1215
|
+
const tlaType = this.fieldConfigToTLAType(`${docId}_${fieldName}`, fieldConfig, config);
|
|
1216
|
+
fieldLines.push(`${this.sanitizeFieldName(fieldName)}: ${tlaType}`);
|
|
1217
|
+
}
|
|
1218
|
+
this.line(`\\* Document type for ${docId}`);
|
|
1219
|
+
this.line(`MeshDoc_${this.sanitizeFieldName(docId)} == [${fieldLines.join(", ")}]`);
|
|
1220
|
+
this.line("");
|
|
1221
|
+
}
|
|
1222
|
+
this.line("\\* Initial mesh-document values (one record per declared docId)");
|
|
1223
|
+
this.line("InitialMesh == [");
|
|
1224
|
+
this.indent++;
|
|
1225
|
+
const initLines = [];
|
|
1226
|
+
for (const docId of docIds) {
|
|
1227
|
+
const fields = mesh[docId];
|
|
1228
|
+
if (!fields)
|
|
1229
|
+
continue;
|
|
1230
|
+
const inner = [];
|
|
1231
|
+
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
1232
|
+
const initVal = this.fieldConfigInitialValue(`${docId}_${fieldName}`, fieldConfig, config);
|
|
1233
|
+
inner.push(`${this.sanitizeFieldName(fieldName)} |-> ${initVal}`);
|
|
1234
|
+
}
|
|
1235
|
+
initLines.push(`"${docId}" |-> [${inner.join(", ")}]`);
|
|
1236
|
+
}
|
|
1237
|
+
initLines.forEach((line, i) => {
|
|
1238
|
+
this.line(line + (i < initLines.length - 1 ? "," : ""));
|
|
1239
|
+
});
|
|
1240
|
+
this.indent--;
|
|
1241
|
+
this.line("]");
|
|
1242
|
+
this.line("");
|
|
1243
|
+
}
|
|
1244
|
+
fieldConfigInitialValue(_path, fieldConfig, _config) {
|
|
1245
|
+
const fc = fieldConfig;
|
|
1246
|
+
if (fc["type"] === "boolean")
|
|
1247
|
+
return "FALSE";
|
|
1248
|
+
if (fc["type"] === "number") {
|
|
1249
|
+
const min = typeof fc["min"] === "number" ? fc["min"] : 0;
|
|
1250
|
+
return String(min);
|
|
1251
|
+
}
|
|
1252
|
+
if (fc["type"] === "enum" && Array.isArray(fc["values"]) && fc["values"].length > 0) {
|
|
1253
|
+
return `"${String(fc["values"][0])}"`;
|
|
1254
|
+
}
|
|
1255
|
+
if (fc["type"] === "string") {
|
|
1256
|
+
return typeof fc["initial"] === "string" ? `"${fc["initial"]}"` : '""';
|
|
1257
|
+
}
|
|
1258
|
+
if (fc["type"] === "array") {
|
|
1259
|
+
return "<<>>";
|
|
1260
|
+
}
|
|
1261
|
+
if (Array.isArray(fc.values)) {
|
|
1262
|
+
const values = fc.values;
|
|
1263
|
+
if (values.length > 0)
|
|
1264
|
+
return `"${values[0]}"`;
|
|
1265
|
+
}
|
|
1266
|
+
return '"v1"';
|
|
1184
1267
|
}
|
|
1185
1268
|
defineValueTypes() {
|
|
1186
1269
|
this.line("\\* Generic value type for sequences and maps");
|
|
@@ -1402,18 +1485,40 @@ var init_tla = __esm(() => {
|
|
|
1402
1485
|
console.log(`[INFO] [TLAGenerator] Tab symmetry enabled: ${this.tabCount} tabs as model values`);
|
|
1403
1486
|
}
|
|
1404
1487
|
}
|
|
1405
|
-
addVariables() {
|
|
1488
|
+
addVariables(config) {
|
|
1489
|
+
const hasMesh = this.hasMeshConfig(config);
|
|
1406
1490
|
this.line("\\* Application state per context");
|
|
1407
1491
|
this.line("VARIABLE contextStates");
|
|
1408
1492
|
this.line("");
|
|
1493
|
+
if (hasMesh) {
|
|
1494
|
+
this.line("\\* polly#117: per-context mesh document replicas. Each context");
|
|
1495
|
+
this.line("\\* carries an independent record per declared $meshState document.");
|
|
1496
|
+
this.line("\\* The PropagateMeshOp action diffuses values between contexts to");
|
|
1497
|
+
this.line("\\* model Automerge sync, and predicate references to mesh-tagged");
|
|
1498
|
+
this.line("\\* signals route through this variable instead of contextStates.");
|
|
1499
|
+
this.line("VARIABLE meshState");
|
|
1500
|
+
this.line("");
|
|
1501
|
+
}
|
|
1409
1502
|
this.line("\\* Message payload (abstract model - non-deterministically chosen)");
|
|
1410
1503
|
this.line("\\* In verification, we model payload fields as potentially any valid value");
|
|
1411
1504
|
this.line("VARIABLE payload");
|
|
1412
1505
|
this.line("");
|
|
1413
1506
|
this.line("\\* All variables (extending MessageRouter vars)");
|
|
1414
|
-
|
|
1507
|
+
const allVarsList = hasMesh ? "ports, messages, pendingRequests, delivered, routingDepth, time, contextStates, meshState, payload" : "ports, messages, pendingRequests, delivered, routingDepth, time, contextStates, payload";
|
|
1508
|
+
this.line(`allVars == <<${allVarsList}>>`);
|
|
1415
1509
|
this.line("");
|
|
1416
1510
|
}
|
|
1511
|
+
hasMeshConfig(config) {
|
|
1512
|
+
const c = config ?? this.currentConfig;
|
|
1513
|
+
return !!c?.mesh && Object.keys(c.mesh).length > 0;
|
|
1514
|
+
}
|
|
1515
|
+
userStateVars(config) {
|
|
1516
|
+
return this.hasMeshConfig(config) ? ["contextStates", "meshState"] : ["contextStates"];
|
|
1517
|
+
}
|
|
1518
|
+
unchangedUserStates(config, andOthers = []) {
|
|
1519
|
+
const all = [...andOthers, ...this.userStateVars(config)];
|
|
1520
|
+
return all.length === 1 ? `UNCHANGED ${all[0]}` : `UNCHANGED <<${all.join(", ")}>>`;
|
|
1521
|
+
}
|
|
1417
1522
|
addInit(config, _analysis) {
|
|
1418
1523
|
this.deriveParamDomains(config, _analysis);
|
|
1419
1524
|
this.line("\\* Initial application state");
|
|
@@ -1434,6 +1539,9 @@ var init_tla = __esm(() => {
|
|
|
1434
1539
|
this.indent++;
|
|
1435
1540
|
this.line("/\\ Init \\* MessageRouter's init");
|
|
1436
1541
|
this.line("/\\ contextStates = [c \\in Contexts |-> InitialState]");
|
|
1542
|
+
if (this.hasMeshConfig(config)) {
|
|
1543
|
+
this.line("/\\ meshState = [c \\in Contexts |-> InitialMesh]");
|
|
1544
|
+
}
|
|
1437
1545
|
this.line("/\\ payload \\in PayloadType \\* Non-deterministic initial payload");
|
|
1438
1546
|
this.indent--;
|
|
1439
1547
|
this.line("");
|
|
@@ -1629,7 +1737,7 @@ var init_tla = __esm(() => {
|
|
|
1629
1737
|
this.line("\\* State remains unchanged for all messages");
|
|
1630
1738
|
this.line("StateTransition(ctx, msgType) ==");
|
|
1631
1739
|
this.indent++;
|
|
1632
|
-
this.line(
|
|
1740
|
+
this.line(this.unchangedUserStates());
|
|
1633
1741
|
this.indent--;
|
|
1634
1742
|
this.line("");
|
|
1635
1743
|
}
|
|
@@ -1638,7 +1746,7 @@ var init_tla = __esm(() => {
|
|
|
1638
1746
|
this.line("\\* State remains unchanged for all messages");
|
|
1639
1747
|
this.line("StateTransition(ctx, msgType) ==");
|
|
1640
1748
|
this.indent++;
|
|
1641
|
-
this.line(
|
|
1749
|
+
this.line(this.unchangedUserStates());
|
|
1642
1750
|
this.indent--;
|
|
1643
1751
|
this.line("");
|
|
1644
1752
|
}
|
|
@@ -1741,7 +1849,7 @@ var init_tla = __esm(() => {
|
|
|
1741
1849
|
recordPostconditionProperty(messageType, actionName, postconditions) {
|
|
1742
1850
|
if (postconditions.length === 0)
|
|
1743
1851
|
return;
|
|
1744
|
-
const predicateClauses = postconditions.map((pc) => this.tsExpressionToTLA(pc.expression, true)).filter((p) => p && p.length > 0).map((p) => p.replace(/\[ctx\]/g, "[target]"));
|
|
1852
|
+
const predicateClauses = postconditions.map((pc) => this.tsExpressionToTLA(pc.expression, true)).filter((p) => p && p.length > 0).map((p) => p.replace(/\[ctx\]/g, "[target]").replace(/\{ctx\}/g, "{target}"));
|
|
1745
1853
|
if (predicateClauses.length === 0)
|
|
1746
1854
|
return;
|
|
1747
1855
|
const conjunction = predicateClauses.length === 1 ? predicateClauses[0] : predicateClauses.map((c) => ` /\\ ${c}`).join(`
|
|
@@ -1782,7 +1890,23 @@ var init_tla = __esm(() => {
|
|
|
1782
1890
|
onBuild: "warn",
|
|
1783
1891
|
onRelease: "warn"
|
|
1784
1892
|
};
|
|
1785
|
-
|
|
1893
|
+
if (this.flattenStateConfig(fakeConfig).has(sanitized))
|
|
1894
|
+
return true;
|
|
1895
|
+
return this.isMeshAssignmentModeled(field);
|
|
1896
|
+
}
|
|
1897
|
+
isMeshAssignmentModeled(field) {
|
|
1898
|
+
const meshHit = this.classifyAssignmentAsMesh(field);
|
|
1899
|
+
if (!meshHit)
|
|
1900
|
+
return false;
|
|
1901
|
+
const mesh = this.currentConfig?.mesh;
|
|
1902
|
+
if (!mesh)
|
|
1903
|
+
return false;
|
|
1904
|
+
const docConfig = mesh[meshHit.docId];
|
|
1905
|
+
if (!docConfig)
|
|
1906
|
+
return false;
|
|
1907
|
+
if (meshHit.innerField === "")
|
|
1908
|
+
return true;
|
|
1909
|
+
return Object.hasOwn(docConfig, meshHit.innerField);
|
|
1786
1910
|
}
|
|
1787
1911
|
shouldIncludeAssignment(assignment, state) {
|
|
1788
1912
|
if (assignment.value !== null)
|
|
@@ -1817,7 +1941,7 @@ var init_tla = __esm(() => {
|
|
|
1817
1941
|
if (preconditions.length === 0) {
|
|
1818
1942
|
this.line("\\* No state changes in handler");
|
|
1819
1943
|
}
|
|
1820
|
-
this.line(
|
|
1944
|
+
this.line(`/\\ ${this.unchangedUserStates()}`);
|
|
1821
1945
|
return true;
|
|
1822
1946
|
}
|
|
1823
1947
|
const ndetAssignments = [];
|
|
@@ -1830,15 +1954,67 @@ var init_tla = __esm(() => {
|
|
|
1830
1954
|
}
|
|
1831
1955
|
}
|
|
1832
1956
|
if (ndetAssignments.length === 0) {
|
|
1833
|
-
const
|
|
1957
|
+
const partition = this.partitionAssignments(detAssignments);
|
|
1958
|
+
this.emitDeterministicAssignmentPartitions(partition);
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
this.emitNDETStateUpdates(ndetAssignments, detAssignments);
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
partitionAssignments(assignments) {
|
|
1965
|
+
const local = [];
|
|
1966
|
+
const mesh = new Map;
|
|
1967
|
+
for (const a of assignments) {
|
|
1968
|
+
const meshHit = this.classifyAssignmentAsMesh(a.field);
|
|
1969
|
+
if (meshHit) {
|
|
1970
|
+
const list = mesh.get(meshHit.docId) ?? [];
|
|
1971
|
+
list.push({ inner: meshHit.innerField, value: a.value });
|
|
1972
|
+
mesh.set(meshHit.docId, list);
|
|
1973
|
+
} else {
|
|
1974
|
+
local.push(a);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return { local, mesh };
|
|
1978
|
+
}
|
|
1979
|
+
classifyAssignmentAsMesh(field) {
|
|
1980
|
+
for (const [signalName, docId] of this.meshSignalDocs.entries()) {
|
|
1981
|
+
if (field === signalName) {
|
|
1982
|
+
return { docId, innerField: "" };
|
|
1983
|
+
}
|
|
1984
|
+
const prefix = `${signalName}_`;
|
|
1985
|
+
if (field.startsWith(prefix)) {
|
|
1986
|
+
return { docId, innerField: field.substring(prefix.length) };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
emitDeterministicAssignmentPartitions(partition) {
|
|
1992
|
+
const { local, mesh } = partition;
|
|
1993
|
+
if (local.length > 0) {
|
|
1994
|
+
const exceptClauses = local.map((a) => {
|
|
1834
1995
|
const tlaValue = this.assignmentValueToTLA(a.value);
|
|
1835
1996
|
return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
|
|
1836
1997
|
});
|
|
1837
1998
|
this.line(`/\\ contextStates' = [contextStates EXCEPT ${exceptClauses.join(", ")}]`);
|
|
1838
|
-
|
|
1999
|
+
} else if (mesh.size > 0) {
|
|
2000
|
+
this.line("/\\ UNCHANGED contextStates");
|
|
2001
|
+
}
|
|
2002
|
+
if (mesh.size > 0) {
|
|
2003
|
+
const clauses = [];
|
|
2004
|
+
for (const [docId, writes] of mesh.entries()) {
|
|
2005
|
+
for (const w of writes) {
|
|
2006
|
+
const tlaValue = this.assignmentValueToTLA(w.value);
|
|
2007
|
+
if (w.inner === "") {
|
|
2008
|
+
clauses.push(`![ctx]["${docId}"] = ${tlaValue}`);
|
|
2009
|
+
} else {
|
|
2010
|
+
clauses.push(`![ctx]["${docId}"].${this.sanitizeFieldName(w.inner)} = ${tlaValue}`);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
this.line(`/\\ meshState' = [meshState EXCEPT ${clauses.join(", ")}]`);
|
|
2015
|
+
} else if (this.hasMeshConfig() && local.length > 0) {
|
|
2016
|
+
this.line("/\\ UNCHANGED meshState");
|
|
1839
2017
|
}
|
|
1840
|
-
this.emitNDETStateUpdates(ndetAssignments, detAssignments);
|
|
1841
|
-
return false;
|
|
1842
2018
|
}
|
|
1843
2019
|
emitNDETStateUpdates(ndetAssignments, detAssignments) {
|
|
1844
2020
|
const detExceptClauses = detAssignments.map((a) => {
|
|
@@ -1869,10 +2045,56 @@ var init_tla = __esm(() => {
|
|
|
1869
2045
|
this.indent--;
|
|
1870
2046
|
}
|
|
1871
2047
|
}
|
|
2048
|
+
tryExtractPeerScopedPredicate(expr) {
|
|
2049
|
+
if (typeof expr !== "string")
|
|
2050
|
+
return null;
|
|
2051
|
+
const trimmed = expr.trim();
|
|
2052
|
+
const match = trimmed.match(/^(forAllPeers|somePeer)\s*\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=>\s*([\s\S]+)\)\s*$/);
|
|
2053
|
+
if (!match)
|
|
2054
|
+
return null;
|
|
2055
|
+
const fn = match[1];
|
|
2056
|
+
const binder = match[2];
|
|
2057
|
+
const innerBody = match[3];
|
|
2058
|
+
if (!fn || !binder || innerBody === undefined)
|
|
2059
|
+
return null;
|
|
2060
|
+
return {
|
|
2061
|
+
binder,
|
|
2062
|
+
innerBody: innerBody.trim(),
|
|
2063
|
+
kind: fn === "forAllPeers" ? "forall" : "exists"
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
1872
2066
|
tsExpressionToTLA(expr, isPrimed = false) {
|
|
1873
2067
|
if (!expr || typeof expr !== "string") {
|
|
1874
2068
|
return expr || "";
|
|
1875
2069
|
}
|
|
2070
|
+
const scoped = this.tryExtractPeerScopedPredicate(expr);
|
|
2071
|
+
if (scoped) {
|
|
2072
|
+
const binder = scoped.binder;
|
|
2073
|
+
const peerPrefix = isPrimed ? `contextStates'[${binder}]` : `contextStates[${binder}]`;
|
|
2074
|
+
let body = scoped.innerBody;
|
|
2075
|
+
const binderRe1 = new RegExp(`\\b${binder}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\.value\\.([a-zA-Z_][a-zA-Z0-9_.]*)`, "g");
|
|
2076
|
+
body = body.replace(binderRe1, (_m, sig, path3) => {
|
|
2077
|
+
const meshDoc = this.meshSignalDocs.get(sig);
|
|
2078
|
+
if (meshDoc !== undefined) {
|
|
2079
|
+
const meshPeerPrefix = isPrimed ? `meshState'[${binder}]["${meshDoc}"]` : `meshState[${binder}]["${meshDoc}"]`;
|
|
2080
|
+
const field = this.sanitizeFieldName(String(path3).replace(/\./g, "_"));
|
|
2081
|
+
return `${meshPeerPrefix}.${field}`;
|
|
2082
|
+
}
|
|
2083
|
+
const flat = this.sanitizeFieldName(`${sig}_${String(path3).replace(/\./g, "_")}`);
|
|
2084
|
+
return `${peerPrefix}.${flat}`;
|
|
2085
|
+
});
|
|
2086
|
+
const binderRe2 = new RegExp(`\\b${binder}\\.([a-zA-Z_][a-zA-Z0-9_]*)\\.value\\b(?!\\.)`, "g");
|
|
2087
|
+
body = body.replace(binderRe2, (_m, sig) => {
|
|
2088
|
+
const meshDoc = this.meshSignalDocs.get(sig);
|
|
2089
|
+
if (meshDoc !== undefined) {
|
|
2090
|
+
return isPrimed ? `meshState'[${binder}]["${meshDoc}"]` : `meshState[${binder}]["${meshDoc}"]`;
|
|
2091
|
+
}
|
|
2092
|
+
return `${peerPrefix}.${sig}`;
|
|
2093
|
+
});
|
|
2094
|
+
const innerTLA = this.tsExpressionToTLA(body, isPrimed);
|
|
2095
|
+
const quantifier = scoped.kind === "forall" ? "\\A" : "\\E";
|
|
2096
|
+
return `${quantifier} ${binder} \\in Contexts \\ {ctx} : (${innerTLA})`;
|
|
2097
|
+
}
|
|
1876
2098
|
let tla = expr;
|
|
1877
2099
|
const statePrefix = isPrimed ? "contextStates'[ctx]" : "contextStates[ctx]";
|
|
1878
2100
|
tla = this.translateComplexExpressions(tla, statePrefix);
|
|
@@ -1889,10 +2111,20 @@ var init_tla = __esm(() => {
|
|
|
1889
2111
|
}
|
|
1890
2112
|
tla = tla.replace(/'([^']+)'/g, '"$1"');
|
|
1891
2113
|
tla = tla.replace(/([a-zA-Z_][a-zA-Z0-9_]*)\.value\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, stateName, path3) => {
|
|
2114
|
+
const meshDoc = this.meshSignalDocs.get(stateName);
|
|
2115
|
+
if (meshDoc !== undefined) {
|
|
2116
|
+
const meshPrefix = isPrimed ? `meshState'[ctx]["${meshDoc}"]` : `meshState[ctx]["${meshDoc}"]`;
|
|
2117
|
+
const field = this.sanitizeFieldName(String(path3).replace(/\./g, "_"));
|
|
2118
|
+
return `${meshPrefix}.${field}`;
|
|
2119
|
+
}
|
|
1892
2120
|
const fullPath = `${stateName}_${path3.replace(/\./g, "_")}`;
|
|
1893
2121
|
return `${statePrefix}.${this.sanitizeFieldName(fullPath)}`;
|
|
1894
2122
|
});
|
|
1895
2123
|
tla = tla.replace(/([a-zA-Z_][a-zA-Z0-9_]*)\.value\b(?!\.)/g, (_match, stateName) => {
|
|
2124
|
+
const meshDoc = this.meshSignalDocs.get(stateName);
|
|
2125
|
+
if (meshDoc !== undefined) {
|
|
2126
|
+
return isPrimed ? `meshState'[ctx]["${meshDoc}"]` : `meshState[ctx]["${meshDoc}"]`;
|
|
2127
|
+
}
|
|
1896
2128
|
return `${statePrefix}.${stateName}`;
|
|
1897
2129
|
});
|
|
1898
2130
|
tla = tla.replace(/state\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, path3) => {
|
|
@@ -2353,28 +2585,48 @@ var init_tla = __esm(() => {
|
|
|
2353
2585
|
this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
|
|
2354
2586
|
this.line(" pendingRequests[id]]");
|
|
2355
2587
|
this.line(" /\\ time' = time + 1");
|
|
2356
|
-
this.line(
|
|
2588
|
+
this.line(` /\\ ${this.unchangedUserStates(undefined, ["delivered", "payload"])}`);
|
|
2357
2589
|
this.line(" /\\ UNCHANGED ports");
|
|
2358
2590
|
this.indent--;
|
|
2359
2591
|
this.line("");
|
|
2360
2592
|
}
|
|
2593
|
+
addPropagateMeshOp() {
|
|
2594
|
+
if (!this.hasMeshConfig())
|
|
2595
|
+
return;
|
|
2596
|
+
this.line("\\* polly#117: propagate a mesh-document value from one context to another.");
|
|
2597
|
+
this.line("\\* Models Automerge sync: src and dst must currently disagree on docId, and");
|
|
2598
|
+
this.line("\\* the action copies src's value into dst's record. All other state is fixed.");
|
|
2599
|
+
this.line("PropagateMeshOp(src, dst, docId) ==");
|
|
2600
|
+
this.indent++;
|
|
2601
|
+
this.line("/\\ src # dst");
|
|
2602
|
+
this.line("/\\ meshState[src][docId] # meshState[dst][docId]");
|
|
2603
|
+
this.line("/\\ meshState' = [meshState EXCEPT ![dst][docId] = meshState[src][docId]]");
|
|
2604
|
+
this.line("/\\ UNCHANGED <<ports, messages, pendingRequests, delivered, routingDepth, time, contextStates, payload>>");
|
|
2605
|
+
this.indent--;
|
|
2606
|
+
this.line("");
|
|
2607
|
+
}
|
|
2361
2608
|
addNext(_config, analysis) {
|
|
2609
|
+
this.addPropagateMeshOp();
|
|
2362
2610
|
this.line("\\* Next state relation (extends MessageRouter)");
|
|
2363
2611
|
this.line("UserNext ==");
|
|
2364
2612
|
this.indent++;
|
|
2365
2613
|
const hasValidHandlers = analysis.handlers.some((h) => this.isValidTLAIdentifier(h.messageType));
|
|
2366
2614
|
if (hasValidHandlers) {
|
|
2367
|
-
|
|
2368
|
-
this.line(
|
|
2615
|
+
const userPayload = this.unchangedUserStates(undefined, ["payload"]);
|
|
2616
|
+
this.line(`\\/ \\E c \\in Contexts : ConnectPort(c) /\\ ${userPayload}`);
|
|
2617
|
+
this.line(`\\/ \\E c \\in Contexts : DisconnectPort(c) /\\ ${userPayload}`);
|
|
2369
2618
|
this.line("\\/ \\E src \\in Contexts : \\E targetSet \\in (SUBSET Contexts \\ {{}}) : \\E tab \\in Tabs : \\E msgType \\in UserMessageTypes :");
|
|
2370
2619
|
this.indent++;
|
|
2371
|
-
this.line(
|
|
2620
|
+
this.line(`SendMessage(src, targetSet, tab, msgType) /\\ ${userPayload}`);
|
|
2372
2621
|
this.indent--;
|
|
2373
2622
|
this.line("\\/ \\E i \\in 1..Len(messages) : UserRouteMessage(i)");
|
|
2374
|
-
this.line(
|
|
2375
|
-
this.line(
|
|
2623
|
+
this.line(`\\/ CompleteRouting /\\ ${userPayload}`);
|
|
2624
|
+
this.line(`\\/ \\E i \\in 1..Len(messages) : TimeoutMessage(i) /\\ ${userPayload}`);
|
|
2625
|
+
if (this.hasMeshConfig()) {
|
|
2626
|
+
this.line("\\/ \\E src, dst \\in Contexts : \\E docId \\in MeshDocs : PropagateMeshOp(src, dst, docId)");
|
|
2627
|
+
}
|
|
2376
2628
|
} else {
|
|
2377
|
-
this.line(
|
|
2629
|
+
this.line(`\\/ Next /\\ ${this.unchangedUserStates(undefined, ["payload"])}`);
|
|
2378
2630
|
}
|
|
2379
2631
|
this.indent--;
|
|
2380
2632
|
this.line("");
|
|
@@ -2957,7 +3209,7 @@ class DockerRunner {
|
|
|
2957
3209
|
});
|
|
2958
3210
|
}
|
|
2959
3211
|
}
|
|
2960
|
-
var __dirname = "/Users/AJT/projects/polly/tools/verify/src/runner";
|
|
3212
|
+
var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src/runner";
|
|
2961
3213
|
var init_docker = () => {};
|
|
2962
3214
|
|
|
2963
3215
|
// tools/verify/src/cli.ts
|
|
@@ -4629,6 +4881,7 @@ class HandlerExtractor {
|
|
|
4629
4881
|
const stateConstraints = [];
|
|
4630
4882
|
const globalStateConstraints = [];
|
|
4631
4883
|
const verifiedStates = [];
|
|
4884
|
+
const meshOrPeerSignals = [];
|
|
4632
4885
|
const resources = [];
|
|
4633
4886
|
this.warnings = [];
|
|
4634
4887
|
const allSourceFiles = this.project.getSourceFiles();
|
|
@@ -4659,6 +4912,12 @@ class HandlerExtractor {
|
|
|
4659
4912
|
}
|
|
4660
4913
|
}
|
|
4661
4914
|
}
|
|
4915
|
+
for (const filePath of this.analyzedFiles) {
|
|
4916
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
4917
|
+
if (!sourceFile)
|
|
4918
|
+
continue;
|
|
4919
|
+
meshOrPeerSignals.push(...this.extractMeshOrPeerSignalsFromFile(sourceFile));
|
|
4920
|
+
}
|
|
4662
4921
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
4663
4922
|
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
4664
4923
|
return {
|
|
@@ -4667,10 +4926,45 @@ class HandlerExtractor {
|
|
|
4667
4926
|
stateConstraints,
|
|
4668
4927
|
globalStateConstraints,
|
|
4669
4928
|
verifiedStates,
|
|
4929
|
+
meshOrPeerSignals,
|
|
4670
4930
|
resources,
|
|
4671
4931
|
warnings: this.warnings
|
|
4672
4932
|
};
|
|
4673
4933
|
}
|
|
4934
|
+
extractMeshOrPeerSignalsFromFile(sourceFile) {
|
|
4935
|
+
const out = [];
|
|
4936
|
+
const filePath = sourceFile.getFilePath();
|
|
4937
|
+
sourceFile.forEachDescendant((node) => {
|
|
4938
|
+
const info = this.recognizeMeshOrPeerStateCall(node, filePath);
|
|
4939
|
+
if (info)
|
|
4940
|
+
out.push(info);
|
|
4941
|
+
});
|
|
4942
|
+
return out;
|
|
4943
|
+
}
|
|
4944
|
+
recognizeMeshOrPeerStateCall(node, filePath) {
|
|
4945
|
+
if (!Node2.isCallExpression(node))
|
|
4946
|
+
return null;
|
|
4947
|
+
const expression = node.getExpression();
|
|
4948
|
+
if (!Node2.isIdentifier(expression))
|
|
4949
|
+
return null;
|
|
4950
|
+
const funcName = expression.getText();
|
|
4951
|
+
const kind = funcName === "$meshState" ? "mesh" : funcName === "$peerState" ? "peer" : null;
|
|
4952
|
+
if (!kind)
|
|
4953
|
+
return null;
|
|
4954
|
+
const args = node.getArguments();
|
|
4955
|
+
const keyArg = args[0];
|
|
4956
|
+
if (!keyArg || !Node2.isStringLiteral(keyArg))
|
|
4957
|
+
return null;
|
|
4958
|
+
const key = keyArg.getLiteralValue();
|
|
4959
|
+
const variableName = this.getVariableNameFromParent(node) || key;
|
|
4960
|
+
return {
|
|
4961
|
+
kind,
|
|
4962
|
+
key,
|
|
4963
|
+
variableName,
|
|
4964
|
+
filePath,
|
|
4965
|
+
line: node.getStartLineNumber()
|
|
4966
|
+
};
|
|
4967
|
+
}
|
|
4674
4968
|
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates, resources) {
|
|
4675
4969
|
const filePath = sourceFile.getFilePath();
|
|
4676
4970
|
if (this.analyzedFiles.has(filePath)) {
|
|
@@ -6790,6 +7084,7 @@ class TypeExtractor {
|
|
|
6790
7084
|
stateConstraints: handlerAnalysis.stateConstraints,
|
|
6791
7085
|
globalStateConstraints: handlerAnalysis.globalStateConstraints,
|
|
6792
7086
|
verifiedStates: handlerAnalysis.verifiedStates,
|
|
7087
|
+
meshOrPeerSignals: handlerAnalysis.meshOrPeerSignals,
|
|
6793
7088
|
resources: handlerAnalysis.resources
|
|
6794
7089
|
};
|
|
6795
7090
|
}
|
|
@@ -7197,7 +7492,7 @@ async function analyzeCodebase(options) {
|
|
|
7197
7492
|
return extractor.analyzeCodebase(options.stateFilePath);
|
|
7198
7493
|
}
|
|
7199
7494
|
// tools/verify/src/cli.ts
|
|
7200
|
-
var __dirname = "/Users/AJT/projects/polly/tools/verify/src";
|
|
7495
|
+
var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src";
|
|
7201
7496
|
var COLORS = {
|
|
7202
7497
|
reset: "\x1B[0m",
|
|
7203
7498
|
red: "\x1B[31m",
|
|
@@ -7449,6 +7744,49 @@ function displayExpressionWarnings(result) {
|
|
|
7449
7744
|
console.log();
|
|
7450
7745
|
}
|
|
7451
7746
|
}
|
|
7747
|
+
function displayMeshOrPeerSignalWarnings(analysis) {
|
|
7748
|
+
const signals = analysis.meshOrPeerSignals ?? [];
|
|
7749
|
+
if (signals.length === 0)
|
|
7750
|
+
return;
|
|
7751
|
+
const findings = [];
|
|
7752
|
+
for (const handler of analysis.handlers) {
|
|
7753
|
+
const scanConditions = (conditions, kind) => {
|
|
7754
|
+
for (const cond of conditions) {
|
|
7755
|
+
for (const sig of signals) {
|
|
7756
|
+
const pattern = new RegExp(`\\b${sig.variableName}\\.value\\b`);
|
|
7757
|
+
if (pattern.test(cond.expression)) {
|
|
7758
|
+
findings.push({
|
|
7759
|
+
handler: handler.messageType,
|
|
7760
|
+
kind,
|
|
7761
|
+
expression: cond.expression,
|
|
7762
|
+
signalName: sig.variableName,
|
|
7763
|
+
signalKind: sig.kind,
|
|
7764
|
+
location: {
|
|
7765
|
+
file: handler.location.file,
|
|
7766
|
+
line: cond.location?.line ?? handler.location.line
|
|
7767
|
+
}
|
|
7768
|
+
});
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
7772
|
+
};
|
|
7773
|
+
scanConditions(handler.preconditions, "precondition");
|
|
7774
|
+
scanConditions(handler.postconditions, "postcondition");
|
|
7775
|
+
}
|
|
7776
|
+
if (findings.length === 0)
|
|
7777
|
+
return;
|
|
7778
|
+
console.log(color(`
|
|
7779
|
+
⚠️ ${findings.length} predicate(s) reference a $meshState/$peerState signal (polly#117):`, COLORS.yellow));
|
|
7780
|
+
console.log(color(" The verifier currently treats these the same as $sharedState — single-context local state.", COLORS.gray));
|
|
7781
|
+
console.log(color(` Cross-peer semantics are NOT verified. A green run does not prove the claim across peers.
|
|
7782
|
+
`, COLORS.gray));
|
|
7783
|
+
for (const f of findings) {
|
|
7784
|
+
console.log(color(` ⚠ ${f.handler} ${f.kind}: ${f.signalName} is $${f.signalKind}State, not single-context`, COLORS.yellow));
|
|
7785
|
+
console.log(color(` ${f.expression}`, COLORS.gray));
|
|
7786
|
+
console.log(color(` at ${f.location.file}:${f.location.line}`, COLORS.gray));
|
|
7787
|
+
console.log();
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7452
7790
|
async function verifyCommand() {
|
|
7453
7791
|
const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
|
|
7454
7792
|
console.log(color(`
|
|
@@ -7517,6 +7855,7 @@ async function runFullVerification(configPath) {
|
|
|
7517
7855
|
if (exprValidation.warnings.length > 0) {
|
|
7518
7856
|
displayExpressionWarnings(exprValidation);
|
|
7519
7857
|
}
|
|
7858
|
+
displayMeshOrPeerSignalWarnings(typedAnalysis);
|
|
7520
7859
|
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
7521
7860
|
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
7522
7861
|
return;
|
|
@@ -7917,4 +8256,4 @@ main().catch((error) => {
|
|
|
7917
8256
|
process.exit(1);
|
|
7918
8257
|
});
|
|
7919
8258
|
|
|
7920
|
-
//# debugId=
|
|
8259
|
+
//# debugId=E07763C4D9F04C6264756E2164756E21
|