@fairfox/polly 0.18.0 → 0.20.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.
@@ -429,13 +429,22 @@ async function generateSubsystemTLA(_subsystemName, subsystem, config, analysis)
429
429
  temporalConstraints: filteredConfig.tier2.temporalConstraints.filter((tc) => handlerNames.has(tc.before) && handlerNames.has(tc.after))
430
430
  };
431
431
  }
432
+ const subsystemFieldPatterns = subsystem.state.map((field) => {
433
+ const dotIdx = field.indexOf(".");
434
+ if (dotIdx >= 0) {
435
+ return `${field.substring(0, dotIdx)}.value.${field.substring(dotIdx + 1)}`;
436
+ }
437
+ return `${field}.value`;
438
+ });
439
+ const filteredGlobalStateConstraints = (analysis.globalStateConstraints ?? []).filter((constraint) => subsystemFieldPatterns.some((pattern) => constraint.expression.includes(pattern)));
432
440
  const filteredAnalysis = {
433
441
  ...analysis,
434
442
  messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
435
- handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType))
443
+ handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType)),
444
+ globalStateConstraints: filteredGlobalStateConstraints
436
445
  };
437
446
  const generator = new TLAGenerator;
438
- return await generator.generate(filteredConfig, filteredAnalysis);
447
+ return await generator.generate(filteredConfig, filteredAnalysis, `UserApp_${_subsystemName}`);
439
448
  }
440
449
  var TLAValidationError, TLAGenerator;
