@fairfox/polly 0.15.1 → 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") {
@@ -1203,8 +1211,12 @@ class TLAGenerator {
1203
1211
  return expr;
1204
1212
  }
1205
1213
  tla = tla.replace(/'([^']+)'/g, '"$1"');
1206
- tla = tla.replace(/([a-zA-Z_][a-zA-Z0-9_]*)\.value\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, _stateName, path3) => {
1207
- return `${statePrefix}.${this.sanitizeFieldName(path3)}`;
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}`;
1208
1220
  });
1209
1221
  tla = tla.replace(/state\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, path3) => {
1210
1222
  return `${statePrefix}.${this.sanitizeFieldName(path3)}`;
@@ -1227,13 +1239,99 @@ class TLAGenerator {
1227
1239
  tla = tla.replace(/>/g, ">");
1228
1240
  tla = tla.replace(/<=/g, "<=");
1229
1241
  tla = tla.replace(/>=/g, ">=");
1242
+ tla = this.convertFunctionParamsToPayload(tla);
1230
1243
  return tla;
1231
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
+ }
1232
1307
  translateArrayOperations(expr, _statePrefix) {
1233
1308
  if (!expr || typeof expr !== "string") {
1234
1309
  return expr || "";
1235
1310
  }
1236
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
+ });
1237
1335
  result = result.replace(/(\w+(?:\.\w+)*)\.length\b/g, (_match, arrayRef) => {
1238
1336
  if (arrayRef.startsWith("state.")) {
1239
1337
  return `Len(${arrayRef})`;
@@ -1523,13 +1621,14 @@ class TLAGenerator {
1523
1621
  this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
1524
1622
  this.line(" pendingRequests[id]]");
1525
1623
  this.line(" /\\ time' = time + 1");
1624
+ this.line(" /\\ \\E p \\in PayloadType : payload' = p \\* Non-deterministic payload");
1526
1625
  this.line(" /\\ StateTransition(target, msg.msgType)");
1527
1626
  this.line(" ELSE \\* Port not connected - message fails");
1528
1627
  this.line(` /\\ messages' = [messages EXCEPT ![msgIndex].status = "failed"]`);
1529
1628
  this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
1530
1629
  this.line(" pendingRequests[id]]");
1531
1630
  this.line(" /\\ time' = time + 1");
1532
- this.line(" /\\ UNCHANGED <<delivered, contextStates>>");
1631
+ this.line(" /\\ UNCHANGED <<delivered, contextStates, payload>>");
1533
1632
  this.line(" /\\ UNCHANGED ports");
1534
1633
  this.indent--;
1535
1634
  this.line("");
@@ -1540,17 +1639,17 @@ class TLAGenerator {
1540
1639
  this.indent++;
1541
1640
  const hasValidHandlers = analysis.handlers.some((h) => this.isValidTLAIdentifier(h.messageType));
1542
1641
  if (hasValidHandlers) {
1543
- this.line("\\/ \\E c \\in Contexts : ConnectPort(c) /\\ UNCHANGED contextStates");
1544
- 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>>");
1545
1644
  this.line("\\/ \\E src \\in Contexts : \\E targetSet \\in (SUBSET Contexts \\ {{}}) : \\E tab \\in 0..MaxTabId : \\E msgType \\in UserMessageTypes :");
1546
1645
  this.indent++;
1547
- this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED contextStates");
1646
+ this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED <<contextStates, payload>>");
1548
1647
  this.indent--;
1549
1648
  this.line("\\/ \\E i \\in 1..Len(messages) : UserRouteMessage(i)");
1550
- this.line("\\/ CompleteRouting /\\ UNCHANGED contextStates");
1551
- 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>>");
1552
1651
  } else {
1553
- this.line("\\/ Next /\\ UNCHANGED contextStates");
1652
+ this.line("\\/ Next /\\ UNCHANGED <<contextStates, payload>>");
1554
1653
  }
1555
1654
  this.indent--;
1556
1655
  this.line("");
@@ -1667,9 +1766,15 @@ class TLAGenerator {
1667
1766
  this.line("=============================================================================");
1668
1767
  }
1669
1768
  fieldConfigToTLAType(_fieldPath, fieldConfig, _config) {
1670
- 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);
1671
1770
  return typeResult || "Value";
1672
1771
  }
