@fairfox/polly 0.15.0 → 0.15.4

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.
@@ -569,6 +569,7 @@ class TLAGenerator {
569
569
  addBasicConstants(lines, config) {
570
570
  lines.push(" Contexts = {background, content, popup}");
571
571
  lines.push(` MaxMessages = ${config.messages.maxInFlight || 3}`);
572
+ lines.push(" NULL = NULL");
572
573
  if (config.messages.perMessageBounds) {
573
574
  for (const [msgType, bound] of Object.entries(config.messages.perMessageBounds)) {
574
575
  const constName = `MaxMessages_${msgType}`;
@@ -682,21 +683,17 @@ class TLAGenerator {
682
683
  addConstants(config) {
683
684
  const hasStateConstants = this.hasCustomConstants(config.state);
684
685
  const hasPerMessageBounds = config.messages.perMessageBounds && Object.keys(config.messages.perMessageBounds).length > 0;
685
- if (!hasStateConstants && !hasPerMessageBounds) {
686
- return;
687
- }
688
686
  this.line("\\* Application-specific constants");
689
687
  this.line("CONSTANTS");
690
688
  this.indent++;
689
+ this.line("NULL");
691
690
  if (hasStateConstants) {
692
- this.generateConstantDeclarations(config.state);
691
+ this.generateConstantDeclarations(config.state, false);
693
692
  }
694
693
  if (hasPerMessageBounds) {
695
- let first = !hasStateConstants;
696
694
  for (const [msgType, _bound] of Object.entries(config.messages.perMessageBounds)) {
697
695
  const constName = `MaxMessages_${msgType}`;
698
- this.line(`${first ? "" : ","}${constName}`);
699
- first = false;
696
+ this.line(`,${constName}`);
700
697
  }
701
698
  }
702
699
  this.indent--;
@@ -709,8 +706,8 @@ class TLAGenerator {
709
706
  return "maxLength" in fieldConfig && fieldConfig.maxLength !== null || "max" in fieldConfig && fieldConfig.max !== null || "maxSize" in fieldConfig && fieldConfig.maxSize !== null;
710
707
  });
711
708
  }
712
- generateConstantDeclarations(state) {
713
- let first = true;
709
+ generateConstantDeclarations(state, firstConstant = true) {
710
+ let first = firstConstant;
714
711
  for (const [field, fieldConfig] of Object.entries(state)) {
715
712
  if (typeof fieldConfig !== "object" || fieldConfig === null)
716
713
  continue;
@@ -747,12 +744,12 @@ class TLAGenerator {
747
744
  }
748
745
  defineValueTypes() {
749
746
  this.line("\\* Generic value type for sequences and maps");
750
- this.line("\\* Bounded to allow model checking");
751
- this.line('Value == {"v1", "v2", "v3"}');
747
+ this.line("\\* Bounded to 2 values to reduce state space (2^n vs 3^n)");
748
+ this.line('Value == {"v1", "v2"}');
752
749
  this.line("");
753
750
  this.line("\\* Generic key type for maps");
754
751
  this.line("\\* Bounded to allow model checking");
755
- this.line('Keys == {"k1", "k2", "k3"}');
752
+ this.line('Keys == {"k1", "k2"}');
756
753
  this.line("");
757
754
  }
758
755
  collectStateFields(config, _analysis) {
@@ -791,7 +788,7 @@ class TLAGenerator {
791
788
  }
792
789
  }
793
790
  hasTypeIndicators(fieldConfig) {
794
- return "type" in fieldConfig || "values" in fieldConfig || "maxLength" in fieldConfig || "min" in fieldConfig || "max" in fieldConfig || "maxSize" in fieldConfig;
791
+ return "type" in fieldConfig || "values" in fieldConfig || "maxLength" in fieldConfig || "min" in fieldConfig || "max" in fieldConfig || "maxSize" in fieldConfig || "abstract" in fieldConfig;
795
792
  }
796
793
  writeStateFields(fields) {
797
794
  if (fields.length === 0) {
@@ -936,8 +933,12 @@ class TLAGenerator {
936
933
  this.line("\\* Application state per context");
937
934
  this.line("VARIABLE contextStates");
938
935
  this.line("");
936
+ this.line("\\* Message payload (abstract model - non-deterministically chosen)");
937
+ this.line("\\* In verification, we model payload fields as potentially any valid value");
938
+ this.line("VARIABLE payload");
939
+ this.line("");
939
940
  this.line("\\* All variables (extending MessageRouter vars)");
940
- this.line("allVars == <<ports, messages, pendingRequests, delivered, routingDepth, time, contextStates>>");
941
+ this.line("allVars == <<ports, messages, pendingRequests, delivered, routingDepth, time, contextStates, payload>>");
941
942
  this.line("");
942
943
  }
943
944
  addInit(config, _analysis) {
@@ -949,11 +950,16 @@ class TLAGenerator {
949
950
  this.indent--;
950
951
  this.line("]");
951
952
  this.line("");
953
+ this.line("\\* Payload modeled with essential fields only (id, text, userId)");
954
+ this.line("\\* Other fields (name, role, filter) use same Value type at runtime");
955
+ this.line("PayloadType == [id: Value, text: Value, userId: Value]");
956
+ this.line("");
952
957
  this.line("\\* Initial state (extends MessageRouter)");
953
958
  this.line("UserInit ==");
954
959
  this.indent++;
955
960
  this.line("/\\ Init \\* MessageRouter's init");
956
961
  this.line("/\\ contextStates = [c \\in Contexts |-> InitialState]");
962
+ this.line("/\\ payload \\in PayloadType \\* Non-deterministic initial payload");
957
963
  this.indent--;
958
964
  this.line("");
959
965
  }
@@ -1131,8 +1137,10 @@ class TLAGenerator {
1131
1137
  }
1132
1138
  this.emitPreconditions(allPreconditions);
1133
1139
  const validAssignments = this.processAssignments(allAssignments, config.state);
1134
- this.emitStateUpdates(validAssignments, allPreconditions);
1135
- this.emitPostconditions(allPostconditions);
1140
+ const usedUnchanged = this.emitStateUpdates(validAssignments, allPreconditions);
1141
+ if (!usedUnchanged) {
1142
+ this.emitPostconditions(allPostconditions);
1143
+ }
1136
1144
  this.indent--;
1137
1145
  this.line("");
1138
1146
  }
@@ -1170,19 +1178,19 @@ class TLAGenerator {
1170
1178
  return assignment;
1171
1179
  }
1172
1180
  emitStateUpdates(validAssignments, preconditions) {
1173
- if (validAssignments.length === 0) {
1174
- if (preconditions.length === 0) {
1175
- this.line("\\* No state changes in handler");
1176
- }
1177
- this.line("/\\ UNCHANGED contextStates");
1178
- return;
1181
+ if (validAssignments.length > 0) {
1182
+ const exceptClauses = validAssignments.map((a) => {
1183
+ const tlaValue = this.assignmentValueToTLA(a.value);
1184
+ return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
1185
+ });
1186
+ this.line(`/\\ contextStates' = [contextStates EXCEPT ${exceptClauses.join(", ")}]`);
1187
+ return false;
1188
+ }
1189
+ if (preconditions.length === 0) {
1190
+ this.line("\\* No state changes in handler");
1179
1191
  }
1180
- const assignments = validAssignments.filter((a) => a && a.value !== undefined).map((assignment) => {
1181
- const fieldName = this.sanitizeFieldName(assignment.field);
1182
- const value = this.assignmentValueToTLA(assignment.value);
1183
- return `![ctx].${fieldName} = ${value}`;
1184
- }).join(", ");
1185
- this.line(`/\\ contextStates' = [contextStates EXCEPT ${assignments}]`);
1192
+ this.line("/\\ UNCHANGED contextStates");
1193
+ return true;
1186
1194
  }
1187
1195
  tsExpressionToTLA(expr, isPrimed = false) {
1188
1196
  if (!expr || typeof expr !== "string") {
@@ -1202,6 +1210,14 @@ class TLAGenerator {
1202
1210
  if (!tla || typeof tla !== "string") {
1203
1211
  return expr;
1204
1212
  }
1213
+ tla = tla.replace(/'([^']+)'/g, '"$1"');
1214
+ tla = tla.replace(/([a-zA-Z_][a-zA-Z0-9_]*)\.value\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, stateName, path3) => {
1215
+ const fullPath = `${stateName}_${path3.replace(/\./g, "_")}`;
1216
+ return `${statePrefix}.${this.sanitizeFieldName(fullPath)}`;
1217
+ });
1218
+ tla = tla.replace(/([a-zA-Z_][a-zA-Z0-9_]*)\.value\b(?!\.)/g, (_match, stateName) => {
1219
+ return `${statePrefix}.${stateName}`;
1220
+ });
1205
1221
  tla = tla.replace(/state\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, path3) => {
1206
1222
  return `${statePrefix}.${this.sanitizeFieldName(path3)}`;
1207
1223
  });
@@ -1223,13 +1239,99 @@ class TLAGenerator {
1223
1239
  tla = tla.replace(/>/g, ">");
1224
1240
  tla = tla.replace(/<=/g, "<=");
1225
1241
  tla = tla.replace(/>=/g, ">=");
1242
+ tla = this.convertFunctionParamsToPayload(tla);
1226
1243
  return tla;
1227
1244
  }
1245
+ convertFunctionParamsToPayload(tla) {
1246
+ const tlaKeywords = new Set([
1247
+ "TRUE",
1248
+ "FALSE",
1249
+ "NULL",
1250
+ "IF",
1251
+ "THEN",
1252
+ "ELSE",
1253
+ "LET",
1254
+ "IN",
1255
+ "CASE",
1256
+ "OTHER",
1257
+ "CHOOSE",
1258
+ "EXCEPT",
1259
+ "DOMAIN",
1260
+ "SUBSET",
1261
+ "UNION",
1262
+ "UNCHANGED",
1263
+ "Len",
1264
+ "Cardinality",
1265
+ "SubSeq",
1266
+ "Append",
1267
+ "Head",
1268
+ "Tail",
1269
+ "Seq",
1270
+ "ctx",
1271
+ "payload",
1272
+ "msg",
1273
+ "state",
1274
+ "contextStates"
1275
+ ]);
1276
+ const quantifiedVars = new Set;
1277
+ const quantifierPattern = /\\[EA]\s+(\w+)\s+\\in|CHOOSE\s+(\w+)\s+\\in/g;
1278
+ for (const qMatch of tla.matchAll(quantifierPattern)) {
1279
+ if (qMatch[1])
1280
+ quantifiedVars.add(qMatch[1]);
1281
+ if (qMatch[2])
1282
+ quantifiedVars.add(qMatch[2]);
1283
+ }
1284
+ const stringRanges = [];
1285
+ const stringPattern = /"[^"]*"/g;
1286
+ for (const sMatch of tla.matchAll(stringPattern)) {
1287
+ stringRanges.push({ start: sMatch.index, end: sMatch.index + sMatch[0].length });
1288
+ }
1289
+ const isInsideString = (offset) => {
1290
+ return stringRanges.some((range) => offset >= range.start && offset < range.end);
1291
+ };
1292
+ const result = tla.replace(/([=#<>]\s*)([a-z][a-zA-Z0-9_]*)(\s*[/#\\)<>,]|\s*$)/g, (match, prefix, ident, suffix, offset) => {
1293
+ if (isInsideString(offset + prefix.length))
1294
+ return match;
1295
+ if (tlaKeywords.has(ident))
1296
+ return match;
1297
+ if (quantifiedVars.has(ident))
1298
+ return match;
1299
+ if (ident === ident.toUpperCase() && ident.length > 1)
1300
+ return match;
1301
+ if (["in", "of", "or", "and", "not"].includes(ident.toLowerCase()))
1302
+ return match;
1303
+ return `${prefix}payload.${ident}${suffix}`;
1304
+ });
1305
+ return result;
1306
+ }
1228
1307
  translateArrayOperations(expr, _statePrefix) {
1229
1308
  if (!expr || typeof expr !== "string") {
1230
1309
  return expr || "";
1231
1310
  }
1232
1311
  let result = expr;
1312
+ result = result.replace(/hasLength\(([^,]+),\s*\{\s*(?:min:\s*(\d+))?\s*,?\s*(?:max:\s*(\d+))?\s*\}\)/g, (_match, arrayRef, minVal, maxVal) => {
1313
+ const constraints = [];
1314
+ const arr = arrayRef.trim();
1315
+ if (minVal !== undefined) {
1316
+ constraints.push(`Len(${arr}) >= ${minVal}`);
1317
+ }
1318
+ if (maxVal !== undefined) {
1319
+ constraints.push(`Len(${arr}) <= ${maxVal}`);
1320
+ }
1321
+ if (constraints.length === 0) {
1322
+ return "TRUE";
1323
+ }
1324
+ return constraints.join(" /\\ ");
1325
+ });
1326
+ result = result.replace(/inRange\(([^,]+),\s*(\d+),\s*(\d+)\)/g, (_match, valueRef, minVal, maxVal) => {
1327
+ const val = valueRef.trim();
1328
+ return `${val} >= ${minVal} /\\ ${val} <= ${maxVal}`;
1329
+ });
1330
+ result = result.replace(/oneOf\(([^,]+),\s*\[([^\]]+)\]\)/g, (_match, valueRef, valuesStr) => {
1331
+ const val = valueRef.trim();
1332
+ const values = valuesStr.split(",").map((v) => v.trim());
1333
+ return `${val} \\in {${values.join(", ")}}`;
1334
+ });
1233
1335
  result = result.replace(/(\w+(?:\.\w+)*)\.length\b/g, (_match, arrayRef) => {
1234
1336
  if (arrayRef.startsWith("state.")) {
1235
1337
  return `Len(${arrayRef})`;
@@ -1519,13 +1621,14 @@ class TLAGenerator {
1519
1621
  this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
1520
1622
  this.line(" pendingRequests[id]]");
1521
1623
  this.line(" /\\ time' = time + 1");
1624
+ this.line(" /\\ \\E p \\in PayloadType : payload' = p \\* Non-deterministic payload");
1522
1625
  this.line(" /\\ StateTransition(target, msg.msgType)");
1523
1626
  this.line(" ELSE \\* Port not connected - message fails");
1524
1627
  this.line(` /\\ messages' = [messages EXCEPT ![msgIndex].status = "failed"]`);
1525
1628
  this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
1526
1629
  this.line(" pendingRequests[id]]");
1527
1630
  this.line(" /\\ time' = time + 1");
1528
- this.line(" /\\ UNCHANGED <<delivered, contextStates>>");
1631
+ this.line(" /\\ UNCHANGED <<delivered, contextStates, payload>>");
1529
1632
  this.line(" /\\ UNCHANGED ports");
1530
1633
  this.indent--;
1531
1634
  this.line("");
@@ -1536,17 +1639,17 @@ class TLAGenerator {
1536
1639
  this.indent++;
1537
1640
  const hasValidHandlers = analysis.handlers.some((h) => this.isValidTLAIdentifier(h.messageType));
1538
1641
  if (hasValidHandlers) {
1539
- this.line("\\/ \\E c \\in Contexts : ConnectPort(c) /\\ UNCHANGED contextStates");
1540
- this.line("\\/ \\E c \\in Contexts : DisconnectPort(c) /\\ UNCHANGED contextStates");
1642
+ this.line("\\/ \\E c \\in Contexts : ConnectPort(c) /\\ UNCHANGED <<contextStates, payload>>");
1643
+ this.line("\\/ \\E c \\in Contexts : DisconnectPort(c) /\\ UNCHANGED <<contextStates, payload>>");
1541
1644
  this.line("\\/ \\E src \\in Contexts : \\E targetSet \\in (SUBSET Contexts \\ {{}}) : \\E tab \\in 0..MaxTabId : \\E msgType \\in UserMessageTypes :");
1542
1645
  this.indent++;
1543
- this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED contextStates");
1646
+ this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED <<contextStates, payload>>");
1544
1647
  this.indent--;
1545
1648
  this.line("\\/ \\E i \\in 1..Len(messages) : UserRouteMessage(i)");
1546
- this.line("\\/ CompleteRouting /\\ UNCHANGED contextStates");
1547
- this.line("\\/ \\E i \\in 1..Len(messages) : TimeoutMessage(i) /\\ UNCHANGED contextStates");
1649
+ this.line("\\/ CompleteRouting /\\ UNCHANGED <<contextStates, payload>>");
1650
+ this.line("\\/ \\E i \\in 1..Len(messages) : TimeoutMessage(i) /\\ UNCHANGED <<contextStates, payload>>");
1548
1651
  } else {
1549
- this.line("\\/ Next /\\ UNCHANGED contextStates");
1652
+ this.line("\\/ Next /\\ UNCHANGED <<contextStates, payload>>");
1550
1653
  }
1551
1654
  this.indent--;
1552
1655
  this.line("");
@@ -1663,9 +1766,15 @@ class TLAGenerator {
1663
1766
  this.line("=============================================================================");
1664
1767
  }
1665
1768
  fieldConfigToTLAType(_fieldPath, fieldConfig, _config) {
1666
- const typeResult = this.tryBooleanType(fieldConfig) || this.tryEnumType(fieldConfig) || this.tryArrayType(fieldConfig) || this.tryNumberType(fieldConfig) || this.tryStringType(fieldConfig) || this.tryMapType(fieldConfig);
1769
+ const typeResult = this.tryAbstractType(fieldConfig) || this.tryBooleanType(fieldConfig) || this.tryEnumType(fieldConfig) || this.tryArrayType(fieldConfig) || this.tryNumberType(fieldConfig) || this.tryStringType(fieldConfig) || this.tryMapType(fieldConfig);
1667
1770
  return typeResult || "Value";
1668
1771
  }
1772
+ tryAbstractType(fieldConfig) {
1773
+ if ("abstract" in fieldConfig && fieldConfig.abstract === true) {
1774
+ return "Value";
1775
+ }
1776
+ return null;
1777
+ }
1669
1778
  tryBooleanType(fieldConfig) {
1670
1779
  if ("type" in fieldConfig && fieldConfig.type === "boolean") {
1671
1780
  return "BOOLEAN";
@@ -1820,6 +1929,9 @@ class TLAGenerator {
1820
1929
  }
1821
1930
  }
1822
1931
  getInitialValue(fieldConfig) {
1932
+ if ("abstract" in fieldConfig && fieldConfig.abstract === true) {
1933
+ return '"v1"';
1934
+ }
1823
1935
  if (Array.isArray(fieldConfig)) {
1824
1936
  if (fieldConfig.length > 0 && typeof fieldConfig[0] === "boolean") {
1825
1937
  return fieldConfig[0] ? "TRUE" : "FALSE";
@@ -3388,6 +3500,7 @@ class HandlerExtractor {
3388
3500
  relationshipExtractor;
3389
3501
  analyzedFiles;
3390
3502
  packageRoot;
3503
+ warnings;
3391
3504
  constructor(tsConfigPath) {
3392
3505
  this.project = new Project({
3393
3506
  tsConfigFilePath: tsConfigPath
@@ -3395,8 +3508,27 @@ class HandlerExtractor {
3395
3508
  this.typeGuardCache = new WeakMap;
3396
3509
  this.relationshipExtractor = new RelationshipExtractor;
3397
3510
  this.analyzedFiles = new Set;
3511
+ this.warnings = [];
3398
3512
  this.packageRoot = this.findPackageRoot(tsConfigPath);
3399
3513
  }
3514
+ warnUnsupportedPattern(pattern, location, suggestion) {
3515
+ const exists = this.warnings.some((w) => w.pattern === pattern && w.location === location);
3516
+ if (!exists) {
3517
+ this.warnings.push({
3518
+ type: "unsupported_pattern",
3519
+ pattern,
3520
+ location,
3521
+ suggestion
3522
+ });
3523
+ }
3524
+ }
3525
+ getNodeLocation(node) {
3526
+ const sourceFile = node.getSourceFile();
3527
+ const lineAndCol = sourceFile.getLineAndColumnAtPos(node.getStart());
3528
+ const filePath = sourceFile.getFilePath();
3529
+ const relativePath = filePath.startsWith(this.packageRoot) ? filePath.substring(this.packageRoot.length + 1) : filePath;
3530
+ return `${relativePath}:${lineAndCol.line}`;
3531
+ }
3400
3532
  findPackageRoot(tsConfigPath) {
3401
3533
  let dir = tsConfigPath.substring(0, tsConfigPath.lastIndexOf("/"));
3402
3534
  while (dir.length > 1) {
@@ -3420,6 +3552,7 @@ class HandlerExtractor {
3420
3552
  const invalidMessageTypes = new Set;
3421
3553
  const stateConstraints = [];
3422
3554
  const verifiedStates = [];
3555
+ this.warnings = [];
3423
3556
  const allSourceFiles = this.project.getSourceFiles();
3424
3557
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
3425
3558
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
@@ -3454,7 +3587,8 @@ class HandlerExtractor {
3454
3587
  handlers,
3455
3588
  messageTypes,
3456
3589
  stateConstraints,
3457
- verifiedStates
3590
+ verifiedStates,
3591
+ warnings: this.warnings
3458
3592
  };
3459
3593
  }
3460
3594
  analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
@@ -3684,46 +3818,364 @@ class HandlerExtractor {
3684
3818
  if (!Node2.isPropertyAccessExpression(left))
3685
3819
  return;
3686
3820
  const fieldPath = this.getPropertyPath(left);
3687
- if (fieldPath.startsWith("state.")) {
3688
- const field = fieldPath.substring(6);
3689
- const value = this.extractValue(right);
3690
- if (value !== undefined) {
3691
- assignments.push({ field, value });
3692
- }
3821
+ if (this.tryExtractStateFieldPattern(fieldPath, right, assignments))
3693
3822
  return;
3694
- }
3695
- const valueMatch = fieldPath.match(/\.value\.(.+)$/);
3696
- if (valueMatch?.[1]) {
3697
- const field = valueMatch[1];
3698
- const value = this.extractValue(right);
3699
- if (value !== undefined) {
3700
- assignments.push({ field, value });
3701
- }
3823
+ if (this.tryExtractSignalNestedFieldPattern(fieldPath, right, assignments))
3824
+ return;
3825
+ if (this.tryExtractSignalObjectPattern(fieldPath, right, assignments))
3826
+ return;
3827
+ if (this.tryExtractSignalArrayPattern(fieldPath, right, assignments))
3702
3828
  return;
3829
+ if (this.tryExtractSignalMethodPattern(fieldPath, right, assignments))
3830
+ return;
3831
+ if (this.tryExtractSetConstructorPattern(fieldPath, right, assignments))
3832
+ return;
3833
+ this.tryExtractMapConstructorPattern(fieldPath, right, assignments);
3834
+ }
3835
+ tryExtractStateFieldPattern(fieldPath, right, assignments) {
3836
+ if (!fieldPath.startsWith("state."))
3837
+ return false;
3838
+ const field = fieldPath.substring(6);
3839
+ const value = this.extractValue(right);
3840
+ if (value !== undefined) {
3841
+ assignments.push({ field, value });
3703
3842
  }
3704
- if (fieldPath.endsWith(".value") && Node2.isObjectLiteralExpression(right)) {
3705
- this.extractObjectLiteralAssignments(right, assignments);
3843
+ return true;
3844
+ }
3845
+ tryExtractSignalNestedFieldPattern(fieldPath, right, assignments) {
3846
+ const valueFieldMatch = fieldPath.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\.value\.(.+)$/);
3847
+ if (!valueFieldMatch?.[1] || !valueFieldMatch?.[2])
3848
+ return false;
3849
+ const signalName = valueFieldMatch[1];
3850
+ const fieldName = valueFieldMatch[2];
3851
+ const value = this.extractValue(right);
3852
+ if (value !== undefined) {
3853
+ assignments.push({ field: `${signalName}_${fieldName}`, value });
3706
3854
  }
3855
+ return true;
3707
3856
  }
3708
- extractObjectLiteralAssignments(objectLiteral, assignments) {
3709
- if (!Node2.isObjectLiteralExpression(objectLiteral))
3857
+ tryExtractSignalObjectPattern(fieldPath, right, assignments) {
3858
+ if (!fieldPath.endsWith(".value") || !Node2.isObjectLiteralExpression(right))
3859
+ return false;
3860
+ const signalName = fieldPath.slice(0, -6);
3861
+ if (this.isSpreadUpdatePattern(right, fieldPath)) {
3862
+ this.extractSpreadUpdateAssignments(right, assignments, signalName);
3863
+ } else {
3864
+ this.extractObjectLiteralAssignments(right, assignments, signalName);
3865
+ }
3866
+ return true;
3867
+ }
3868
+ isSpreadUpdatePattern(objectLiteral, fieldPath) {
3869
+ const properties = objectLiteral.getProperties();
3870
+ if (properties.length === 0)
3871
+ return false;
3872
+ const firstProp = properties[0];
3873
+ if (!firstProp || !Node2.isSpreadAssignment(firstProp))
3874
+ return false;
3875
+ const spreadExpr = firstProp.getExpression();
3876
+ if (!spreadExpr)
3877
+ return false;
3878
+ return this.getPropertyPath(spreadExpr) === fieldPath;
3879
+ }
3880
+ tryExtractSignalArrayPattern(fieldPath, right, assignments) {
3881
+ if (!fieldPath.endsWith(".value") || !Node2.isArrayLiteralExpression(right))
3882
+ return false;
3883
+ const signalName = fieldPath.slice(0, -6);
3884
+ const arrayAssignment = this.extractArraySpreadOperation(right, fieldPath, signalName);
3885
+ if (arrayAssignment) {
3886
+ assignments.push(arrayAssignment);
3887
+ }
3888
+ return true;
3889
+ }
3890
+ tryExtractSignalMethodPattern(fieldPath, right, assignments) {
3891
+ if (!fieldPath.endsWith(".value") || !Node2.isCallExpression(right))
3892
+ return false;
3893
+ const signalName = fieldPath.slice(0, -6);
3894
+ this.checkForMutatingCollectionMethods(right);
3895
+ const methodAssignment = this.extractArrayMethodOperation(right, fieldPath, signalName);
3896
+ if (methodAssignment) {
3897
+ assignments.push(methodAssignment);
3898
+ }
3899
+ return true;
3900
+ }
3901
+ checkForMutatingCollectionMethods(callExpr) {
3902
+ const expression = callExpr.getExpression();
3903
+ if (!Node2.isPropertyAccessExpression(expression))
3710
3904
  return;
3711
- for (const prop of objectLiteral.getProperties()) {
3712
- if (Node2.isPropertyAssignment(prop)) {
3713
- const name = prop.getName();
3714
- const initializer = prop.getInitializer();
3715
- if (!name || !initializer)
3716
- continue;
3717
- const value = this.extractValue(initializer);
3718
- if (value !== undefined) {
3719
- assignments.push({ field: name, value });
3905
+ const methodName = expression.getName();
3906
+ const sourceExpr = expression.getExpression();
3907
+ const setMethods = ["add", "delete", "clear"];
3908
+ if (setMethods.includes(methodName)) {
3909
+ const sourceText = sourceExpr.getText();
3910
+ if (sourceText.includes("Set") || this.looksLikeSetOrMap(sourceExpr, "Set")) {
3911
+ this.warnUnsupportedPattern(`set.${methodName}()`, this.getNodeLocation(callExpr), `Set.${methodName}() mutates in place. Use: new Set([...set, item]) for add, new Set([...set].filter(...)) for delete.`);
3912
+ }
3913
+ }
3914
+ const mapMethods = ["set", "delete", "clear"];
3915
+ if (mapMethods.includes(methodName)) {
3916
+ const sourceText = sourceExpr.getText();
3917
+ if (sourceText.includes("Map") || this.looksLikeSetOrMap(sourceExpr, "Map")) {
3918
+ this.warnUnsupportedPattern(`map.${methodName}()`, this.getNodeLocation(callExpr), `Map.${methodName}() mutates in place. Use: new Map([...map, [key, value]]) for set, new Map([...map].filter(...)) for delete.`);
3919
+ }
3920
+ }
3921
+ }
3922
+ looksLikeSetOrMap(expr, collectionType) {
3923
+ const text = expr.getText().toLowerCase();
3924
+ return text.includes(collectionType.toLowerCase());
3925
+ }
3926
+ tryExtractSetConstructorPattern(fieldPath, right, assignments) {
3927
+ if (!fieldPath.endsWith(".value") || !Node2.isNewExpression(right))
3928
+ return false;
3929
+ const constructorExpr = right.getExpression();
3930
+ if (!Node2.isIdentifier(constructorExpr) || constructorExpr.getText() !== "Set")
3931
+ return false;
3932
+ const signalName = fieldPath.slice(0, -6);
3933
+ const setAssignment = this.extractSetOperation(right, fieldPath, signalName);
3934
+ if (setAssignment) {
3935
+ assignments.push(setAssignment);
3936
+ }
3937
+ return true;
3938
+ }
3939
+ tryExtractMapConstructorPattern(fieldPath, right, assignments) {
3940
+ if (!fieldPath.endsWith(".value") || !Node2.isNewExpression(right))
3941
+ return false;
3942
+ const constructorExpr = right.getExpression();
3943
+ if (!Node2.isIdentifier(constructorExpr) || constructorExpr.getText() !== "Map")
3944
+ return false;
3945
+ const signalName = fieldPath.slice(0, -6);
3946
+ const mapAssignment = this.extractMapOperation(right, fieldPath, signalName);
3947
+ if (mapAssignment) {
3948
+ assignments.push(mapAssignment);
3949
+ }
3950
+ return true;
3951
+ }
3952
+ extractSetOperation(newExpr, fieldPath, signalName) {
3953
+ const args = newExpr.getArguments();
3954
+ if (args.length === 0) {
3955
+ return { field: signalName, value: "{}" };
3956
+ }
3957
+ const firstArg = args[0];
3958
+ if (!firstArg)
3959
+ return null;
3960
+ if (Node2.isArrayLiteralExpression(firstArg)) {
3961
+ return this.extractSetArrayOperation(firstArg, fieldPath, signalName);
3962
+ }
3963
+ if (Node2.isCallExpression(firstArg)) {
3964
+ return this.extractSetMethodChainOperation(firstArg, fieldPath, signalName);
3965
+ }
3966
+ return null;
3967
+ }
3968
+ extractSetArrayOperation(arrayLiteral, fieldPath, signalName) {
3969
+ const elements = arrayLiteral.getElements();
3970
+ if (elements.length < 1)
3971
+ return null;
3972
+ const firstElement = elements[0];
3973
+ const lastElement = elements[elements.length - 1];
3974
+ if (firstElement && Node2.isSpreadElement(firstElement)) {
3975
+ const spreadExpr = firstElement.getExpression();
3976
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
3977
+ return { field: signalName, value: "@ \\union {payload}" };
3978
+ }
3979
+ }
3980
+ if (lastElement && Node2.isSpreadElement(lastElement)) {
3981
+ const spreadExpr = lastElement.getExpression();
3982
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
3983
+ return { field: signalName, value: "{payload} \\union @" };
3984
+ }
3985
+ }
3986
+ return null;
3987
+ }
3988
+ extractSetMethodChainOperation(callExpr, fieldPath, signalName) {
3989
+ const expression = callExpr.getExpression();
3990
+ if (!Node2.isPropertyAccessExpression(expression))
3991
+ return null;
3992
+ const methodName = expression.getName();
3993
+ const sourceExpr = expression.getExpression();
3994
+ if (methodName === "filter" && Node2.isArrayLiteralExpression(sourceExpr)) {
3995
+ const elements = sourceExpr.getElements();
3996
+ if (elements.length === 1) {
3997
+ const spreadEl = elements[0];
3998
+ if (spreadEl && Node2.isSpreadElement(spreadEl)) {
3999
+ const spreadExpr = spreadEl.getExpression();
4000
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
4001
+ return { field: signalName, value: "@ \\ {payload}" };
4002
+ }
3720
4003
  }
3721
4004
  }
3722
- if (Node2.isShorthandPropertyAssignment(prop)) {
3723
- const name = prop.getName();
3724
- assignments.push({ field: name, value: "@" });
4005
+ }
4006
+ return null;
4007
+ }
4008
+ extractMapOperation(newExpr, fieldPath, signalName) {
4009
+ const args = newExpr.getArguments();
4010
+ if (args.length === 0) {
4011
+ return { field: signalName, value: "<<>>" };
4012
+ }
4013
+ const firstArg = args[0];
4014
+ if (!firstArg)
4015
+ return null;
4016
+ if (Node2.isArrayLiteralExpression(firstArg)) {
4017
+ return this.extractMapArrayOperation(firstArg, fieldPath, signalName);
4018
+ }
4019
+ if (Node2.isCallExpression(firstArg)) {
4020
+ return this.extractMapMethodChainOperation(firstArg, fieldPath, signalName);
4021
+ }
4022
+ return null;
4023
+ }
4024
+ extractMapArrayOperation(arrayLiteral, fieldPath, signalName) {
4025
+ const elements = arrayLiteral.getElements();
4026
+ if (elements.length < 1)
4027
+ return null;
4028
+ const firstElement = elements[0];
4029
+ if (firstElement && Node2.isSpreadElement(firstElement)) {
4030
+ const spreadExpr = firstElement.getExpression();
4031
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
4032
+ return { field: signalName, value: "[@ EXCEPT ![payload.key] = payload.value]" };
4033
+ }
4034
+ }
4035
+ return null;
4036
+ }
4037
+ extractMapMethodChainOperation(callExpr, fieldPath, signalName) {
4038
+ const expression = callExpr.getExpression();
4039
+ if (!Node2.isPropertyAccessExpression(expression))
4040
+ return null;
4041
+ const methodName = expression.getName();
4042
+ const sourceExpr = expression.getExpression();
4043
+ if (methodName === "filter" && Node2.isArrayLiteralExpression(sourceExpr)) {
4044
+ const elements = sourceExpr.getElements();
4045
+ if (elements.length === 1) {
4046
+ const spreadEl = elements[0];
4047
+ if (spreadEl && Node2.isSpreadElement(spreadEl)) {
4048
+ const spreadExpr = spreadEl.getExpression();
4049
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
4050
+ return { field: signalName, value: "[k \\in DOMAIN @ \\ {payload.key} |-> @[k]]" };
4051
+ }
4052
+ }
3725
4053
  }
3726
4054
  }
4055
+ return null;
4056
+ }
4057
+ extractArrayMethodOperation(callExpr, fieldPath, signalName) {
4058
+ const expression = callExpr.getExpression();
4059
+ if (!Node2.isPropertyAccessExpression(expression))
4060
+ return null;
4061
+ const methodName = expression.getName();
4062
+ const sourceExpr = expression.getExpression();
4063
+ const sourcePath = this.getPropertyPath(sourceExpr);
4064
+ if (sourcePath !== fieldPath)
4065
+ return null;
4066
+ switch (methodName) {
4067
+ case "filter":
4068
+ return { field: signalName, value: "SelectSeq(@, LAMBDA t: TRUE)" };
4069
+ case "map":
4070
+ return { field: signalName, value: "[i \\in DOMAIN @ |-> @[i]]" };
4071
+ case "slice":
4072
+ return { field: signalName, value: "SubSeq(@, 1, Len(@))" };
4073
+ case "concat":
4074
+ return { field: signalName, value: "@ \\o <<payload>>" };
4075
+ case "reverse":
4076
+ return { field: signalName, value: "[i \\in DOMAIN @ |-> @[Len(@) - i + 1]]" };
4077
+ default:
4078
+ this.warnUnsupportedArrayMethod(methodName, callExpr);
4079
+ return null;
4080
+ }
4081
+ }
4082
+ warnUnsupportedArrayMethod(methodName, node) {
4083
+ const mutatingMethods = [
4084
+ "push",
4085
+ "pop",
4086
+ "shift",
4087
+ "unshift",
4088
+ "splice",
4089
+ "sort",
4090
+ "fill",
4091
+ "copyWithin"
4092
+ ];
4093
+ const queryMethods = [
4094
+ "find",
4095
+ "findIndex",
4096
+ "reduce",
4097
+ "reduceRight",
4098
+ "some",
4099
+ "every",
4100
+ "includes",
4101
+ "indexOf",
4102
+ "lastIndexOf"
4103
+ ];
4104
+ const otherMethods = ["flat", "flatMap", "join", "toString", "toLocaleString"];
4105
+ if (mutatingMethods.includes(methodName)) {
4106
+ this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' mutates in place. Use spread syntax: [...arr, item] for append, arr.filter() for removal.`);
4107
+ } else if (queryMethods.includes(methodName)) {
4108
+ this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' returns a single value, not a new array. State assignment won't be extracted.`);
4109
+ } else if (otherMethods.includes(methodName)) {
4110
+ this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' is not supported for state extraction. Consider using map/filter instead.`);
4111
+ }
4112
+ }
4113
+ extractSpreadUpdateAssignments(objectLiteral, assignments, signalName) {
4114
+ for (const prop of objectLiteral.getProperties()) {
4115
+ if (Node2.isSpreadAssignment(prop))
4116
+ continue;
4117
+ this.extractPropertyAssignment(prop, assignments, signalName);
4118
+ }
4119
+ }
4120
+ extractArraySpreadOperation(arrayLiteral, fieldPath, signalName) {
4121
+ const elements = arrayLiteral.getElements();
4122
+ if (elements.length < 1)
4123
+ return null;
4124
+ return this.tryExtractAppendOperation(elements, fieldPath, signalName) ?? this.tryExtractPrependOperation(elements, fieldPath, signalName);
4125
+ }
4126
+ tryExtractAppendOperation(elements, fieldPath, signalName) {
4127
+ const firstElement = elements[0];
4128
+ if (!firstElement || !Node2.isSpreadElement(firstElement))
4129
+ return null;
4130
+ const spreadExpr = firstElement.getExpression();
4131
+ if (!spreadExpr || this.getPropertyPath(spreadExpr) !== fieldPath)
4132
+ return null;
4133
+ if (elements.length === 2) {
4134
+ return { field: signalName, value: "Append(@, payload)" };
4135
+ }
4136
+ const placeholders = Array(elements.length - 1).fill("payload").join(", ");
4137
+ return { field: signalName, value: `@ \\o <<${placeholders}>>` };
4138
+ }
4139
+ tryExtractPrependOperation(elements, fieldPath, signalName) {
4140
+ if (elements.length < 2)
4141
+ return null;
4142
+ const lastElement = elements[elements.length - 1];
4143
+ if (!lastElement || !Node2.isSpreadElement(lastElement))
4144
+ return null;
4145
+ const spreadExpr = lastElement.getExpression();
4146
+ if (!spreadExpr || this.getPropertyPath(spreadExpr) !== fieldPath)
4147
+ return null;
4148
+ if (elements.length === 2) {
4149
+ return { field: signalName, value: "<<payload>> \\o @" };
4150
+ }
4151
+ const placeholders = Array(elements.length - 1).fill("payload").join(", ");
4152
+ return { field: signalName, value: `<<${placeholders}>> \\o @` };
4153
+ }
4154
+ extractObjectLiteralAssignments(objectLiteral, assignments, signalName) {
4155
+ if (!Node2.isObjectLiteralExpression(objectLiteral))
4156
+ return;
4157
+ for (const prop of objectLiteral.getProperties()) {
4158
+ this.extractPropertyAssignment(prop, assignments, signalName);
4159
+ }
4160
+ }
4161
+ extractPropertyAssignment(prop, assignments, signalName) {
4162
+ if (Node2.isPropertyAssignment(prop)) {
4163
+ const name = prop.getName();
4164
+ const initializer = prop.getInitializer();
4165
+ if (!name || !initializer)
4166
+ return;
4167
+ const value = this.extractValue(initializer);
4168
+ if (value === undefined)
4169
+ return;
4170
+ const field = signalName ? `${signalName}_${name}` : name;
4171
+ assignments.push({ field, value });
4172
+ return;
4173
+ }
4174
+ if (Node2.isShorthandPropertyAssignment(prop)) {
4175
+ const name = prop.getName();
4176
+ const field = signalName ? `${signalName}_${name}` : name;
4177
+ assignments.push({ field, value: "@" });
4178
+ }
3727
4179
  }
3728
4180
  extractElementAccessAssignment(left, right, assignments) {
3729
4181
  if (!Node2.isElementAccessExpression(left))
@@ -4736,15 +5188,15 @@ class HandlerExtractor {
4736
5188
  if (path2 === `${varName}.value`) {
4737
5189
  const right = node.getRight();
4738
5190
  if (Node2.isObjectLiteralExpression(right)) {
4739
- this.extractObjectLiteralAssignments(right, mutations);
5191
+ this.extractObjectLiteralAssignments(right, mutations, varName);
4740
5192
  }
4741
5193
  break;
4742
5194
  }
4743
5195
  const fieldPrefix = `${varName}.value.`;
4744
5196
  if (path2.startsWith(fieldPrefix)) {
4745
- const field = path2.substring(fieldPrefix.length);
5197
+ const fieldName = path2.substring(fieldPrefix.length);
4746
5198
  const value = this.extractValue(node.getRight());
4747
- mutations.push({ field, value: value ?? "@" });
5199
+ mutations.push({ field: `${varName}_${fieldName}`, value: value ?? "@" });
4748
5200
  break;
4749
5201
  }
4750
5202
  }
@@ -5659,4 +6111,4 @@ main().catch((error) => {
5659
6111
  process.exit(1);
5660
6112
  });
5661
6113
 
5662
- //# debugId=B1C82DC4E0975F9164756E2164756E21
6114
+ //# debugId=143C5A4485F127EB64756E2164756E21