@fairfox/polly 0.17.0 → 0.18.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 +479 -25
- 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 +364 -30
- 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,49 @@ 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 filteredAnalysis = {
|
|
433
|
+
...analysis,
|
|
434
|
+
messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
|
|
435
|
+
handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType))
|
|
436
|
+
};
|
|
437
|
+
const generator = new TLAGenerator;
|
|
438
|
+
return await generator.generate(filteredConfig, filteredAnalysis);
|
|
439
|
+
}
|
|
354
440
|
var TLAValidationError, TLAGenerator;
|
|
355
441
|
var init_tla = __esm(() => {
|
|
356
442
|
init_invariants();
|
|
@@ -3335,6 +3421,9 @@ class ConfigValidator {
|
|
|
3335
3421
|
if (config.tier2) {
|
|
3336
3422
|
this.validateTier2Optimizations(config.tier2);
|
|
3337
3423
|
}
|
|
3424
|
+
if (config.subsystems) {
|
|
3425
|
+
this.validateSubsystems(config.subsystems, config.state);
|
|
3426
|
+
}
|
|
3338
3427
|
}
|
|
3339
3428
|
findNullPlaceholders(obj, path2) {
|
|
3340
3429
|
if (obj === null || obj === undefined) {
|
|
@@ -3651,6 +3740,68 @@ class ConfigValidator {
|
|
|
3651
3740
|
}
|
|
3652
3741
|
}
|
|
3653
3742
|
}
|
|
3743
|
+
validateSubsystems(subsystems, stateConfig) {
|
|
3744
|
+
const stateFieldNames = Object.keys(stateConfig);
|
|
3745
|
+
const allAssignedHandlers = new Map;
|
|
3746
|
+
const allAssignedFields = new Map;
|
|
3747
|
+
for (const [subsystemName, subsystem] of Object.entries(subsystems)) {
|
|
3748
|
+
for (const field of subsystem.state) {
|
|
3749
|
+
if (!stateFieldNames.includes(field)) {
|
|
3750
|
+
this.issues.push({
|
|
3751
|
+
type: "invalid_value",
|
|
3752
|
+
severity: "error",
|
|
3753
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3754
|
+
message: `State field "${field}" does not exist in top-level state config`,
|
|
3755
|
+
suggestion: `Available fields: ${stateFieldNames.join(", ")}`
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
3758
|
+
const existingOwner = allAssignedFields.get(field);
|
|
3759
|
+
if (existingOwner) {
|
|
3760
|
+
this.issues.push({
|
|
3761
|
+
type: "invalid_value",
|
|
3762
|
+
severity: "warning",
|
|
3763
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3764
|
+
message: `State field "${field}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
|
|
3765
|
+
suggestion: "State fields should be partitioned across subsystems for non-interference"
|
|
3766
|
+
});
|
|
3767
|
+
} else {
|
|
3768
|
+
allAssignedFields.set(field, subsystemName);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
if (subsystem.handlers.length === 0) {
|
|
3772
|
+
this.issues.push({
|
|
3773
|
+
type: "invalid_value",
|
|
3774
|
+
severity: "warning",
|
|
3775
|
+
field: `subsystems.${subsystemName}.handlers`,
|
|
3776
|
+
message: `Subsystem "${subsystemName}" has no handlers`,
|
|
3777
|
+
suggestion: "Add at least one handler to the subsystem"
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
for (const handler of subsystem.handlers) {
|
|
3781
|
+
const existingOwner = allAssignedHandlers.get(handler);
|
|
3782
|
+
if (existingOwner) {
|
|
3783
|
+
this.issues.push({
|
|
3784
|
+
type: "invalid_value",
|
|
3785
|
+
severity: "error",
|
|
3786
|
+
field: `subsystems.${subsystemName}.handlers`,
|
|
3787
|
+
message: `Handler "${handler}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
|
|
3788
|
+
suggestion: "Each handler must belong to exactly one subsystem"
|
|
3789
|
+
});
|
|
3790
|
+
} else {
|
|
3791
|
+
allAssignedHandlers.set(handler, subsystemName);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
if (subsystem.state.length === 0) {
|
|
3795
|
+
this.issues.push({
|
|
3796
|
+
type: "invalid_value",
|
|
3797
|
+
severity: "warning",
|
|
3798
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3799
|
+
message: `Subsystem "${subsystemName}" has no state fields`,
|
|
3800
|
+
suggestion: "Add at least one state field to the subsystem"
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3654
3805
|
}
|
|
3655
3806
|
function validateConfig(configPath) {
|
|
3656
3807
|
const validator = new ConfigValidator;
|
|
@@ -4069,7 +4220,8 @@ class HandlerExtractor {
|
|
|
4069
4220
|
packageRoot;
|
|
4070
4221
|
warnings;
|
|
4071
4222
|
currentFunctionParams = [];
|
|
4072
|
-
|
|
4223
|
+
contextOverrides;
|
|
4224
|
+
constructor(tsConfigPath, contextOverrides) {
|
|
4073
4225
|
this.project = new Project({
|
|
4074
4226
|
tsConfigFilePath: tsConfigPath
|
|
4075
4227
|
});
|
|
@@ -4077,6 +4229,7 @@ class HandlerExtractor {
|
|
|
4077
4229
|
this.relationshipExtractor = new RelationshipExtractor;
|
|
4078
4230
|
this.analyzedFiles = new Set;
|
|
4079
4231
|
this.warnings = [];
|
|
4232
|
+
this.contextOverrides = contextOverrides || new Map;
|
|
4080
4233
|
this.packageRoot = this.findPackageRoot(tsConfigPath);
|
|
4081
4234
|
}
|
|
4082
4235
|
warnUnsupportedPattern(pattern, location, suggestion) {
|
|
@@ -4294,8 +4447,20 @@ class HandlerExtractor {
|
|
|
4294
4447
|
handlers.push(handler);
|
|
4295
4448
|
}
|
|
4296
4449
|
}
|
|
4450
|
+
if (methodName === "ws") {
|
|
4451
|
+
this.extractElysiaWsHandlers(node, context, filePath, handlers);
|
|
4452
|
+
}
|
|
4453
|
+
if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
|
|
4454
|
+
const restHandler = this.extractRestHandler(node, methodName, context, filePath);
|
|
4455
|
+
if (restHandler) {
|
|
4456
|
+
handlers.push(restHandler);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4297
4459
|
}
|
|
4298
4460
|
}
|
|
4461
|
+
isRestMethod(name) {
|
|
4462
|
+
return ["get", "post", "put", "delete", "patch"].includes(name);
|
|
4463
|
+
}
|
|
4299
4464
|
isElseIfStatement(node) {
|
|
4300
4465
|
const parent = node.getParent();
|
|
4301
4466
|
return parent !== undefined && Node2.isIfStatement(parent);
|
|
@@ -4361,6 +4526,135 @@ class HandlerExtractor {
|
|
|
4361
4526
|
parameters
|
|
4362
4527
|
};
|
|
4363
4528
|
}
|
|
4529
|
+
extractElysiaWsHandlers(node, context, filePath, handlers) {
|
|
4530
|
+
const args = node.getArguments();
|
|
4531
|
+
if (args.length < 2)
|
|
4532
|
+
return;
|
|
4533
|
+
const routeArg = args[0];
|
|
4534
|
+
if (!routeArg || !Node2.isStringLiteral(routeArg))
|
|
4535
|
+
return;
|
|
4536
|
+
const routePath = routeArg.getLiteralValue();
|
|
4537
|
+
const configArg = args[1];
|
|
4538
|
+
if (!configArg || !Node2.isObjectLiteralExpression(configArg))
|
|
4539
|
+
return;
|
|
4540
|
+
const callbacks = ["message", "open", "close"];
|
|
4541
|
+
for (const cbName of callbacks) {
|
|
4542
|
+
const prop = configArg.getProperty(cbName);
|
|
4543
|
+
if (!prop)
|
|
4544
|
+
continue;
|
|
4545
|
+
let funcBody = null;
|
|
4546
|
+
if (Node2.isMethodDeclaration(prop)) {
|
|
4547
|
+
funcBody = prop;
|
|
4548
|
+
} else if (Node2.isPropertyAssignment(prop)) {
|
|
4549
|
+
const init = prop.getInitializer();
|
|
4550
|
+
if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
|
|
4551
|
+
funcBody = init;
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
if (!funcBody)
|
|
4555
|
+
continue;
|
|
4556
|
+
if (cbName === "message") {
|
|
4557
|
+
const body = funcBody.getBody();
|
|
4558
|
+
if (!body)
|
|
4559
|
+
continue;
|
|
4560
|
+
const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
|
|
4561
|
+
if (subHandlers.length > 0) {
|
|
4562
|
+
handlers.push(...subHandlers);
|
|
4563
|
+
} else {
|
|
4564
|
+
handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
|
|
4565
|
+
}
|
|
4566
|
+
} else {
|
|
4567
|
+
handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
extractSubHandlersFromBody(body, context, filePath) {
|
|
4572
|
+
const subHandlers = [];
|
|
4573
|
+
body.forEachDescendant((child) => {
|
|
4574
|
+
if (Node2.isIfStatement(child) && !this.isElseIfStatement(child)) {
|
|
4575
|
+
const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
|
|
4576
|
+
subHandlers.push(...typeGuardHandlers);
|
|
4577
|
+
}
|
|
4578
|
+
if (Node2.isSwitchStatement(child)) {
|
|
4579
|
+
const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
|
|
4580
|
+
subHandlers.push(...switchHandlers);
|
|
4581
|
+
}
|
|
4582
|
+
});
|
|
4583
|
+
return subHandlers;
|
|
4584
|
+
}
|
|
4585
|
+
buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
|
|
4586
|
+
const assignments = [];
|
|
4587
|
+
const preconditions = [];
|
|
4588
|
+
const postconditions = [];
|
|
4589
|
+
this.currentFunctionParams = this.extractParameterNames(funcBody);
|
|
4590
|
+
this.extractAssignments(funcBody, assignments);
|
|
4591
|
+
this.extractVerificationConditions(funcBody, preconditions, postconditions);
|
|
4592
|
+
this.currentFunctionParams = [];
|
|
4593
|
+
return {
|
|
4594
|
+
messageType,
|
|
4595
|
+
node: context,
|
|
4596
|
+
assignments,
|
|
4597
|
+
preconditions,
|
|
4598
|
+
postconditions,
|
|
4599
|
+
location: { file: filePath, line },
|
|
4600
|
+
origin: "event"
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
extractRestHandler(node, methodName, context, filePath) {
|
|
4604
|
+
const args = node.getArguments();
|
|
4605
|
+
if (args.length < 2)
|
|
4606
|
+
return null;
|
|
4607
|
+
const routeArg = args[0];
|
|
4608
|
+
if (!routeArg || !Node2.isStringLiteral(routeArg))
|
|
4609
|
+
return null;
|
|
4610
|
+
const routePath = routeArg.getLiteralValue();
|
|
4611
|
+
const httpMethod = methodName.toUpperCase();
|
|
4612
|
+
const messageType = `${httpMethod} ${routePath}`;
|
|
4613
|
+
const handlerArg = args[1];
|
|
4614
|
+
const assignments = [];
|
|
4615
|
+
const preconditions = [];
|
|
4616
|
+
const postconditions = [];
|
|
4617
|
+
let actualHandler = null;
|
|
4618
|
+
if (Node2.isArrowFunction(handlerArg) || Node2.isFunctionExpression(handlerArg)) {
|
|
4619
|
+
actualHandler = handlerArg;
|
|
4620
|
+
} else if (Node2.isIdentifier(handlerArg)) {
|
|
4621
|
+
actualHandler = this.resolveFunctionReference(handlerArg);
|
|
4622
|
+
}
|
|
4623
|
+
let parameters;
|
|
4624
|
+
if (actualHandler) {
|
|
4625
|
+
this.currentFunctionParams = this.extractParameterNames(actualHandler);
|
|
4626
|
+
parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
|
|
4627
|
+
this.extractAssignments(actualHandler, assignments);
|
|
4628
|
+
this.extractVerificationConditions(actualHandler, preconditions, postconditions);
|
|
4629
|
+
this.currentFunctionParams = [];
|
|
4630
|
+
}
|
|
4631
|
+
return {
|
|
4632
|
+
messageType,
|
|
4633
|
+
node: context,
|
|
4634
|
+
assignments,
|
|
4635
|
+
preconditions,
|
|
4636
|
+
postconditions,
|
|
4637
|
+
location: {
|
|
4638
|
+
file: filePath,
|
|
4639
|
+
line: node.getStartLineNumber()
|
|
4640
|
+
},
|
|
4641
|
+
origin: "event",
|
|
4642
|
+
parameters,
|
|
4643
|
+
handlerKind: "rest",
|
|
4644
|
+
httpMethod,
|
|
4645
|
+
routePath
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
isWebFrameworkFile(sourceFile) {
|
|
4649
|
+
const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
|
|
4650
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
4651
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
4652
|
+
if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
|
|
4653
|
+
return true;
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
return false;
|
|
4657
|
+
}
|
|
4364
4658
|
extractAssignments(funcNode, assignments) {
|
|
4365
4659
|
funcNode.forEachDescendant((node) => {
|
|
4366
4660
|
if (Node2.isBinaryExpression(node)) {
|
|
@@ -5195,32 +5489,64 @@ class HandlerExtractor {
|
|
|
5195
5489
|
try {
|
|
5196
5490
|
const ifStmt = ifNode;
|
|
5197
5491
|
const condition = ifStmt.getExpression();
|
|
5198
|
-
if (
|
|
5199
|
-
|
|
5492
|
+
if (Node2.isCallExpression(condition)) {
|
|
5493
|
+
const funcExpr = condition.getExpression();
|
|
5494
|
+
const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
|
|
5495
|
+
this.debugLogProcessingFunction(funcName);
|
|
5496
|
+
const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
|
|
5497
|
+
if (!messageType) {
|
|
5498
|
+
this.debugLogUnresolvedMessageType(funcName);
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5501
|
+
const line = ifStmt.getStartLineNumber();
|
|
5502
|
+
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5503
|
+
return {
|
|
5504
|
+
messageType,
|
|
5505
|
+
node: context,
|
|
5506
|
+
assignments: [],
|
|
5507
|
+
preconditions: [],
|
|
5508
|
+
postconditions: [],
|
|
5509
|
+
location: { file: filePath, line },
|
|
5510
|
+
relationships
|
|
5511
|
+
};
|
|
5200
5512
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5513
|
+
if (Node2.isBinaryExpression(condition)) {
|
|
5514
|
+
const messageType = this.extractMessageTypeFromEqualityCheck(condition);
|
|
5515
|
+
if (messageType) {
|
|
5516
|
+
const line = ifStmt.getStartLineNumber();
|
|
5517
|
+
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5518
|
+
return {
|
|
5519
|
+
messageType,
|
|
5520
|
+
node: context,
|
|
5521
|
+
assignments: [],
|
|
5522
|
+
preconditions: [],
|
|
5523
|
+
postconditions: [],
|
|
5524
|
+
location: { file: filePath, line },
|
|
5525
|
+
relationships
|
|
5526
|
+
};
|
|
5527
|
+
}
|
|
5208
5528
|
}
|
|
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
|
-
};
|
|
5529
|
+
return null;
|
|
5220
5530
|
} catch (_error) {
|
|
5221
5531
|
return null;
|
|
5222
5532
|
}
|
|
5223
5533
|
}
|
|
5534
|
+
extractMessageTypeFromEqualityCheck(expr) {
|
|
5535
|
+
const operator = expr.getOperatorToken().getText();
|
|
5536
|
+
if (operator !== "===" && operator !== "==")
|
|
5537
|
+
return null;
|
|
5538
|
+
const left = expr.getLeft();
|
|
5539
|
+
const right = expr.getRight();
|
|
5540
|
+
const stringLiteral = Node2.isStringLiteral(right) ? right : Node2.isStringLiteral(left) ? left : null;
|
|
5541
|
+
const propAccess = Node2.isPropertyAccessExpression(left) ? left : Node2.isPropertyAccessExpression(right) ? right : null;
|
|
5542
|
+
if (!stringLiteral || !propAccess)
|
|
5543
|
+
return null;
|
|
5544
|
+
const propName = propAccess.getName();
|
|
5545
|
+
if (propName !== "type" && propName !== "kind" && propName !== "action") {
|
|
5546
|
+
return null;
|
|
5547
|
+
}
|
|
5548
|
+
return stringLiteral.getLiteralValue();
|
|
5549
|
+
}
|
|
5224
5550
|
debugLogProcessingFunction(funcName) {
|
|
5225
5551
|
if (process.env["POLLY_DEBUG"] && funcName) {
|
|
5226
5552
|
console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
|
|
@@ -5430,6 +5756,10 @@ class HandlerExtractor {
|
|
|
5430
5756
|
return messageType;
|
|
5431
5757
|
}
|
|
5432
5758
|
inferContext(filePath) {
|
|
5759
|
+
for (const [pathPrefix, context] of this.contextOverrides.entries()) {
|
|
5760
|
+
if (filePath.startsWith(pathPrefix))
|
|
5761
|
+
return context;
|
|
5762
|
+
}
|
|
5433
5763
|
const path2 = filePath.toLowerCase();
|
|
5434
5764
|
return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
|
|
5435
5765
|
}
|
|
@@ -6645,15 +6975,23 @@ async function runFullVerification(configPath) {
|
|
|
6645
6975
|
if (exprValidation.warnings.length > 0) {
|
|
6646
6976
|
displayExpressionWarnings(exprValidation);
|
|
6647
6977
|
}
|
|
6978
|
+
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
6979
|
+
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
6980
|
+
return;
|
|
6981
|
+
}
|
|
6982
|
+
await runMonolithicVerification(config, analysis);
|
|
6983
|
+
}
|
|
6984
|
+
async function runMonolithicVerification(config, analysis) {
|
|
6648
6985
|
const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
|
|
6649
6986
|
findAndCopyBaseSpec(specDir);
|
|
6650
6987
|
console.log(color("✓ Specification generated", COLORS.green));
|
|
6651
6988
|
console.log(color(` ${specPath}`, COLORS.gray));
|
|
6652
6989
|
console.log();
|
|
6653
6990
|
const docker = await setupDocker();
|
|
6654
|
-
const
|
|
6655
|
-
const
|
|
6656
|
-
const
|
|
6991
|
+
const typedConfig = config;
|
|
6992
|
+
const timeoutSeconds = getTimeout(typedConfig);
|
|
6993
|
+
const workers = getWorkers(typedConfig);
|
|
6994
|
+
const maxDepth = getMaxDepth(typedConfig);
|
|
6657
6995
|
console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
|
|
6658
6996
|
if (timeoutSeconds === 0) {
|
|
6659
6997
|
console.log(color(" No timeout set - will run until completion", COLORS.gray));
|
|
@@ -6673,6 +7011,122 @@ async function runFullVerification(configPath) {
|
|
|
6673
7011
|
});
|
|
6674
7012
|
displayVerificationResults(result, specDir);
|
|
6675
7013
|
}
|
|
7014
|
+
async function runSubsystemVerification(config, analysis) {
|
|
7015
|
+
const subsystems = config.subsystems;
|
|
7016
|
+
const subsystemNames = Object.keys(subsystems);
|
|
7017
|
+
console.log(color(`\uD83D\uDCE6 Subsystem-scoped verification (${subsystemNames.length} subsystems)
|
|
7018
|
+
`, COLORS.blue));
|
|
7019
|
+
const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
|
|
7020
|
+
const interference = checkNonInterference2(subsystems, analysis.handlers);
|
|
7021
|
+
if (interference.valid) {
|
|
7022
|
+
console.log(color("✓ Non-interference: verified (no cross-subsystem state writes)", COLORS.green));
|
|
7023
|
+
console.log();
|
|
7024
|
+
} else {
|
|
7025
|
+
console.log(color(`⚠️ Non-interference violations detected:
|
|
7026
|
+
`, COLORS.yellow));
|
|
7027
|
+
for (const v of interference.violations) {
|
|
7028
|
+
console.log(color(` • Handler "${v.handler}" (${v.subsystem}) writes to "${v.writesTo}" owned by "${v.ownedBy}"`, COLORS.yellow));
|
|
7029
|
+
}
|
|
7030
|
+
console.log();
|
|
7031
|
+
console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
|
|
7032
|
+
console.log();
|
|
7033
|
+
}
|
|
7034
|
+
const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
|
|
7035
|
+
const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
|
|
7036
|
+
if (unassigned.length > 0) {
|
|
7037
|
+
console.log(color(`⚠️ ${unassigned.length} handler(s) not assigned to any subsystem (will not be verified):`, COLORS.yellow));
|
|
7038
|
+
for (const h of unassigned.slice(0, 10)) {
|
|
7039
|
+
console.log(color(` • ${h}`, COLORS.yellow));
|
|
7040
|
+
}
|
|
7041
|
+
if (unassigned.length > 10) {
|
|
7042
|
+
console.log(color(` ... and ${unassigned.length - 10} more`, COLORS.yellow));
|
|
7043
|
+
}
|
|
7044
|
+
console.log();
|
|
7045
|
+
}
|
|
7046
|
+
const docker = await setupDocker();
|
|
7047
|
+
const timeoutSeconds = getTimeout(config);
|
|
7048
|
+
const workers = getWorkers(config);
|
|
7049
|
+
const maxDepth = getMaxDepth(config);
|
|
7050
|
+
const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
7051
|
+
const results = [];
|
|
7052
|
+
for (const name of subsystemNames) {
|
|
7053
|
+
const sub = subsystems[name];
|
|
7054
|
+
const startTime = Date.now();
|
|
7055
|
+
console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
|
|
7056
|
+
const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
|
|
7057
|
+
const specDir = path4.join(process.cwd(), "specs", "tla", "generated", name);
|
|
7058
|
+
if (!fs4.existsSync(specDir)) {
|
|
7059
|
+
fs4.mkdirSync(specDir, { recursive: true });
|
|
7060
|
+
}
|
|
7061
|
+
const specPath = path4.join(specDir, `UserApp_${name}.tla`);
|
|
7062
|
+
const cfgPath = path4.join(specDir, `UserApp_${name}.cfg`);
|
|
7063
|
+
fs4.writeFileSync(specPath, spec);
|
|
7064
|
+
fs4.writeFileSync(cfgPath, cfg);
|
|
7065
|
+
findAndCopyBaseSpec(specDir);
|
|
7066
|
+
const result = await docker.runTLC(specPath, {
|
|
7067
|
+
workers,
|
|
7068
|
+
timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
|
|
7069
|
+
maxDepth
|
|
7070
|
+
});
|
|
7071
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
7072
|
+
results.push({
|
|
7073
|
+
name,
|
|
7074
|
+
success: result.success,
|
|
7075
|
+
handlerCount: sub.handlers.length,
|
|
7076
|
+
stateCount: result.stats?.distinctStates ?? 0,
|
|
7077
|
+
elapsed,
|
|
7078
|
+
stats: result.stats,
|
|
7079
|
+
error: result.error
|
|
7080
|
+
});
|
|
7081
|
+
if (result.success) {
|
|
7082
|
+
console.log(color(` ✓ ${name} passed (${elapsed.toFixed(1)}s)`, COLORS.green));
|
|
7083
|
+
} else {
|
|
7084
|
+
console.log(color(` ✗ ${name} failed`, COLORS.red));
|
|
7085
|
+
if (result.violation) {
|
|
7086
|
+
console.log(color(` Invariant violated: ${result.violation.name}`, COLORS.red));
|
|
7087
|
+
} else if (result.error) {
|
|
7088
|
+
console.log(color(` Error: ${result.error}`, COLORS.red));
|
|
7089
|
+
}
|
|
7090
|
+
fs4.writeFileSync(path4.join(specDir, "tlc-output.log"), result.output);
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
console.log();
|
|
7094
|
+
displayCompositionalReport(results, interference.valid);
|
|
7095
|
+
}
|
|
7096
|
+
function displayCompositionalReport(results, nonInterferenceValid) {
|
|
7097
|
+
console.log(color(`Subsystem verification results:
|
|
7098
|
+
`, COLORS.blue));
|
|
7099
|
+
for (const r of results) {
|
|
7100
|
+
const status = r.success ? color("✓", COLORS.green) : color("✗", COLORS.red);
|
|
7101
|
+
const name = r.name.padEnd(20);
|
|
7102
|
+
const handlers = `${r.handlerCount} handler${r.handlerCount !== 1 ? "s" : ""}`;
|
|
7103
|
+
const states = `${r.stateCount} states`;
|
|
7104
|
+
const time = `${r.elapsed.toFixed(1)}s`;
|
|
7105
|
+
console.log(` ${status} ${name} ${handlers.padEnd(14)} ${states.padEnd(14)} ${time}`);
|
|
7106
|
+
}
|
|
7107
|
+
console.log();
|
|
7108
|
+
const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
|
|
7109
|
+
console.log(` Non-interference: ${nonIntLabel}`);
|
|
7110
|
+
console.log();
|
|
7111
|
+
const allPassed = results.every((r) => r.success);
|
|
7112
|
+
if (allPassed && nonInterferenceValid) {
|
|
7113
|
+
console.log(color("Compositional result: ✓ PASS", COLORS.green));
|
|
7114
|
+
console.log(color(" All subsystems verified independently. By non-interference,", COLORS.gray));
|
|
7115
|
+
console.log(color(" the full system satisfies all per-subsystem invariants.", COLORS.gray));
|
|
7116
|
+
} else if (allPassed && !nonInterferenceValid) {
|
|
7117
|
+
console.log(color("Compositional result: ⚠ PASS (with warnings)", COLORS.yellow));
|
|
7118
|
+
console.log(color(" All subsystems passed, but non-interference violations exist.", COLORS.gray));
|
|
7119
|
+
console.log(color(" Compositional soundness is not guaranteed.", COLORS.gray));
|
|
7120
|
+
} else {
|
|
7121
|
+
console.log(color("Compositional result: ✗ FAIL", COLORS.red));
|
|
7122
|
+
const failed = results.filter((r) => !r.success);
|
|
7123
|
+
for (const f of failed) {
|
|
7124
|
+
console.log(color(` Failed: ${f.name}`, COLORS.red));
|
|
7125
|
+
}
|
|
7126
|
+
process.exit(1);
|
|
7127
|
+
}
|
|
7128
|
+
console.log();
|
|
7129
|
+
}
|
|
6676
7130
|
async function loadVerificationConfig(configPath) {
|
|
6677
7131
|
const resolvedPath = path4.resolve(configPath);
|
|
6678
7132
|
const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
|
|
@@ -6889,4 +7343,4 @@ main().catch((error) => {
|
|
|
6889
7343
|
process.exit(1);
|
|
6890
7344
|
});
|
|
6891
7345
|
|
|
6892
|
-
//# debugId=
|
|
7346
|
+
//# debugId=7EE824400DEB8F7564756E2164756E21
|