@fairfox/polly 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/verify/src/cli.js +570 -37
- package/dist/tools/verify/src/cli.js.map +8 -7
- package/dist/tools/verify/src/config.d.ts +5 -0
- package/dist/tools/verify/src/config.js.map +2 -2
- package/dist/tools/visualize/src/cli.js +389 -34
- package/dist/tools/visualize/src/cli.js.map +7 -7
- package/package.json +1 -1
|
@@ -13,6 +13,48 @@ var __export = (target, all) => {
|
|
|
13
13
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
14
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
15
15
|
|
|
16
|
+
// tools/verify/src/analysis/non-interference.ts
|
|
17
|
+
var exports_non_interference = {};
|
|
18
|
+
__export(exports_non_interference, {
|
|
19
|
+
checkNonInterference: () => checkNonInterference
|
|
20
|
+
});
|
|
21
|
+
function checkNonInterference(subsystems, handlers) {
|
|
22
|
+
const violations = [];
|
|
23
|
+
const fieldOwner = new Map;
|
|
24
|
+
for (const [name, sub] of Object.entries(subsystems)) {
|
|
25
|
+
for (const field of sub.state) {
|
|
26
|
+
fieldOwner.set(field, name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const handlerSubsystem = new Map;
|
|
30
|
+
for (const [name, sub] of Object.entries(subsystems)) {
|
|
31
|
+
for (const h of sub.handlers) {
|
|
32
|
+
handlerSubsystem.set(h, name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const handler of handlers) {
|
|
36
|
+
const subsystemName = handlerSubsystem.get(handler.messageType);
|
|
37
|
+
if (!subsystemName)
|
|
38
|
+
continue;
|
|
39
|
+
for (const assignment of handler.assignments) {
|
|
40
|
+
const fieldName = assignment.field;
|
|
41
|
+
const owner = fieldOwner.get(fieldName);
|
|
42
|
+
if (owner && owner !== subsystemName) {
|
|
43
|
+
violations.push({
|
|
44
|
+
handler: handler.messageType,
|
|
45
|
+
subsystem: subsystemName,
|
|
46
|
+
writesTo: fieldName,
|
|
47
|
+
ownedBy: owner
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
valid: violations.length === 0,
|
|
54
|
+
violations
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
16
58
|
// tools/verify/src/codegen/invariants.ts
|
|
17
59
|
import { Node as Node3, Project as Project3 } from "ts-morph";
|
|
18
60
|
|
|
@@ -341,6 +383,7 @@ class TemporalTLAGenerator {
|
|
|
341
383
|
var exports_tla = {};
|
|
342
384
|
__export(exports_tla, {
|
|
343
385
|
generateTLA: () => generateTLA,
|
|
386
|
+
generateSubsystemTLA: () => generateSubsystemTLA,
|
|
344
387
|
TLAValidationError: () => TLAValidationError,
|
|
345
388
|
TLAGenerator: () => TLAGenerator
|
|
346
389
|
});
|
|
@@ -351,6 +394,58 @@ async function generateTLA(config, analysis) {
|
|
|
351
394
|
const generator = new TLAGenerator;
|
|
352
395
|
return await generator.generate(config, analysis);
|
|
353
396
|
}
|
|
397
|
+
async function generateSubsystemTLA(_subsystemName, subsystem, config, analysis) {
|
|
398
|
+
const stateFields = new Set(subsystem.state);
|
|
399
|
+
const handlerNames = new Set(subsystem.handlers);
|
|
400
|
+
const filteredState = {};
|
|
401
|
+
for (const [field, fieldConfig] of Object.entries(config.state)) {
|
|
402
|
+
if (stateFields.has(field)) {
|
|
403
|
+
filteredState[field] = fieldConfig;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const filteredMessages = {
|
|
407
|
+
...config.messages,
|
|
408
|
+
include: subsystem.handlers
|
|
409
|
+
};
|
|
410
|
+
filteredMessages.exclude = undefined;
|
|
411
|
+
if (filteredMessages.perMessageBounds) {
|
|
412
|
+
const filteredBounds = {};
|
|
413
|
+
for (const [msg, bound] of Object.entries(filteredMessages.perMessageBounds)) {
|
|
414
|
+
if (handlerNames.has(msg)) {
|
|
415
|
+
filteredBounds[msg] = bound;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
filteredMessages.perMessageBounds = filteredBounds;
|
|
419
|
+
}
|
|
420
|
+
const filteredConfig = {
|
|
421
|
+
...config,
|
|
422
|
+
state: filteredState,
|
|
423
|
+
messages: filteredMessages,
|
|
424
|
+
subsystems: undefined
|
|
425
|
+
};
|
|
426
|
+
if (filteredConfig.tier2?.temporalConstraints) {
|
|
427
|
+
filteredConfig.tier2 = {
|
|
428
|
+
...filteredConfig.tier2,
|
|
429
|
+
temporalConstraints: filteredConfig.tier2.temporalConstraints.filter((tc) => handlerNames.has(tc.before) && handlerNames.has(tc.after))
|
|
430
|
+
};
|
|
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)));
|
|
440
|
+
const filteredAnalysis = {
|
|
441
|
+
...analysis,
|
|
442
|
+
messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
|
|
443
|
+
handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType)),
|
|
444
|
+
globalStateConstraints: filteredGlobalStateConstraints
|
|
445
|
+
};
|
|
446
|
+
const generator = new TLAGenerator;
|
|
447
|
+
return await generator.generate(filteredConfig, filteredAnalysis, `UserApp_${_subsystemName}`);
|
|
448
|
+
}
|
|
354
449
|
var TLAValidationError, TLAGenerator;
|
|
355
450
|
var init_tla = __esm(() => {
|
|
356
451
|
init_invariants();
|
|
@@ -372,6 +467,7 @@ var init_tla = __esm(() => {
|
|
|
372
467
|
resolvedActionNames = new Map;
|
|
373
468
|
tabSymmetryEnabled = false;
|
|
374
469
|
tabCount = 0;
|
|
470
|
+
moduleName = "UserApp";
|
|
375
471
|
constructor(options) {
|
|
376
472
|
this.options = options;
|
|
377
473
|
}
|
|
@@ -381,7 +477,10 @@ var init_tla = __esm(() => {
|
|
|
381
477
|
}
|
|
382
478
|
return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s);
|
|
383
479
|
}
|
|
384
|
-
async generate(config, analysis) {
|
|
480
|
+
async generate(config, analysis, moduleName) {
|
|
481
|
+
if (moduleName) {
|
|
482
|
+
this.moduleName = moduleName;
|
|
483
|
+
}
|
|
385
484
|
this.validateInputs(config, analysis);
|
|
386
485
|
this.extractInvariantsIfEnabled();
|
|
387
486
|
this.generateTemporalPropertiesIfEnabled(analysis);
|
|
@@ -671,7 +770,7 @@ var init_tla = __esm(() => {
|
|
|
671
770
|
}
|
|
672
771
|
}
|
|
673
772
|
addHeader() {
|
|
674
|
-
this.line(
|
|
773
|
+
this.line(`------------------------- MODULE ${this.moduleName} -------------------------`);
|
|
675
774
|
this.line("(*");
|
|
676
775
|
this.line(" Auto-generated TLA+ specification for web extension");
|
|
677
776
|
this.line(" ");
|
|
@@ -1276,19 +1375,61 @@ var init_tla = __esm(() => {
|
|
|
1276
1375
|
return assignment;
|
|
1277
1376
|
}
|
|
1278
1377
|
emitStateUpdates(validAssignments, preconditions) {
|
|
1279
|
-
if (validAssignments.length
|
|
1280
|
-
|
|
1378
|
+
if (validAssignments.length === 0) {
|
|
1379
|
+
if (preconditions.length === 0) {
|
|
1380
|
+
this.line("\\* No state changes in handler");
|
|
1381
|
+
}
|
|
1382
|
+
this.line("/\\ UNCHANGED contextStates");
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
const ndetAssignments = [];
|
|
1386
|
+
const detAssignments = [];
|
|
1387
|
+
for (const a of validAssignments) {
|
|
1388
|
+
if (typeof a.value === "string" && a.value.startsWith("NDET:")) {
|
|
1389
|
+
ndetAssignments.push({ field: a.field, value: a.value });
|
|
1390
|
+
} else {
|
|
1391
|
+
detAssignments.push(a);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (ndetAssignments.length === 0) {
|
|
1395
|
+
const exceptClauses = detAssignments.map((a) => {
|
|
1281
1396
|
const tlaValue = this.assignmentValueToTLA(a.value);
|
|
1282
1397
|
return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
|
|
1283
1398
|
});
|
|
1284
1399
|
this.line(`/\\ contextStates' = [contextStates EXCEPT ${exceptClauses.join(", ")}]`);
|
|
1285
1400
|
return false;
|
|
1286
1401
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1402
|
+
this.emitNDETStateUpdates(ndetAssignments, detAssignments);
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
emitNDETStateUpdates(ndetAssignments, detAssignments) {
|
|
1406
|
+
const detExceptClauses = detAssignments.map((a) => {
|
|
1407
|
+
const tlaValue = this.assignmentValueToTLA(a.value);
|
|
1408
|
+
return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
|
|
1409
|
+
});
|
|
1410
|
+
const ndetExceptClauses = [];
|
|
1411
|
+
const quantifierOpeners = [];
|
|
1412
|
+
for (const a of ndetAssignments) {
|
|
1413
|
+
const fieldName = this.sanitizeFieldName(a.field);
|
|
1414
|
+
const fieldRef = `contextStates[ctx].${fieldName}`;
|
|
1415
|
+
if (a.value === "NDET:FILTER") {
|
|
1416
|
+
quantifierOpeners.push(`/\\ \\E newLen_${fieldName} \\in 0..Len(${fieldRef}) :`);
|
|
1417
|
+
ndetExceptClauses.push(`![ctx].${fieldName} = SubSeq(@, 1, newLen_${fieldName})`);
|
|
1418
|
+
} else if (a.value === "NDET:MAP") {
|
|
1419
|
+
quantifierOpeners.push(`/\\ \\E mapIdx_${fieldName} \\in 1..Len(${fieldRef}) :`);
|
|
1420
|
+
quantifierOpeners.push(`\\E mapVal_${fieldName} \\in Value :`);
|
|
1421
|
+
ndetExceptClauses.push(`![ctx].${fieldName} = [@ EXCEPT ![mapIdx_${fieldName}] = mapVal_${fieldName}]`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
for (const opener of quantifierOpeners) {
|
|
1425
|
+
this.line(opener);
|
|
1426
|
+
this.indent++;
|
|
1427
|
+
}
|
|
1428
|
+
const allExceptClauses = [...detExceptClauses, ...ndetExceptClauses];
|
|
1429
|
+
this.line(`/\\ contextStates' = [contextStates EXCEPT ${allExceptClauses.join(", ")}]`);
|
|
1430
|
+
for (const _opener of quantifierOpeners) {
|
|
1431
|
+
this.indent--;
|
|
1289
1432
|
}
|
|
1290
|
-
this.line("/\\ UNCHANGED contextStates");
|
|
1291
|
-
return true;
|
|
1292
1433
|
}
|
|
1293
1434
|
tsExpressionToTLA(expr, isPrimed = false) {
|
|
1294
1435
|
if (!expr || typeof expr !== "string") {
|
|
@@ -1701,6 +1842,9 @@ var init_tla = __esm(() => {
|
|
|
1701
1842
|
return "NULL";
|
|
1702
1843
|
}
|
|
1703
1844
|
if (typeof value === "string") {
|
|
1845
|
+
if (value.startsWith("NDET:")) {
|
|
1846
|
+
throw new Error(`NDET marker "${value}" reached assignmentValueToTLA — should have been partitioned in emitStateUpdates`);
|
|
1847
|
+
}
|
|
1704
1848
|
if (value.startsWith("param:")) {
|
|
1705
1849
|
const paramName = value.substring(6);
|
|
1706
1850
|
return `payload.${this.sanitizeFieldName(paramName)}`;
|
|
@@ -3335,6 +3479,9 @@ class ConfigValidator {
|
|
|
3335
3479
|
if (config.tier2) {
|
|
3336
3480
|
this.validateTier2Optimizations(config.tier2);
|
|
3337
3481
|
}
|
|
3482
|
+
if (config.subsystems) {
|
|
3483
|
+
this.validateSubsystems(config.subsystems, config.state);
|
|
3484
|
+
}
|
|
3338
3485
|
}
|
|
3339
3486
|
findNullPlaceholders(obj, path2) {
|
|
3340
3487
|
if (obj === null || obj === undefined) {
|
|
@@ -3651,6 +3798,68 @@ class ConfigValidator {
|
|
|
3651
3798
|
}
|
|
3652
3799
|
}
|
|
3653
3800
|
}
|
|
3801
|
+
validateSubsystems(subsystems, stateConfig) {
|
|
3802
|
+
const stateFieldNames = Object.keys(stateConfig);
|
|
3803
|
+
const allAssignedHandlers = new Map;
|
|
3804
|
+
const allAssignedFields = new Map;
|
|
3805
|
+
for (const [subsystemName, subsystem] of Object.entries(subsystems)) {
|
|
3806
|
+
for (const field of subsystem.state) {
|
|
3807
|
+
if (!stateFieldNames.includes(field)) {
|
|
3808
|
+
this.issues.push({
|
|
3809
|
+
type: "invalid_value",
|
|
3810
|
+
severity: "error",
|
|
3811
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3812
|
+
message: `State field "${field}" does not exist in top-level state config`,
|
|
3813
|
+
suggestion: `Available fields: ${stateFieldNames.join(", ")}`
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
const existingOwner = allAssignedFields.get(field);
|
|
3817
|
+
if (existingOwner) {
|
|
3818
|
+
this.issues.push({
|
|
3819
|
+
type: "invalid_value",
|
|
3820
|
+
severity: "warning",
|
|
3821
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3822
|
+
message: `State field "${field}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
|
|
3823
|
+
suggestion: "State fields should be partitioned across subsystems for non-interference"
|
|
3824
|
+
});
|
|
3825
|
+
} else {
|
|
3826
|
+
allAssignedFields.set(field, subsystemName);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
if (subsystem.handlers.length === 0) {
|
|
3830
|
+
this.issues.push({
|
|
3831
|
+
type: "invalid_value",
|
|
3832
|
+
severity: "warning",
|
|
3833
|
+
field: `subsystems.${subsystemName}.handlers`,
|
|
3834
|
+
message: `Subsystem "${subsystemName}" has no handlers`,
|
|
3835
|
+
suggestion: "Add at least one handler to the subsystem"
|
|
3836
|
+
});
|
|
3837
|
+
}
|
|
3838
|
+
for (const handler of subsystem.handlers) {
|
|
3839
|
+
const existingOwner = allAssignedHandlers.get(handler);
|
|
3840
|
+
if (existingOwner) {
|
|
3841
|
+
this.issues.push({
|
|
3842
|
+
type: "invalid_value",
|
|
3843
|
+
severity: "error",
|
|
3844
|
+
field: `subsystems.${subsystemName}.handlers`,
|
|
3845
|
+
message: `Handler "${handler}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
|
|
3846
|
+
suggestion: "Each handler must belong to exactly one subsystem"
|
|
3847
|
+
});
|
|
3848
|
+
} else {
|
|
3849
|
+
allAssignedHandlers.set(handler, subsystemName);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
if (subsystem.state.length === 0) {
|
|
3853
|
+
this.issues.push({
|
|
3854
|
+
type: "invalid_value",
|
|
3855
|
+
severity: "warning",
|
|
3856
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3857
|
+
message: `Subsystem "${subsystemName}" has no state fields`,
|
|
3858
|
+
suggestion: "Add at least one state field to the subsystem"
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3654
3863
|
}
|
|
3655
3864
|
function validateConfig(configPath) {
|
|
3656
3865
|
const validator = new ConfigValidator;
|
|
@@ -4069,7 +4278,8 @@ class HandlerExtractor {
|
|
|
4069
4278
|
packageRoot;
|
|
4070
4279
|
warnings;
|
|
4071
4280
|
currentFunctionParams = [];
|
|
4072
|
-
|
|
4281
|
+
contextOverrides;
|
|
4282
|
+
constructor(tsConfigPath, contextOverrides) {
|
|
4073
4283
|
this.project = new Project({
|
|
4074
4284
|
tsConfigFilePath: tsConfigPath
|
|
4075
4285
|
});
|
|
@@ -4077,6 +4287,7 @@ class HandlerExtractor {
|
|
|
4077
4287
|
this.relationshipExtractor = new RelationshipExtractor;
|
|
4078
4288
|
this.analyzedFiles = new Set;
|
|
4079
4289
|
this.warnings = [];
|
|
4290
|
+
this.contextOverrides = contextOverrides || new Map;
|
|
4080
4291
|
this.packageRoot = this.findPackageRoot(tsConfigPath);
|
|
4081
4292
|
}
|
|
4082
4293
|
warnUnsupportedPattern(pattern, location, suggestion) {
|
|
@@ -4294,8 +4505,20 @@ class HandlerExtractor {
|
|
|
4294
4505
|
handlers.push(handler);
|
|
4295
4506
|
}
|
|
4296
4507
|
}
|
|
4508
|
+
if (methodName === "ws") {
|
|
4509
|
+
this.extractElysiaWsHandlers(node, context, filePath, handlers);
|
|
4510
|
+
}
|
|
4511
|
+
if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
|
|
4512
|
+
const restHandler = this.extractRestHandler(node, methodName, context, filePath);
|
|
4513
|
+
if (restHandler) {
|
|
4514
|
+
handlers.push(restHandler);
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4297
4517
|
}
|
|
4298
4518
|
}
|
|
4519
|
+
isRestMethod(name) {
|
|
4520
|
+
return ["get", "post", "put", "delete", "patch"].includes(name);
|
|
4521
|
+
}
|
|
4299
4522
|
isElseIfStatement(node) {
|
|
4300
4523
|
const parent = node.getParent();
|
|
4301
4524
|
return parent !== undefined && Node2.isIfStatement(parent);
|
|
@@ -4361,6 +4584,135 @@ class HandlerExtractor {
|
|
|
4361
4584
|
parameters
|
|
4362
4585
|
};
|
|
4363
4586
|
}
|
|
4587
|
+
extractElysiaWsHandlers(node, context, filePath, handlers) {
|
|
4588
|
+
const args = node.getArguments();
|
|
4589
|
+
if (args.length < 2)
|
|
4590
|
+
return;
|
|
4591
|
+
const routeArg = args[0];
|
|
4592
|
+
if (!routeArg || !Node2.isStringLiteral(routeArg))
|
|
4593
|
+
return;
|
|
4594
|
+
const routePath = routeArg.getLiteralValue();
|
|
4595
|
+
const configArg = args[1];
|
|
4596
|
+
if (!configArg || !Node2.isObjectLiteralExpression(configArg))
|
|
4597
|
+
return;
|
|
4598
|
+
const callbacks = ["message", "open", "close"];
|
|
4599
|
+
for (const cbName of callbacks) {
|
|
4600
|
+
const prop = configArg.getProperty(cbName);
|
|
4601
|
+
if (!prop)
|
|
4602
|
+
continue;
|
|
4603
|
+
let funcBody = null;
|
|
4604
|
+
if (Node2.isMethodDeclaration(prop)) {
|
|
4605
|
+
funcBody = prop;
|
|
4606
|
+
} else if (Node2.isPropertyAssignment(prop)) {
|
|
4607
|
+
const init = prop.getInitializer();
|
|
4608
|
+
if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
|
|
4609
|
+
funcBody = init;
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
if (!funcBody)
|
|
4613
|
+
continue;
|
|
4614
|
+
if (cbName === "message") {
|
|
4615
|
+
const body = funcBody.getBody();
|
|
4616
|
+
if (!body)
|
|
4617
|
+
continue;
|
|
4618
|
+
const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
|
|
4619
|
+
if (subHandlers.length > 0) {
|
|
4620
|
+
handlers.push(...subHandlers);
|
|
4621
|
+
} else {
|
|
4622
|
+
handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
|
|
4623
|
+
}
|
|
4624
|
+
} else {
|
|
4625
|
+
handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
extractSubHandlersFromBody(body, context, filePath) {
|
|
4630
|
+
const subHandlers = [];
|
|
4631
|
+
body.forEachDescendant((child) => {
|
|
4632
|
+
if (Node2.isIfStatement(child) && !this.isElseIfStatement(child)) {
|
|
4633
|
+
const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
|
|
4634
|
+
subHandlers.push(...typeGuardHandlers);
|
|
4635
|
+
}
|
|
4636
|
+
if (Node2.isSwitchStatement(child)) {
|
|
4637
|
+
const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
|
|
4638
|
+
subHandlers.push(...switchHandlers);
|
|
4639
|
+
}
|
|
4640
|
+
});
|
|
4641
|
+
return subHandlers;
|
|
4642
|
+
}
|
|
4643
|
+
buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
|
|
4644
|
+
const assignments = [];
|
|
4645
|
+
const preconditions = [];
|
|
4646
|
+
const postconditions = [];
|
|
4647
|
+
this.currentFunctionParams = this.extractParameterNames(funcBody);
|
|
4648
|
+
this.extractAssignments(funcBody, assignments);
|
|
4649
|
+
this.extractVerificationConditions(funcBody, preconditions, postconditions);
|
|
4650
|
+
this.currentFunctionParams = [];
|
|
4651
|
+
return {
|
|
4652
|
+
messageType,
|
|
4653
|
+
node: context,
|
|
4654
|
+
assignments,
|
|
4655
|
+
preconditions,
|
|
4656
|
+
postconditions,
|
|
4657
|
+
location: { file: filePath, line },
|
|
4658
|
+
origin: "event"
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
extractRestHandler(node, methodName, context, filePath) {
|
|
4662
|
+
const args = node.getArguments();
|
|
4663
|
+
if (args.length < 2)
|
|
4664
|
+
return null;
|
|
4665
|
+
const routeArg = args[0];
|
|
4666
|
+
if (!routeArg || !Node2.isStringLiteral(routeArg))
|
|
4667
|
+
return null;
|
|
4668
|
+
const routePath = routeArg.getLiteralValue();
|
|
4669
|
+
const httpMethod = methodName.toUpperCase();
|
|
4670
|
+
const messageType = `${httpMethod} ${routePath}`;
|
|
4671
|
+
const handlerArg = args[1];
|
|
4672
|
+
const assignments = [];
|
|
4673
|
+
const preconditions = [];
|
|
4674
|
+
const postconditions = [];
|
|
4675
|
+
let actualHandler = null;
|
|
4676
|
+
if (Node2.isArrowFunction(handlerArg) || Node2.isFunctionExpression(handlerArg)) {
|
|
4677
|
+
actualHandler = handlerArg;
|
|
4678
|
+
} else if (Node2.isIdentifier(handlerArg)) {
|
|
4679
|
+
actualHandler = this.resolveFunctionReference(handlerArg);
|
|
4680
|
+
}
|
|
4681
|
+
let parameters;
|
|
4682
|
+
if (actualHandler) {
|
|
4683
|
+
this.currentFunctionParams = this.extractParameterNames(actualHandler);
|
|
4684
|
+
parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
|
|
4685
|
+
this.extractAssignments(actualHandler, assignments);
|
|
4686
|
+
this.extractVerificationConditions(actualHandler, preconditions, postconditions);
|
|
4687
|
+
this.currentFunctionParams = [];
|
|
4688
|
+
}
|
|
4689
|
+
return {
|
|
4690
|
+
messageType,
|
|
4691
|
+
node: context,
|
|
4692
|
+
assignments,
|
|
4693
|
+
preconditions,
|
|
4694
|
+
postconditions,
|
|
4695
|
+
location: {
|
|
4696
|
+
file: filePath,
|
|
4697
|
+
line: node.getStartLineNumber()
|
|
4698
|
+
},
|
|
4699
|
+
origin: "event",
|
|
4700
|
+
parameters,
|
|
4701
|
+
handlerKind: "rest",
|
|
4702
|
+
httpMethod,
|
|
4703
|
+
routePath
|
|
4704
|
+
};
|
|
4705
|
+
}
|
|
4706
|
+
isWebFrameworkFile(sourceFile) {
|
|
4707
|
+
const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
|
|
4708
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
4709
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
4710
|
+
if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
|
|
4711
|
+
return true;
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
return false;
|
|
4715
|
+
}
|
|
4364
4716
|
extractAssignments(funcNode, assignments) {
|
|
4365
4717
|
funcNode.forEachDescendant((node) => {
|
|
4366
4718
|
if (Node2.isBinaryExpression(node)) {
|
|
@@ -4411,7 +4763,9 @@ class HandlerExtractor {
|
|
|
4411
4763
|
return;
|
|
4412
4764
|
if (this.tryExtractSetConstructorPattern(fieldPath, right, assignments))
|
|
4413
4765
|
return;
|
|
4414
|
-
this.tryExtractMapConstructorPattern(fieldPath, right, assignments)
|
|
4766
|
+
if (this.tryExtractMapConstructorPattern(fieldPath, right, assignments))
|
|
4767
|
+
return;
|
|
4768
|
+
this.tryExtractSignalDirectValuePattern(fieldPath, right, assignments);
|
|
4415
4769
|
}
|
|
4416
4770
|
tryExtractStateFieldPattern(fieldPath, right, assignments) {
|
|
4417
4771
|
if (!fieldPath.startsWith("state."))
|
|
@@ -4530,6 +4884,25 @@ class HandlerExtractor {
|
|
|
4530
4884
|
}
|
|
4531
4885
|
return true;
|
|
4532
4886
|
}
|
|
4887
|
+
tryExtractSignalDirectValuePattern(fieldPath, right, assignments) {
|
|
4888
|
+
if (!fieldPath.endsWith(".value"))
|
|
4889
|
+
return false;
|
|
4890
|
+
const signalName = fieldPath.slice(0, -6);
|
|
4891
|
+
const literalValue = this.extractValue(right);
|
|
4892
|
+
if (literalValue !== undefined) {
|
|
4893
|
+
assignments.push({ field: signalName, value: literalValue });
|
|
4894
|
+
return true;
|
|
4895
|
+
}
|
|
4896
|
+
if (Node2.isPropertyAccessExpression(right)) {
|
|
4897
|
+
const rightPath = this.getPropertyPath(right);
|
|
4898
|
+
const parts = rightPath.split(".");
|
|
4899
|
+
if (parts.length === 2 && parts[0] !== undefined && parts[1] !== undefined && this.currentFunctionParams.includes(parts[0])) {
|
|
4900
|
+
assignments.push({ field: signalName, value: `param:${parts[1]}` });
|
|
4901
|
+
return true;
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
return false;
|
|
4905
|
+
}
|
|
4533
4906
|
extractSetOperation(newExpr, fieldPath, signalName) {
|
|
4534
4907
|
const args = newExpr.getArguments();
|
|
4535
4908
|
if (args.length === 0) {
|
|
@@ -4646,11 +5019,11 @@ class HandlerExtractor {
|
|
|
4646
5019
|
return null;
|
|
4647
5020
|
switch (methodName) {
|
|
4648
5021
|
case "filter":
|
|
4649
|
-
return { field: signalName, value: "
|
|
5022
|
+
return { field: signalName, value: "NDET:FILTER" };
|
|
4650
5023
|
case "map":
|
|
4651
|
-
return { field: signalName, value: "
|
|
5024
|
+
return { field: signalName, value: "NDET:MAP" };
|
|
4652
5025
|
case "slice":
|
|
4653
|
-
return { field: signalName, value: "
|
|
5026
|
+
return { field: signalName, value: "NDET:FILTER" };
|
|
4654
5027
|
case "concat":
|
|
4655
5028
|
return { field: signalName, value: "@ \\o <<payload>>" };
|
|
4656
5029
|
case "reverse":
|
|
@@ -5195,32 +5568,64 @@ class HandlerExtractor {
|
|
|
5195
5568
|
try {
|
|
5196
5569
|
const ifStmt = ifNode;
|
|
5197
5570
|
const condition = ifStmt.getExpression();
|
|
5198
|
-
if (
|
|
5199
|
-
|
|
5571
|
+
if (Node2.isCallExpression(condition)) {
|
|
5572
|
+
const funcExpr = condition.getExpression();
|
|
5573
|
+
const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
|
|
5574
|
+
this.debugLogProcessingFunction(funcName);
|
|
5575
|
+
const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
|
|
5576
|
+
if (!messageType) {
|
|
5577
|
+
this.debugLogUnresolvedMessageType(funcName);
|
|
5578
|
+
return null;
|
|
5579
|
+
}
|
|
5580
|
+
const line = ifStmt.getStartLineNumber();
|
|
5581
|
+
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5582
|
+
return {
|
|
5583
|
+
messageType,
|
|
5584
|
+
node: context,
|
|
5585
|
+
assignments: [],
|
|
5586
|
+
preconditions: [],
|
|
5587
|
+
postconditions: [],
|
|
5588
|
+
location: { file: filePath, line },
|
|
5589
|
+
relationships
|
|
5590
|
+
};
|
|
5200
5591
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5592
|
+
if (Node2.isBinaryExpression(condition)) {
|
|
5593
|
+
const messageType = this.extractMessageTypeFromEqualityCheck(condition);
|
|
5594
|
+
if (messageType) {
|
|
5595
|
+
const line = ifStmt.getStartLineNumber();
|
|
5596
|
+
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5597
|
+
return {
|
|
5598
|
+
messageType,
|
|
5599
|
+
node: context,
|
|
5600
|
+
assignments: [],
|
|
5601
|
+
preconditions: [],
|
|
5602
|
+
postconditions: [],
|
|
5603
|
+
location: { file: filePath, line },
|
|
5604
|
+
relationships
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5208
5607
|
}
|
|
5209
|
-
|
|
5210
|
-
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5211
|
-
return {
|
|
5212
|
-
messageType,
|
|
5213
|
-
node: context,
|
|
5214
|
-
assignments: [],
|
|
5215
|
-
preconditions: [],
|
|
5216
|
-
postconditions: [],
|
|
5217
|
-
location: { file: filePath, line },
|
|
5218
|
-
relationships
|
|
5219
|
-
};
|
|
5608
|
+
return null;
|
|
5220
5609
|
} catch (_error) {
|
|
5221
5610
|
return null;
|
|
5222
5611
|
}
|
|
5223
5612
|
}
|
|
5613
|
+
extractMessageTypeFromEqualityCheck(expr) {
|
|
5614
|
+
const operator = expr.getOperatorToken().getText();
|
|
5615
|
+
if (operator !== "===" && operator !== "==")
|
|
5616
|
+
return null;
|
|
5617
|
+
const left = expr.getLeft();
|
|
5618
|
+
const right = expr.getRight();
|
|
5619
|
+
const stringLiteral = Node2.isStringLiteral(right) ? right : Node2.isStringLiteral(left) ? left : null;
|
|
5620
|
+
const propAccess = Node2.isPropertyAccessExpression(left) ? left : Node2.isPropertyAccessExpression(right) ? right : null;
|
|
5621
|
+
if (!stringLiteral || !propAccess)
|
|
5622
|
+
return null;
|
|
5623
|
+
const propName = propAccess.getName();
|
|
5624
|
+
if (propName !== "type" && propName !== "kind" && propName !== "action") {
|
|
5625
|
+
return null;
|
|
5626
|
+
}
|
|
5627
|
+
return stringLiteral.getLiteralValue();
|
|
5628
|
+
}
|
|
5224
5629
|
debugLogProcessingFunction(funcName) {
|
|
5225
5630
|
if (process.env["POLLY_DEBUG"] && funcName) {
|
|
5226
5631
|
console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
|
|
@@ -5430,6 +5835,10 @@ class HandlerExtractor {
|
|
|
5430
5835
|
return messageType;
|
|
5431
5836
|
}
|
|
5432
5837
|
inferContext(filePath) {
|
|
5838
|
+
for (const [pathPrefix, context] of this.contextOverrides.entries()) {
|
|
5839
|
+
if (filePath.startsWith(pathPrefix))
|
|
5840
|
+
return context;
|
|
5841
|
+
}
|
|
5433
5842
|
const path2 = filePath.toLowerCase();
|
|
5434
5843
|
return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
|
|
5435
5844
|
}
|
|
@@ -6645,15 +7054,23 @@ async function runFullVerification(configPath) {
|
|
|
6645
7054
|
if (exprValidation.warnings.length > 0) {
|
|
6646
7055
|
displayExpressionWarnings(exprValidation);
|
|
6647
7056
|
}
|
|
7057
|
+
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
7058
|
+
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
7059
|
+
return;
|
|
7060
|
+
}
|
|
7061
|
+
await runMonolithicVerification(config, analysis);
|
|
7062
|
+
}
|
|
7063
|
+
async function runMonolithicVerification(config, analysis) {
|
|
6648
7064
|
const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
|
|
6649
7065
|
findAndCopyBaseSpec(specDir);
|
|
6650
7066
|
console.log(color("✓ Specification generated", COLORS.green));
|
|
6651
7067
|
console.log(color(` ${specPath}`, COLORS.gray));
|
|
6652
7068
|
console.log();
|
|
6653
7069
|
const docker = await setupDocker();
|
|
6654
|
-
const
|
|
6655
|
-
const
|
|
6656
|
-
const
|
|
7070
|
+
const typedConfig = config;
|
|
7071
|
+
const timeoutSeconds = getTimeout(typedConfig);
|
|
7072
|
+
const workers = getWorkers(typedConfig);
|
|
7073
|
+
const maxDepth = getMaxDepth(typedConfig);
|
|
6657
7074
|
console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
|
|
6658
7075
|
if (timeoutSeconds === 0) {
|
|
6659
7076
|
console.log(color(" No timeout set - will run until completion", COLORS.gray));
|
|
@@ -6673,6 +7090,122 @@ async function runFullVerification(configPath) {
|
|
|
6673
7090
|
});
|
|
6674
7091
|
displayVerificationResults(result, specDir);
|
|
6675
7092
|
}
|
|
7093
|
+
async function runSubsystemVerification(config, analysis) {
|
|
7094
|
+
const subsystems = config.subsystems;
|
|
7095
|
+
const subsystemNames = Object.keys(subsystems);
|
|
7096
|
+
console.log(color(`\uD83D\uDCE6 Subsystem-scoped verification (${subsystemNames.length} subsystems)
|
|
7097
|
+
`, COLORS.blue));
|
|
7098
|
+
const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
|
|
7099
|
+
const interference = checkNonInterference2(subsystems, analysis.handlers);
|
|
7100
|
+
if (interference.valid) {
|
|
7101
|
+
console.log(color("✓ Non-interference: verified (no cross-subsystem state writes)", COLORS.green));
|
|
7102
|
+
console.log();
|
|
7103
|
+
} else {
|
|
7104
|
+
console.log(color(`⚠️ Non-interference violations detected:
|
|
7105
|
+
`, COLORS.yellow));
|
|
7106
|
+
for (const v of interference.violations) {
|
|
7107
|
+
console.log(color(` • Handler "${v.handler}" (${v.subsystem}) writes to "${v.writesTo}" owned by "${v.ownedBy}"`, COLORS.yellow));
|
|
7108
|
+
}
|
|
7109
|
+
console.log();
|
|
7110
|
+
console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
|
|
7111
|
+
console.log();
|
|
7112
|
+
}
|
|
7113
|
+
const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
|
|
7114
|
+
const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
|
|
7115
|
+
if (unassigned.length > 0) {
|
|
7116
|
+
console.log(color(`⚠️ ${unassigned.length} handler(s) not assigned to any subsystem (will not be verified):`, COLORS.yellow));
|
|
7117
|
+
for (const h of unassigned.slice(0, 10)) {
|
|
7118
|
+
console.log(color(` • ${h}`, COLORS.yellow));
|
|
7119
|
+
}
|
|
7120
|
+
if (unassigned.length > 10) {
|
|
7121
|
+
console.log(color(` ... and ${unassigned.length - 10} more`, COLORS.yellow));
|
|
7122
|
+
}
|
|
7123
|
+
console.log();
|
|
7124
|
+
}
|
|
7125
|
+
const docker = await setupDocker();
|
|
7126
|
+
const timeoutSeconds = getTimeout(config);
|
|
7127
|
+
const workers = getWorkers(config);
|
|
7128
|
+
const maxDepth = getMaxDepth(config);
|
|
7129
|
+
const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
7130
|
+
const results = [];
|
|
7131
|
+
for (const name of subsystemNames) {
|
|
7132
|
+
const sub = subsystems[name];
|
|
7133
|
+
const startTime = Date.now();
|
|
7134
|
+
console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
|
|
7135
|
+
const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
|
|
7136
|
+
const specDir = path4.join(process.cwd(), "specs", "tla", "generated", name);
|
|
7137
|
+
if (!fs4.existsSync(specDir)) {
|
|
7138
|
+
fs4.mkdirSync(specDir, { recursive: true });
|
|
7139
|
+
}
|
|
7140
|
+
const specPath = path4.join(specDir, `UserApp_${name}.tla`);
|
|
7141
|
+
const cfgPath = path4.join(specDir, `UserApp_${name}.cfg`);
|
|
7142
|
+
fs4.writeFileSync(specPath, spec);
|
|
7143
|
+
fs4.writeFileSync(cfgPath, cfg);
|
|
7144
|
+
findAndCopyBaseSpec(specDir);
|
|
7145
|
+
const result = await docker.runTLC(specPath, {
|
|
7146
|
+
workers,
|
|
7147
|
+
timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
|
|
7148
|
+
maxDepth
|
|
7149
|
+
});
|
|
7150
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
7151
|
+
results.push({
|
|
7152
|
+
name,
|
|
7153
|
+
success: result.success,
|
|
7154
|
+
handlerCount: sub.handlers.length,
|
|
7155
|
+
stateCount: result.stats?.distinctStates ?? 0,
|
|
7156
|
+
elapsed,
|
|
7157
|
+
stats: result.stats,
|
|
7158
|
+
error: result.error
|
|
7159
|
+
});
|
|
7160
|
+
if (result.success) {
|
|
7161
|
+
console.log(color(` ✓ ${name} passed (${elapsed.toFixed(1)}s)`, COLORS.green));
|
|
7162
|
+
} else {
|
|
7163
|
+
console.log(color(` ✗ ${name} failed`, COLORS.red));
|
|
7164
|
+
if (result.violation) {
|
|
7165
|
+
console.log(color(` Invariant violated: ${result.violation.name}`, COLORS.red));
|
|
7166
|
+
} else if (result.error) {
|
|
7167
|
+
console.log(color(` Error: ${result.error}`, COLORS.red));
|
|
7168
|
+
}
|
|
7169
|
+
fs4.writeFileSync(path4.join(specDir, "tlc-output.log"), result.output);
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
console.log();
|
|
7173
|
+
displayCompositionalReport(results, interference.valid);
|
|
7174
|
+
}
|
|
7175
|
+
function displayCompositionalReport(results, nonInterferenceValid) {
|
|
7176
|
+
console.log(color(`Subsystem verification results:
|
|
7177
|
+
`, COLORS.blue));
|
|
7178
|
+
for (const r of results) {
|
|
7179
|
+
const status = r.success ? color("✓", COLORS.green) : color("✗", COLORS.red);
|
|
7180
|
+
const name = r.name.padEnd(20);
|
|
7181
|
+
const handlers = `${r.handlerCount} handler${r.handlerCount !== 1 ? "s" : ""}`;
|
|
7182
|
+
const states = `${r.stateCount} states`;
|
|
7183
|
+
const time = `${r.elapsed.toFixed(1)}s`;
|
|
7184
|
+
console.log(` ${status} ${name} ${handlers.padEnd(14)} ${states.padEnd(14)} ${time}`);
|
|
7185
|
+
}
|
|
7186
|
+
console.log();
|
|
7187
|
+
const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
|
|
7188
|
+
console.log(` Non-interference: ${nonIntLabel}`);
|
|
7189
|
+
console.log();
|
|
7190
|
+
const allPassed = results.every((r) => r.success);
|
|
7191
|
+
if (allPassed && nonInterferenceValid) {
|
|
7192
|
+
console.log(color("Compositional result: ✓ PASS", COLORS.green));
|
|
7193
|
+
console.log(color(" All subsystems verified independently. By non-interference,", COLORS.gray));
|
|
7194
|
+
console.log(color(" the full system satisfies all per-subsystem invariants.", COLORS.gray));
|
|
7195
|
+
} else if (allPassed && !nonInterferenceValid) {
|
|
7196
|
+
console.log(color("Compositional result: ⚠ PASS (with warnings)", COLORS.yellow));
|
|
7197
|
+
console.log(color(" All subsystems passed, but non-interference violations exist.", COLORS.gray));
|
|
7198
|
+
console.log(color(" Compositional soundness is not guaranteed.", COLORS.gray));
|
|
7199
|
+
} else {
|
|
7200
|
+
console.log(color("Compositional result: ✗ FAIL", COLORS.red));
|
|
7201
|
+
const failed = results.filter((r) => !r.success);
|
|
7202
|
+
for (const f of failed) {
|
|
7203
|
+
console.log(color(` Failed: ${f.name}`, COLORS.red));
|
|
7204
|
+
}
|
|
7205
|
+
process.exit(1);
|
|
7206
|
+
}
|
|
7207
|
+
console.log();
|
|
7208
|
+
}
|
|
6676
7209
|
async function loadVerificationConfig(configPath) {
|
|
6677
7210
|
const resolvedPath = path4.resolve(configPath);
|
|
6678
7211
|
const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
|
|
@@ -6889,4 +7422,4 @@ main().catch((error) => {
|
|
|
6889
7422
|
process.exit(1);
|
|
6890
7423
|
});
|
|
6891
7424
|
|
|
6892
|
-
//# debugId=
|
|
7425
|
+
//# debugId=E5DA6950FD1E0C7764756E2164756E21
|