441
450
  var init_tla = __esm(() => {
@@ -458,6 +467,7 @@ var init_tla = __esm(() => {
458
467
  resolvedActionNames = new Map;
459
468
  tabSymmetryEnabled = false;
460
469
  tabCount = 0;
470
+ moduleName = "UserApp";
461
471
  constructor(options) {
462
472
  this.options = options;
463
473
  }
@@ -467,7 +477,10 @@ var init_tla = __esm(() => {
467
477
  }
468
478
  return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s);
469
479
  }
470
- async generate(config, analysis) {
480
+ async generate(config, analysis, moduleName) {
481
+ if (moduleName) {
482
+ this.moduleName = moduleName;
483
+ }
471
484
  this.validateInputs(config, analysis);
472
485
  this.extractInvariantsIfEnabled();
473
486
  this.generateTemporalPropertiesIfEnabled(analysis);
@@ -757,7 +770,7 @@ var init_tla = __esm(() => {
757
770
  }
758
771
  }
759
772
  addHeader() {
760
- this.line("------------------------- MODULE UserApp -------------------------");
773
+ this.line(`------------------------- MODULE ${this.moduleName} -------------------------`);
761
774
  this.line("(*");
762
775
  this.line(" Auto-generated TLA+ specification for web extension");
763
776
  this.line(" ");
@@ -1349,32 +1362,86 @@ var init_tla = __esm(() => {
1349
1362
  if (assignment.value !== null)
1350
1363
  return true;
1351
1364
  const fieldConfig = state[assignment.field];
1352
- return !!(fieldConfig && typeof fieldConfig === "object" && ("values" in fieldConfig) && fieldConfig.values);
1365
+ if (!fieldConfig || typeof fieldConfig !== "object")
1366
+ return false;
1367
+ if ("abstract" in fieldConfig && fieldConfig.abstract)
1368
+ return true;
1369
+ if ("nullable" in fieldConfig && fieldConfig.nullable)
1370
+ return true;
1371
+ return !!(("values" in fieldConfig) && fieldConfig.values);
1353
1372
  }
1354
1373
  mapNullAssignment(assignment, state) {
1355
1374
  if (assignment.value !== null)
1356
1375
  return assignment;
1357
1376
  const fieldConfig = state[assignment.field];
1358
- if (fieldConfig && typeof fieldConfig === "object" && "values" in fieldConfig && fieldConfig.values) {
1377
+ if (!fieldConfig || typeof fieldConfig !== "object")
1378
+ return assignment;
1379
+ if ("abstract" in fieldConfig && fieldConfig.abstract)
1380
+ return assignment;
1381
+ if ("nullable" in fieldConfig && fieldConfig.nullable)
1382
+ return assignment;
1383
+ if ("values" in fieldConfig && fieldConfig.values) {
1359
1384
  const nullValue = fieldConfig.values[fieldConfig.values.length - 1];
1360
1385
  return { ...assignment, value: nullValue };
1361
1386
  }
1362
1387
  return assignment;
1363
1388
  }
1364
1389
  emitStateUpdates(validAssignments, preconditions) {
1365
- if (validAssignments.length > 0) {
1366
- const exceptClauses = validAssignments.map((a) => {
1390
+ if (validAssignments.length === 0) {
1391
+ if (preconditions.length === 0) {
1392
+ this.line("\\* No state changes in handler");
1393
+ }
1394
+ this.line("/\\ UNCHANGED contextStates");
1395
+ return true;
1396
+ }
1397
+ const ndetAssignments = [];
1398
+ const detAssignments = [];
1399
+ for (const a of validAssignments) {
1400
+ if (typeof a.value === "string" && a.value.startsWith("NDET:")) {
1401
+ ndetAssignments.push({ field: a.field, value: a.value });
1402
+ } else {
1403
+ detAssignments.push(a);
1404
+ }
1405
+ }
1406
+ if (ndetAssignments.length === 0) {
1407
+ const exceptClauses = detAssignments.map((a) => {
1367
1408
  const tlaValue = this.assignmentValueToTLA(a.value);
1368
1409
  return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
1369
1410
  });
1370
1411
  this.line(`/\\ contextStates' = [contextStates EXCEPT ${exceptClauses.join(", ")}]`);
1371
1412
  return false;
1372
1413
  }
1373
- if (preconditions.length === 0) {
1374
- this.line("\\* No state changes in handler");
1414
+ this.emitNDETStateUpdates(ndetAssignments, detAssignments);
1415
+ return false;
1416
+ }
1417
+ emitNDETStateUpdates(ndetAssignments, detAssignments) {
1418
+ const detExceptClauses = detAssignments.map((a) => {
1419
+ const tlaValue = this.assignmentValueToTLA(a.value);
1420
+ return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
1421
+ });
1422
+ const ndetExceptClauses = [];
1423
+ const quantifierOpeners = [];
1424
+ for (const a of ndetAssignments) {
1425
+ const fieldName = this.sanitizeFieldName(a.field);
1426
+ const fieldRef = `contextStates[ctx].${fieldName}`;
1427
+ if (a.value === "NDET:FILTER") {
1428
+ quantifierOpeners.push(`/\\ \\E newLen_${fieldName} \\in 0..Len(${fieldRef}) :`);
1429
+ ndetExceptClauses.push(`![ctx].${fieldName} = SubSeq(@, 1, newLen_${fieldName})`);
1430
+ } else if (a.value === "NDET:MAP") {
1431
+ quantifierOpeners.push(`/\\ \\E mapIdx_${fieldName} \\in 1..Len(${fieldRef}) :`);
1432
+ quantifierOpeners.push(`\\E mapVal_${fieldName} \\in Value :`);
1433
+ ndetExceptClauses.push(`![ctx].${fieldName} = [@ EXCEPT ![mapIdx_${fieldName}] = mapVal_${fieldName}]`);
1434
+ }
1435
+ }
1436
+ for (const opener of quantifierOpeners) {
1437
+ this.line(opener);
1438
+ this.indent++;
1439
+ }
1440
+ const allExceptClauses = [...detExceptClauses, ...ndetExceptClauses];
1441
+ this.line(`/\\ contextStates' = [contextStates EXCEPT ${allExceptClauses.join(", ")}]`);
1442
+ for (const _opener of quantifierOpeners) {
1443
+ this.indent--;
1375
1444
  }
1376
- this.line("/\\ UNCHANGED contextStates");
1377
- return true;
1378
1445
  }
1379
1446
  tsExpressionToTLA(expr, isPrimed = false) {
1380
1447
  if (!expr || typeof expr !== "string") {
@@ -1787,6 +1854,9 @@ var init_tla = __esm(() => {
1787
1854
  return "NULL";
1788
1855
  }
1789
1856
  if (typeof value === "string") {
1857
+ if (value.startsWith("NDET:")) {
1858
+ throw new Error(`NDET marker "${value}" reached assignmentValueToTLA — should have been partitioned in emitStateUpdates`);
1859
+ }
1790
1860
  if (value.startsWith("param:")) {
1791
1861
  const paramName = value.substring(6);
1792
1862
  return `payload.${this.sanitizeFieldName(paramName)}`;
@@ -2011,6 +2081,9 @@ var init_tla = __esm(() => {
2011
2081
  }
2012
2082
  tryAbstractType(fieldConfig) {
2013
2083
  if ("abstract" in fieldConfig && fieldConfig.abstract === true) {
2084
+ if ("nullable" in fieldConfig && fieldConfig.nullable === true) {
2085
+ return "Value \\union {NULL}";
2086
+ }
2014
2087
  return "Value";
2015
2088
  }
2016
2089
  return null;
@@ -2178,6 +2251,9 @@ var init_tla = __esm(() => {
2178
2251
  }
2179
2252
  getInitialValue(fieldConfig) {
2180
2253
  if ("abstract" in fieldConfig && fieldConfig.abstract === true) {
2254
+ if ("nullable" in fieldConfig && fieldConfig.nullable === true) {
2255
+ return "NULL";
2256
+ }
2181
2257
  return '"v1"';
2182
2258
  }
2183
2259
  if (Array.isArray(fieldConfig)) {
@@ -2869,7 +2945,7 @@ class ConfigGenerator {
2869
2945
  this.addHeader();
2870
2946
  this.addImports();
2871
2947
  this.addExport();
2872
- this.addStateConfig(analysis.fields);
2948
+ this.addStateConfig(analysis.fields, analysis.resources);
2873
2949
  this.addMessagesConfig();
2874
2950
  this.addBehaviorConfig();
2875
2951
  this.closeExport();
@@ -2935,7 +3011,7 @@ class ConfigGenerator {
2935
3011
  this.indent--;
2936
3012
  this.line("})");
2937
3013
  }
2938
- addStateConfig(fields) {
3014
+ addStateConfig(fields, resources) {
2939
3015
  this.line("state: {");
2940
3016
  this.indent++;
2941
3017
  for (let i = 0;i < fields.length; i++) {
@@ -2947,6 +3023,18 @@ class ConfigGenerator {
2947
3023
  }
2948
3024
  this.addFieldConfig(field);
2949
3025
  }
3026
+ if (resources && resources.length > 0) {
3027
+ if (fields.length > 0) {
3028
+ this.line("");
3029
+ }
3030
+ this.line("// ─── $resource async lifecycle fields (auto-generated) ───");
3031
+ for (const resource of resources) {
3032
+ this.line("");
3033
+ this.line(`// ${resource.name}: fetch lifecycle status`);
3034
+ this.line(`"${resource.name}_status": { type: "enum", values: ["idle", "loading", "success", "error"] },`);
3035
+ this.line(`"${resource.name}_error": { type: "boolean" },`);
3036
+ }
3037
+ }
2950
3038
  this.indent--;
2951
3039
  this.line("},");
2952
3040
  this.line("");
@@ -4277,12 +4365,13 @@ class HandlerExtractor {
4277
4365
  const stateConstraints = [];
4278
4366
  const globalStateConstraints = [];
4279
4367
  const verifiedStates = [];
4368
+ const resources = [];
4280
4369
  this.warnings = [];
4281
4370
  const allSourceFiles = this.project.getSourceFiles();
4282
4371
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
4283
4372
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
4284
4373
  for (const entryPoint of entryPoints) {
4285
- this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
4374
+ this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates, resources);
4286
4375
  }
4287
4376
  if (verifiedStates.length > 0) {
4288
4377
  if (process.env["POLLY_DEBUG"]) {
@@ -4314,10 +4403,11 @@ class HandlerExtractor {
4314
4403
  stateConstraints,
4315
4404
  globalStateConstraints,
4316
4405
  verifiedStates,
4406
+ resources,
4317
4407
  warnings: this.warnings
4318
4408
  };
4319
4409
  }
4320
- analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
4410
+ analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates, resources) {
4321
4411
  const filePath = sourceFile.getFilePath();
4322
4412
  if (this.analyzedFiles.has(filePath)) {
4323
4413
  return;
@@ -4335,6 +4425,20 @@ class HandlerExtractor {
4335
4425
  globalStateConstraints.push(...fileGlobalConstraints);
4336
4426
  const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
4337
4427
  verifiedStates.push(...fileVerifiedStates);
4428
+ const fileResources = this.extractResourcesFromFile(sourceFile, filePath);
4429
+ for (const resource of fileResources) {
4430
+ resources.push(resource);
4431
+ const context = this.inferContext(filePath);
4432
+ const syntheticHandlers = this.createResourceHandlers(resource, context);
4433
+ for (const handler of syntheticHandlers) {
4434
+ handlers.push(handler);
4435
+ if (this.isValidTLAIdentifier(handler.messageType)) {
4436
+ messageTypes.add(handler.messageType);
4437
+ } else {
4438
+ invalidMessageTypes.add(handler.messageType);
4439
+ }
4440
+ }
4441
+ }
4338
4442
  const importDeclarations = sourceFile.getImportDeclarations();
4339
4443
  for (const importDecl of importDeclarations) {
4340
4444
  const importedFile = importDecl.getModuleSpecifierSourceFile();
@@ -4346,7 +4450,7 @@ class HandlerExtractor {
4346
4450
  }
4347
4451
  continue;
4348
4452
  }
4349
- this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
4453
+ this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates, resources);
4350
4454
  } else if (process.env["POLLY_DEBUG"]) {
4351
4455
  const specifier = importDecl.getModuleSpecifierValue();
4352
4456
  if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
@@ -4705,7 +4809,9 @@ class HandlerExtractor {
4705
4809
  return;
4706
4810
  if (this.tryExtractSetConstructorPattern(fieldPath, right, assignments))
4707
4811
  return;
4708
- this.tryExtractMapConstructorPattern(fieldPath, right, assignments);
4812
+ if (this.tryExtractMapConstructorPattern(fieldPath, right, assignments))
4813
+ return;
4814
+ this.tryExtractSignalDirectValuePattern(fieldPath, right, assignments);
4709
4815
  }
4710
4816
  tryExtractStateFieldPattern(fieldPath, right, assignments) {
4711
4817
  if (!fieldPath.startsWith("state."))
@@ -4824,6 +4930,25 @@ class HandlerExtractor {
4824
4930
  }
4825
4931
  return true;
4826
4932
  }
4933
+ tryExtractSignalDirectValuePattern(fieldPath, right, assignments) {
4934
+ if (!fieldPath.endsWith(".value"))
4935
+ return false;
4936
+ const signalName = fieldPath.slice(0, -6);
4937
+ const literalValue = this.extractValue(right);
4938
+ if (literalValue !== undefined) {
4939
+ assignments.push({ field: signalName, value: literalValue });
4940
+ return true;
4941
+ }
4942
+ if (Node2.isPropertyAccessExpression(right)) {
4943
+ const rightPath = this.getPropertyPath(right);
4944
+ const parts = rightPath.split(".");
4945
+ if (parts.length === 2 && parts[0] !== undefined && parts[1] !== undefined && this.currentFunctionParams.includes(parts[0])) {
4946
+ assignments.push({ field: signalName, value: `param:${parts[1]}` });
4947
+ return true;
4948
+ }
4949
+ }
4950
+ return false;
4951
+ }
4827
4952
  extractSetOperation(newExpr, fieldPath, signalName) {
4828
4953
  const args = newExpr.getArguments();
4829
4954
  if (args.length === 0) {
@@ -4940,11 +5065,11 @@ class HandlerExtractor {
4940
5065
  return null;
4941
5066
  switch (methodName) {
4942
5067
  case "filter":
4943
- return { field: signalName, value: "SelectSeq(@, LAMBDA t: TRUE)" };
5068
+ return { field: signalName, value: "NDET:FILTER" };
4944
5069
  case "map":
4945
- return { field: signalName, value: "[i \\in DOMAIN @ |-> @[i]]" };
5070
+ return { field: signalName, value: "NDET:MAP" };
4946
5071
  case "slice":
4947
- return { field: signalName, value: "SubSeq(@, 1, Len(@))" };
5072
+ return { field: signalName, value: "NDET:FILTER" };
4948
5073
  case "concat":
4949
5074
  return { field: signalName, value: "@ \\o <<payload>>" };
4950
5075
  case "reverse":
@@ -6215,6 +6340,125 @@ class HandlerExtractor {
6215
6340
  }
6216
6341
  return name || funcName;
6217
6342
  }
6343
+ extractResourcesFromFile(sourceFile, filePath) {
6344
+ const resources = [];
6345
+ sourceFile.forEachDescendant((node) => {
6346
+ if (!Node2.isCallExpression(node))
6347
+ return;
6348
+ const resource = this.extractResourcePattern(node, filePath);
6349
+ if (resource) {
6350
+ resources.push(resource);
6351
+ }
6352
+ });
6353
+ return resources;
6354
+ }
6355
+ extractResourcePattern(node, filePath) {
6356
+ const expression = node.getExpression();
6357
+ if (!Node2.isIdentifier(expression))
6358
+ return null;
6359
+ if (expression.getText() !== "$resource")
6360
+ return null;
6361
+ const args = node.getArguments();
6362
+ if (args.length < 2)
6363
+ return null;
6364
+ const nameArg = args[0];
6365
+ if (!nameArg || !Node2.isStringLiteral(nameArg))
6366
+ return null;
6367
+ const name = nameArg.getLiteralValue();
6368
+ const optionsArg = args[1];
6369
+ if (!optionsArg || !Node2.isObjectLiteralExpression(optionsArg))
6370
+ return null;
6371
+ const sourceSignals = this.extractResourceSourceReads(optionsArg);
6372
+ const variableName = this.getVariableNameFromParent(node) || name;
6373
+ if (process.env["POLLY_DEBUG"]) {
6374
+ console.log(`[DEBUG] Found $resource: ${variableName} (name: "${name}") with source signals: [${sourceSignals.join(", ")}]`);
6375
+ }
6376
+ return {
6377
+ name,
6378
+ variableName,
6379
+ filePath,
6380
+ line: node.getStartLineNumber(),
6381
+ sourceSignals
6382
+ };
6383
+ }
6384
+ extractResourceSourceReads(optionsObj) {
6385
+ const signals = [];
6386
+ const sourceProp = optionsObj.getProperty("source");
6387
+ if (!sourceProp || !Node2.isPropertyAssignment(sourceProp))
6388
+ return signals;
6389
+ const sourceInit = sourceProp.getInitializer();
6390
+ if (!sourceInit)
6391
+ return signals;
6392
+ sourceInit.forEachDescendant((node) => {
6393
+ if (!Node2.isPropertyAccessExpression(node))
6394
+ return;
6395
+ const text = node.getText();
6396
+ const match = text.match(/^(\w+)\.value(?:\.(\w+))?$/);
6397
+ if (match) {
6398
+ const signalName = match[1];
6399
+ const fieldName = match[2];
6400
+ if (fieldName) {
6401
+ signals.push(`${signalName}_${fieldName}`);
6402
+ } else {
6403
+ signals.push(signalName);
6404
+ }
6405
+ }
6406
+ });
6407
+ return signals;
6408
+ }
6409
+ createResourceHandlers(resource, context) {
6410
+ const { name, filePath, line } = resource;
6411
+ const location = { file: filePath, line };
6412
+ const fetchStart = {
6413
+ messageType: `${name}_FetchStart`,
6414
+ node: context,
6415
+ assignments: [
6416
+ { field: `${name}_status`, value: "loading" },
6417
+ { field: `${name}_error`, value: false }
6418
+ ],
6419
+ preconditions: [
6420
+ {
6421
+ expression: `${name}_status !== "loading"`,
6422
+ location: { line, column: 0 }
6423
+ }
6424
+ ],
6425
+ postconditions: [],
6426
+ location
6427
+ };
6428
+ const fetchSuccess = {
6429
+ messageType: `${name}_FetchSuccess`,
6430
+ node: context,
6431
+ assignments: [
6432
+ { field: `${name}_status`, value: "success" },
6433
+ { field: `${name}_error`, value: false }
6434
+ ],
6435
+ preconditions: [
6436
+ {
6437
+ expression: `${name}_status === "loading"`,
6438
+ location: { line, column: 0 }
6439
+ }
6440
+ ],
6441
+ postconditions: [],
6442
+ location
6443
+ };
6444
+ const fetchError = {
6445
+ messageType: `${name}_FetchError`,
6446
+ node: context,
6447
+ assignments: [
6448
+ { field: `${name}_status`, value: "error" },
6449
+ { field: `${name}_error`, value: true }
6450
+ ],
6451
+ preconditions: [
6452
+ {
6453
+ expression: `${name}_status === "loading"`,
6454
+ location: { line, column: 0 }
6455
+ }
6456
+ ],
6457
+ postconditions: [],
6458
+ location
6459
+ };
6460
+ return [fetchStart, fetchSuccess, fetchError];
6461
+ }
6218
6462
  }
6219
6463
 
6220
6464
  // tools/analysis/src/extract/types.ts
@@ -6248,7 +6492,9 @@ class TypeExtractor {
6248
6492
  fields,
6249
6493
  handlers: completeHandlers,
6250
6494
  stateConstraints: handlerAnalysis.stateConstraints,
6251
- globalStateConstraints: handlerAnalysis.globalStateConstraints
6495
+ globalStateConstraints: handlerAnalysis.globalStateConstraints,
6496
+ verifiedStates: handlerAnalysis.verifiedStates,
6497
+ resources: handlerAnalysis.resources
6252
6498
  };
6253
6499
  }
6254
6500
  extractHandlerAnalysis() {
@@ -7343,4 +7589,4 @@ main().catch((error) => {
7343
7589
  process.exit(1);
7344
7590
  });
7345
7591
 
7346
- //# debugId=7EE824400DEB8F7564756E2164756E21
7592
+ //# debugId=AA5557533E02826364756E2164756E21