1772
+ tryAbstractType(fieldConfig) {
1773
+ if ("abstract" in fieldConfig && fieldConfig.abstract === true) {
1774
+ return "Value";
1775
+ }
1776
+ return null;
1777
+ }
1673
1778
  tryBooleanType(fieldConfig) {
1674
1779
  if ("type" in fieldConfig && fieldConfig.type === "boolean") {
1675
1780
  return "BOOLEAN";
@@ -1824,6 +1929,9 @@ class TLAGenerator {
1824
1929
  }
1825
1930
  }
1826
1931
  getInitialValue(fieldConfig) {
1932
+ if ("abstract" in fieldConfig && fieldConfig.abstract === true) {
1933
+ return '"v1"';
1934
+ }
1827
1935
  if (Array.isArray(fieldConfig)) {
1828
1936
  if (fieldConfig.length > 0 && typeof fieldConfig[0] === "boolean") {
1829
1937
  return fieldConfig[0] ? "TRUE" : "FALSE";
@@ -3392,6 +3500,7 @@ class HandlerExtractor {
3392
3500
  relationshipExtractor;
3393
3501
  analyzedFiles;
3394
3502
  packageRoot;
3503
+ warnings;
3395
3504
  constructor(tsConfigPath) {
3396
3505
  this.project = new Project({
3397
3506
  tsConfigFilePath: tsConfigPath
@@ -3399,8 +3508,27 @@ class HandlerExtractor {
3399
3508
  this.typeGuardCache = new WeakMap;
3400
3509
  this.relationshipExtractor = new RelationshipExtractor;
3401
3510
  this.analyzedFiles = new Set;
3511
+ this.warnings = [];
3402
3512
  this.packageRoot = this.findPackageRoot(tsConfigPath);
3403
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
+ }
3404
3532
  findPackageRoot(tsConfigPath) {
3405
3533
  let dir = tsConfigPath.substring(0, tsConfigPath.lastIndexOf("/"));
3406
3534
  while (dir.length > 1) {
@@ -3424,6 +3552,7 @@ class HandlerExtractor {
3424
3552
  const invalidMessageTypes = new Set;
3425
3553
  const stateConstraints = [];
3426
3554
  const verifiedStates = [];
3555
+ this.warnings = [];
3427
3556
  const allSourceFiles = this.project.getSourceFiles();
3428
3557
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
3429
3558
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
@@ -3458,7 +3587,8 @@ class HandlerExtractor {
3458
3587
  handlers,
3459
3588
  messageTypes,
3460
3589
  stateConstraints,
3461
- verifiedStates
3590
+ verifiedStates,
3591
+ warnings: this.warnings
3462
3592
  };
3463
3593
  }
3464
3594
  analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
@@ -3688,46 +3818,364 @@ class HandlerExtractor {
3688
3818
  if (!Node2.isPropertyAccessExpression(left))
3689
3819
  return;
3690
3820
  const fieldPath = this.getPropertyPath(left);
3691
- if (fieldPath.startsWith("state.")) {
3692
- const field = fieldPath.substring(6);
3693
- const value = this.extractValue(right);
3694
- if (value !== undefined) {
3695
- assignments.push({ field, value });
3696
- }
3821
+ if (this.tryExtractStateFieldPattern(fieldPath, right, assignments))
3697
3822
  return;
3698
- }
3699
- const valueMatch = fieldPath.match(/\.value\.(.+)$/);
3700
- if (valueMatch?.[1]) {
3701
- const field = valueMatch[1];
3702
- const value = this.extractValue(right);
3703
- if (value !== undefined) {
3704
- assignments.push({ field, value });
3705
- }
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))
3706
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 });
3707
3842
  }
3708
- if (fieldPath.endsWith(".value") && Node2.isObjectLiteralExpression(right)) {
3709
- 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 });
3710
3854
  }
3855
+ return true;
3711
3856
  }
3712
- extractObjectLiteralAssignments(objectLiteral, assignments) {
3713
- 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))
3714
3904
  return;
3715
- for (const prop of objectLiteral.getProperties()) {
3716
- if (Node2.isPropertyAssignment(prop)) {
3717
- const name = prop.getName();
3718
- const initializer = prop.getInitializer();
3719
- if (!name || !initializer)
3720
- continue;
3721
- const value = this.extractValue(initializer);
3722
- if (value !== undefined) {
3723
- 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
+ }
3724
4003
  }
3725
4004
  }
3726
- if (Node2.isShorthandPropertyAssignment(prop)) {
3727
- const name = prop.getName();
3728
- 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
+ }
3729
4053
  }
3730
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
+ }
3731
4179
  }
3732
4180
  extractElementAccessAssignment(left, right, assignments) {
3733
4181
  if (!Node2.isElementAccessExpression(left))
@@ -4740,15 +5188,15 @@ class HandlerExtractor {
4740
5188
  if (path2 === `${varName}.value`) {
4741
5189
  const right = node.getRight();
4742
5190
  if (Node2.isObjectLiteralExpression(right)) {
4743
- this.extractObjectLiteralAssignments(right, mutations);
5191
+ this.extractObjectLiteralAssignments(right, mutations, varName);
4744
5192
  }
4745
5193
  break;
4746
5194
  }
4747
5195
  const fieldPrefix = `${varName}.value.`;
4748
5196
  if (path2.startsWith(fieldPrefix)) {
4749
- const field = path2.substring(fieldPrefix.length);
5197
+ const fieldName = path2.substring(fieldPrefix.length);
4750
5198
  const value = this.extractValue(node.getRight());
4751
- mutations.push({ field, value: value ?? "@" });
5199
+ mutations.push({ field: `${varName}_${fieldName}`, value: value ?? "@" });
4752
5200
  break;
4753
5201
  }
4754
5202
  }
@@ -5663,4 +6111,4 @@ main().catch((error) => {
5663
6111
  process.exit(1);
5664
6112
  });
5665
6113
 
5666
- //# debugId=102A9867273619A364756E2164756E21
6114
+ //# debugId=143C5A4485F127EB64756E2164756E21