@fairfox/polly 0.1.3 → 0.1.5
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/README.md +560 -184
- package/{cli/polly.ts → dist/cli/polly.js} +100 -206
- package/dist/cli/polly.js.map +10 -0
- package/dist/scripts/build-extension.js +137 -0
- package/dist/scripts/build-extension.js.map +10 -0
- package/dist/vendor/verify/src/cli.js +2089 -0
- package/dist/vendor/verify/src/cli.js.map +16 -0
- package/dist/vendor/visualize/src/cli.js +2204 -0
- package/dist/vendor/visualize/src/cli.js.map +19 -0
- package/package.json +12 -12
- package/vendor/analysis/src/extract/adr.ts +0 -212
- package/vendor/analysis/src/extract/architecture.ts +0 -160
- package/vendor/analysis/src/extract/contexts.ts +0 -298
- package/vendor/analysis/src/extract/flows.ts +0 -309
- package/vendor/analysis/src/extract/handlers.ts +0 -321
- package/vendor/analysis/src/extract/index.ts +0 -9
- package/vendor/analysis/src/extract/integrations.ts +0 -329
- package/vendor/analysis/src/extract/manifest.ts +0 -298
- package/vendor/analysis/src/extract/types.ts +0 -389
- package/vendor/analysis/src/index.ts +0 -7
- package/vendor/analysis/src/types/adr.ts +0 -53
- package/vendor/analysis/src/types/architecture.ts +0 -245
- package/vendor/analysis/src/types/core.ts +0 -210
- package/vendor/analysis/src/types/index.ts +0 -18
- package/vendor/verify/src/adapters/base.ts +0 -164
- package/vendor/verify/src/adapters/detection.ts +0 -281
- package/vendor/verify/src/adapters/event-bus/index.ts +0 -480
- package/vendor/verify/src/adapters/web-extension/index.ts +0 -508
- package/vendor/verify/src/adapters/websocket/index.ts +0 -486
- package/vendor/verify/src/cli.ts +0 -430
- package/vendor/verify/src/codegen/config.ts +0 -354
- package/vendor/verify/src/codegen/tla.ts +0 -719
- package/vendor/verify/src/config/parser.ts +0 -303
- package/vendor/verify/src/config/types.ts +0 -113
- package/vendor/verify/src/core/model.ts +0 -267
- package/vendor/verify/src/core/primitives.ts +0 -106
- package/vendor/verify/src/extract/handlers.ts +0 -2
- package/vendor/verify/src/extract/types.ts +0 -2
- package/vendor/verify/src/index.ts +0 -150
- package/vendor/verify/src/primitives/index.ts +0 -102
- package/vendor/verify/src/runner/docker.ts +0 -283
- package/vendor/verify/src/types.ts +0 -51
- package/vendor/visualize/src/cli.ts +0 -365
- package/vendor/visualize/src/codegen/structurizr.ts +0 -770
- package/vendor/visualize/src/index.ts +0 -13
- package/vendor/visualize/src/runner/export.ts +0 -235
- package/vendor/visualize/src/viewer/server.ts +0 -485
- /package/dist/{background → src/background}/index.js +0 -0
- /package/dist/{background → src/background}/index.js.map +0 -0
- /package/dist/{background → src/background}/message-router.js +0 -0
- /package/dist/{background → src/background}/message-router.js.map +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{index.js.map → src/index.js.map} +0 -0
- /package/dist/{shared → src/shared}/adapters/index.js +0 -0
- /package/dist/{shared → src/shared}/adapters/index.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/context-helpers.js +0 -0
- /package/dist/{shared → src/shared}/lib/context-helpers.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/errors.js +0 -0
- /package/dist/{shared → src/shared}/lib/errors.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/message-bus.js +0 -0
- /package/dist/{shared → src/shared}/lib/message-bus.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/state.js +0 -0
- /package/dist/{shared → src/shared}/lib/state.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/test-helpers.js +0 -0
- /package/dist/{shared → src/shared}/lib/test-helpers.js.map +0 -0
- /package/dist/{shared → src/shared}/state/app-state.js +0 -0
- /package/dist/{shared → src/shared}/state/app-state.js.map +0 -0
- /package/dist/{shared → src/shared}/types/messages.js +0 -0
- /package/dist/{shared → src/shared}/types/messages.js.map +0 -0
|
@@ -1,719 +0,0 @@
|
|
|
1
|
-
// TLA+ specification generator
|
|
2
|
-
|
|
3
|
-
import type { VerificationConfig, CodebaseAnalysis } from "../types";
|
|
4
|
-
import type { MessageHandler } from "../core/model";
|
|
5
|
-
|
|
6
|
-
export class TLAGenerator {
|
|
7
|
-
private lines: string[] = [];
|
|
8
|
-
private indent = 0;
|
|
9
|
-
|
|
10
|
-
generate(
|
|
11
|
-
config: VerificationConfig,
|
|
12
|
-
analysis: CodebaseAnalysis
|
|
13
|
-
): {
|
|
14
|
-
spec: string;
|
|
15
|
-
cfg: string;
|
|
16
|
-
} {
|
|
17
|
-
this.lines = [];
|
|
18
|
-
this.indent = 0;
|
|
19
|
-
|
|
20
|
-
const spec = this.generateSpec(config, analysis);
|
|
21
|
-
const cfg = this.generateConfig(config, analysis);
|
|
22
|
-
|
|
23
|
-
return { spec, cfg };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private generateSpec(config: VerificationConfig, analysis: CodebaseAnalysis): string {
|
|
27
|
-
this.lines = [];
|
|
28
|
-
this.indent = 0;
|
|
29
|
-
|
|
30
|
-
this.addHeader();
|
|
31
|
-
this.addExtends();
|
|
32
|
-
this.addConstants(config, analysis);
|
|
33
|
-
this.addMessageTypes(config, analysis);
|
|
34
|
-
this.addStateType(config, analysis);
|
|
35
|
-
this.addVariables();
|
|
36
|
-
this.addInit(config, analysis);
|
|
37
|
-
this.addActions(config, analysis);
|
|
38
|
-
this.addRouteWithHandlers(config, analysis);
|
|
39
|
-
this.addNext(config, analysis);
|
|
40
|
-
this.addSpec();
|
|
41
|
-
this.addInvariants(config, analysis);
|
|
42
|
-
|
|
43
|
-
return this.lines.join("\n");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private generateConfig(config: VerificationConfig, analysis: CodebaseAnalysis): string {
|
|
47
|
-
const lines: string[] = [];
|
|
48
|
-
|
|
49
|
-
lines.push("SPECIFICATION UserSpec");
|
|
50
|
-
lines.push("");
|
|
51
|
-
lines.push("\\* Constants");
|
|
52
|
-
lines.push("CONSTANTS");
|
|
53
|
-
|
|
54
|
-
// Generate context set (reduced for faster verification)
|
|
55
|
-
lines.push(" Contexts = {background, content, popup}");
|
|
56
|
-
|
|
57
|
-
// Message bounds (defaults chosen for reasonable verification time)
|
|
58
|
-
lines.push(` MaxMessages = ${config.messages.maxInFlight || 3}`);
|
|
59
|
-
lines.push(` MaxTabId = ${config.messages.maxTabs || 1}`);
|
|
60
|
-
lines.push(" TimeoutLimit = 3");
|
|
61
|
-
|
|
62
|
-
// State bounds from config
|
|
63
|
-
for (const [field, fieldConfig] of Object.entries(config.state)) {
|
|
64
|
-
if (typeof fieldConfig === "object" && fieldConfig !== null) {
|
|
65
|
-
if ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) {
|
|
66
|
-
const constName = this.fieldToConstName(field);
|
|
67
|
-
lines.push(` ${constName}_MaxLength = ${fieldConfig.maxLength}`);
|
|
68
|
-
}
|
|
69
|
-
if ("max" in fieldConfig && fieldConfig.max !== null) {
|
|
70
|
-
const constName = this.fieldToConstName(field);
|
|
71
|
-
lines.push(` ${constName}_Max = ${fieldConfig.max}`);
|
|
72
|
-
}
|
|
73
|
-
if ("maxSize" in fieldConfig && fieldConfig.maxSize !== null) {
|
|
74
|
-
const constName = this.fieldToConstName(field);
|
|
75
|
-
lines.push(` ${constName}_MaxSize = ${fieldConfig.maxSize}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
lines.push("");
|
|
81
|
-
lines.push("\\* Invariants to check");
|
|
82
|
-
lines.push("INVARIANTS");
|
|
83
|
-
lines.push(" TypeOK");
|
|
84
|
-
lines.push(" NoRoutingLoops");
|
|
85
|
-
lines.push(" UserStateTypeInvariant");
|
|
86
|
-
lines.push("");
|
|
87
|
-
lines.push("\\* State constraint");
|
|
88
|
-
lines.push("CONSTRAINT");
|
|
89
|
-
lines.push(" StateConstraint");
|
|
90
|
-
|
|
91
|
-
return lines.join("\n");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private addHeader(): void {
|
|
95
|
-
this.line("------------------------- MODULE UserApp -------------------------");
|
|
96
|
-
this.line("(*");
|
|
97
|
-
this.line(" Auto-generated TLA+ specification for web extension");
|
|
98
|
-
this.line(" ");
|
|
99
|
-
this.line(" Generated from:");
|
|
100
|
-
this.line(" - TypeScript type definitions");
|
|
101
|
-
this.line(" - Verification configuration");
|
|
102
|
-
this.line(" ");
|
|
103
|
-
this.line(" This spec extends MessageRouter with:");
|
|
104
|
-
this.line(" - Application-specific state types");
|
|
105
|
-
this.line(" - Message type definitions");
|
|
106
|
-
this.line(" - State transition actions");
|
|
107
|
-
this.line("*)");
|
|
108
|
-
this.line("");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private addExtends(): void {
|
|
112
|
-
this.line("EXTENDS MessageRouter");
|
|
113
|
-
this.line("");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private addConstants(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
117
|
-
// MessageRouter already defines: Contexts, MaxMessages, MaxTabId, TimeoutLimit
|
|
118
|
-
// We only add application-specific constants
|
|
119
|
-
|
|
120
|
-
const hasCustomConstants = Object.entries(config.state).some(([field, fieldConfig]) => {
|
|
121
|
-
if (typeof fieldConfig !== "object" || fieldConfig === null) return false;
|
|
122
|
-
return (
|
|
123
|
-
("maxLength" in fieldConfig && fieldConfig.maxLength !== null) ||
|
|
124
|
-
("max" in fieldConfig && fieldConfig.max !== null) ||
|
|
125
|
-
("maxSize" in fieldConfig && fieldConfig.maxSize !== null)
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
if (!hasCustomConstants) {
|
|
130
|
-
// No custom constants needed
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.line("\\* Application-specific constants");
|
|
135
|
-
this.line("CONSTANTS");
|
|
136
|
-
this.indent++;
|
|
137
|
-
|
|
138
|
-
let first = true;
|
|
139
|
-
for (const [field, fieldConfig] of Object.entries(config.state)) {
|
|
140
|
-
if (typeof fieldConfig === "object" && fieldConfig !== null) {
|
|
141
|
-
const constName = this.fieldToConstName(field);
|
|
142
|
-
|
|
143
|
-
if ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) {
|
|
144
|
-
this.line(`${first ? "" : ","}${constName}_MaxLength \\* Max length for ${field}`);
|
|
145
|
-
first = false;
|
|
146
|
-
}
|
|
147
|
-
if ("max" in fieldConfig && fieldConfig.max !== null) {
|
|
148
|
-
this.line(`${first ? "" : ","}${constName}_Max \\* Max value for ${field}`);
|
|
149
|
-
first = false;
|
|
150
|
-
}
|
|
151
|
-
if ("maxSize" in fieldConfig && fieldConfig.maxSize !== null) {
|
|
152
|
-
this.line(`${first ? "" : ","}${constName}_MaxSize \\* Max size for ${field}`);
|
|
153
|
-
first = false;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
this.indent--;
|
|
159
|
-
this.line("");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private addStateType(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
163
|
-
// Define Value type for generic sequences and maps
|
|
164
|
-
// Use a finite set for model checking
|
|
165
|
-
this.line("\\* Generic value type for sequences and maps");
|
|
166
|
-
this.line("\\* Bounded to allow model checking");
|
|
167
|
-
this.line('Value == {"v1", "v2", "v3"}');
|
|
168
|
-
this.line("");
|
|
169
|
-
|
|
170
|
-
// Define Keys type for map domains
|
|
171
|
-
this.line("\\* Generic key type for maps");
|
|
172
|
-
this.line("\\* Bounded to allow model checking");
|
|
173
|
-
this.line('Keys == {"k1", "k2", "k3"}');
|
|
174
|
-
this.line("");
|
|
175
|
-
|
|
176
|
-
this.line("\\* Application state type definition");
|
|
177
|
-
this.line("State == [");
|
|
178
|
-
this.indent++;
|
|
179
|
-
|
|
180
|
-
const stateFields: string[] = [];
|
|
181
|
-
|
|
182
|
-
for (const [fieldPath, fieldConfig] of Object.entries(config.state)) {
|
|
183
|
-
if (typeof fieldConfig !== "object" || fieldConfig === null) continue;
|
|
184
|
-
|
|
185
|
-
const fieldName = this.sanitizeFieldName(fieldPath);
|
|
186
|
-
const tlaType = this.fieldConfigToTLAType(fieldPath, fieldConfig, config);
|
|
187
|
-
|
|
188
|
-
stateFields.push(`${fieldName}: ${tlaType}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (let i = 0; i < stateFields.length; i++) {
|
|
192
|
-
const field = stateFields[i];
|
|
193
|
-
const suffix = i < stateFields.length - 1 ? "," : "";
|
|
194
|
-
this.line(`${field}${suffix}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
this.indent--;
|
|
198
|
-
this.line("]");
|
|
199
|
-
this.line("");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private addMessageTypes(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
203
|
-
if (analysis.messageTypes.length === 0) {
|
|
204
|
-
// No message types found, skip
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this.line("\\* Message types from application");
|
|
209
|
-
const messageTypeSet = analysis.messageTypes.map((t) => `"${t}"`).join(", ");
|
|
210
|
-
this.line(`UserMessageTypes == {${messageTypeSet}}`);
|
|
211
|
-
this.line("");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private addVariables(): void {
|
|
215
|
-
// MessageRouter already defines: ports, messages, pendingRequests, delivered, routingDepth, time
|
|
216
|
-
// We add: contextStates for application state
|
|
217
|
-
|
|
218
|
-
this.line("\\* Application state per context");
|
|
219
|
-
this.line("VARIABLE contextStates");
|
|
220
|
-
this.line("");
|
|
221
|
-
this.line("\\* All variables (extending MessageRouter vars)");
|
|
222
|
-
this.line(
|
|
223
|
-
"allVars == <<ports, messages, pendingRequests, delivered, routingDepth, time, contextStates>>"
|
|
224
|
-
);
|
|
225
|
-
this.line("");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private addInit(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
229
|
-
// Generate InitialState first
|
|
230
|
-
this.line("\\* Initial application state");
|
|
231
|
-
this.line("InitialState == [");
|
|
232
|
-
this.indent++;
|
|
233
|
-
|
|
234
|
-
const fields: string[] = [];
|
|
235
|
-
for (const [fieldPath, fieldConfig] of Object.entries(config.state)) {
|
|
236
|
-
if (typeof fieldConfig !== "object" || fieldConfig === null) continue;
|
|
237
|
-
|
|
238
|
-
const fieldName = this.sanitizeFieldName(fieldPath);
|
|
239
|
-
const initialValue = this.getInitialValue(fieldConfig);
|
|
240
|
-
fields.push(`${fieldName} |-> ${initialValue}`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for (let i = 0; i < fields.length; i++) {
|
|
244
|
-
const field = fields[i];
|
|
245
|
-
const suffix = i < fields.length - 1 ? "," : "";
|
|
246
|
-
this.line(`${field}${suffix}`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
this.indent--;
|
|
250
|
-
this.line("]");
|
|
251
|
-
this.line("");
|
|
252
|
-
|
|
253
|
-
// Init extends MessageRouter's Init
|
|
254
|
-
this.line("\\* Initial state (extends MessageRouter)");
|
|
255
|
-
this.line("UserInit ==");
|
|
256
|
-
this.indent++;
|
|
257
|
-
this.line("/\\ Init \\* MessageRouter's init");
|
|
258
|
-
this.line("/\\ contextStates = [c \\in Contexts |-> InitialState]");
|
|
259
|
-
this.indent--;
|
|
260
|
-
this.line("");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
private addActions(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
264
|
-
this.line("\\* =============================================================================");
|
|
265
|
-
this.line("\\* Application-specific actions");
|
|
266
|
-
this.line("\\* =============================================================================");
|
|
267
|
-
this.line("");
|
|
268
|
-
|
|
269
|
-
if (analysis.handlers.length === 0) {
|
|
270
|
-
// No handlers found, keep the stub
|
|
271
|
-
this.line("\\* No message handlers found in codebase");
|
|
272
|
-
this.line("\\* State remains unchanged for all messages");
|
|
273
|
-
this.line("StateTransition(ctx, msgType) ==");
|
|
274
|
-
this.indent++;
|
|
275
|
-
this.line("UNCHANGED contextStates");
|
|
276
|
-
this.indent--;
|
|
277
|
-
this.line("");
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Generate state transition actions for each handler
|
|
282
|
-
this.line("\\* State transitions extracted from message handlers");
|
|
283
|
-
this.line("");
|
|
284
|
-
|
|
285
|
-
// Group handlers by message type
|
|
286
|
-
const handlersByType = new Map<string, typeof analysis.handlers>();
|
|
287
|
-
for (const handler of analysis.handlers) {
|
|
288
|
-
if (!handlersByType.has(handler.messageType)) {
|
|
289
|
-
handlersByType.set(handler.messageType, []);
|
|
290
|
-
}
|
|
291
|
-
handlersByType.get(handler.messageType)!.push(handler);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Generate an action for each message type
|
|
295
|
-
for (const [messageType, handlers] of handlersByType.entries()) {
|
|
296
|
-
this.generateHandlerAction(messageType, handlers, config);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Generate the main StateTransition action that dispatches to specific handlers
|
|
300
|
-
this.line("\\* Main state transition action");
|
|
301
|
-
this.line("StateTransition(ctx, msgType) ==");
|
|
302
|
-
this.indent++;
|
|
303
|
-
|
|
304
|
-
const messageTypes = Array.from(handlersByType.keys());
|
|
305
|
-
for (let i = 0; i < messageTypes.length; i++) {
|
|
306
|
-
const msgType = messageTypes[i];
|
|
307
|
-
const actionName = this.messageTypeToActionName(msgType);
|
|
308
|
-
|
|
309
|
-
if (i === 0) {
|
|
310
|
-
this.line(`IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
|
|
311
|
-
} else if (i === messageTypes.length - 1) {
|
|
312
|
-
this.line(`ELSE IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
|
|
313
|
-
this.line("ELSE UNCHANGED contextStates \\* Unknown message type");
|
|
314
|
-
} else {
|
|
315
|
-
this.line(`ELSE IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
this.indent--;
|
|
320
|
-
this.line("");
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
private generateHandlerAction(
|
|
324
|
-
messageType: string,
|
|
325
|
-
handlers: MessageHandler[],
|
|
326
|
-
config: VerificationConfig
|
|
327
|
-
): void {
|
|
328
|
-
const actionName = this.messageTypeToActionName(messageType);
|
|
329
|
-
|
|
330
|
-
this.line(`\\* Handler for ${messageType}`);
|
|
331
|
-
this.line(`${actionName}(ctx) ==`);
|
|
332
|
-
this.indent++;
|
|
333
|
-
|
|
334
|
-
// Collect all preconditions from all handlers
|
|
335
|
-
const allPreconditions = handlers.flatMap((h) => h.preconditions);
|
|
336
|
-
|
|
337
|
-
// Collect all assignments from all handlers for this message type
|
|
338
|
-
const allAssignments = handlers.flatMap((h) => h.assignments);
|
|
339
|
-
|
|
340
|
-
// Collect all postconditions from all handlers
|
|
341
|
-
const allPostconditions = handlers.flatMap((h) => h.postconditions);
|
|
342
|
-
|
|
343
|
-
// Emit preconditions first
|
|
344
|
-
if (allPreconditions.length > 0) {
|
|
345
|
-
for (const precondition of allPreconditions) {
|
|
346
|
-
const tlaExpr = this.tsExpressionToTLA(precondition.expression);
|
|
347
|
-
const comment = precondition.message ? ` \\* ${precondition.message}` : "";
|
|
348
|
-
this.line(`/\\ ${tlaExpr}${comment}`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Filter out null assignments and map them to valid values
|
|
353
|
-
const validAssignments = allAssignments
|
|
354
|
-
.filter((a) => {
|
|
355
|
-
if (a.value === null) {
|
|
356
|
-
// For null values, check if we can map to a valid value based on field config
|
|
357
|
-
const fieldConfig = config.state[a.field];
|
|
358
|
-
if (
|
|
359
|
-
fieldConfig &&
|
|
360
|
-
typeof fieldConfig === "object" &&
|
|
361
|
-
"values" in fieldConfig &&
|
|
362
|
-
fieldConfig.values
|
|
363
|
-
) {
|
|
364
|
-
// Use the last value as the "null" value (often "guest", "none", etc.)
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
// Skip null assignments if we can't map them
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
return true;
|
|
371
|
-
})
|
|
372
|
-
.map((a) => {
|
|
373
|
-
if (a.value === null) {
|
|
374
|
-
const fieldConfig = config.state[a.field];
|
|
375
|
-
if (
|
|
376
|
-
fieldConfig &&
|
|
377
|
-
typeof fieldConfig === "object" &&
|
|
378
|
-
"values" in fieldConfig &&
|
|
379
|
-
fieldConfig.values
|
|
380
|
-
) {
|
|
381
|
-
// Use the last value as the "null" value
|
|
382
|
-
const nullValue = fieldConfig.values[fieldConfig.values.length - 1];
|
|
383
|
-
return { ...a, value: nullValue };
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return a;
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
if (validAssignments.length === 0) {
|
|
390
|
-
// Handler exists but makes no state changes
|
|
391
|
-
if (allPreconditions.length === 0) {
|
|
392
|
-
this.line("\\* No state changes in handler");
|
|
393
|
-
}
|
|
394
|
-
this.line("/\\ UNCHANGED contextStates");
|
|
395
|
-
} else {
|
|
396
|
-
// Generate state updates
|
|
397
|
-
this.line("/\\ contextStates' = [contextStates EXCEPT");
|
|
398
|
-
this.indent++;
|
|
399
|
-
|
|
400
|
-
for (let i = 0; i < validAssignments.length; i++) {
|
|
401
|
-
const assignment = validAssignments[i];
|
|
402
|
-
const fieldName = this.sanitizeFieldName(assignment.field);
|
|
403
|
-
const value = this.assignmentValueToTLA(assignment.value);
|
|
404
|
-
const suffix = i < validAssignments.length - 1 ? "," : "";
|
|
405
|
-
|
|
406
|
-
this.line(`![ctx].${fieldName} = ${value}${suffix}`);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
this.indent--;
|
|
410
|
-
this.line("]");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Emit postconditions last
|
|
414
|
-
if (allPostconditions.length > 0) {
|
|
415
|
-
for (const postcondition of allPostconditions) {
|
|
416
|
-
const tlaExpr = this.tsExpressionToTLA(postcondition.expression, true);
|
|
417
|
-
const comment = postcondition.message ? ` \\* ${postcondition.message}` : "";
|
|
418
|
-
this.line(`/\\ ${tlaExpr}${comment}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
this.indent--;
|
|
423
|
-
this.line("");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Translate TypeScript expression to TLA+ syntax
|
|
428
|
-
* @param expr - TypeScript expression from requires() or ensures()
|
|
429
|
-
* @param isPrimed - If true, use contextStates' (for postconditions)
|
|
430
|
-
*/
|
|
431
|
-
private tsExpressionToTLA(expr: string, isPrimed: boolean = false): string {
|
|
432
|
-
let tla = expr;
|
|
433
|
-
|
|
434
|
-
// Replace state references with contextStates[ctx] or contextStates'[ctx]
|
|
435
|
-
const statePrefix = isPrimed ? "contextStates'[ctx]" : "contextStates[ctx]";
|
|
436
|
-
|
|
437
|
-
// Replace state.field.subfield with contextStates[ctx].field_subfield
|
|
438
|
-
tla = tla.replace(/state\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match, path) => {
|
|
439
|
-
return `${statePrefix}.${this.sanitizeFieldName(path)}`;
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Replace payload.field with payload.field (no change needed, but sanitize)
|
|
443
|
-
tla = tla.replace(/payload\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match, path) => {
|
|
444
|
-
return `payload.${this.sanitizeFieldName(path)}`;
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
// Replace comparison operators
|
|
448
|
-
tla = tla.replace(/===/g, "=");
|
|
449
|
-
tla = tla.replace(/!==/g, "#");
|
|
450
|
-
tla = tla.replace(/!=/g, "#");
|
|
451
|
-
tla = tla.replace(/==/g, "=");
|
|
452
|
-
|
|
453
|
-
// Replace logical operators
|
|
454
|
-
tla = tla.replace(/&&/g, "/\\");
|
|
455
|
-
tla = tla.replace(/\|\|/g, "\\/");
|
|
456
|
-
|
|
457
|
-
// Replace negation operator (careful with !== already handled)
|
|
458
|
-
// Match ! that's not part of !== or !=
|
|
459
|
-
tla = tla.replace(/!([^=])/g, "~$1");
|
|
460
|
-
tla = tla.replace(/!$/g, "~"); // Handle ! at end of string
|
|
461
|
-
|
|
462
|
-
// Replace boolean literals
|
|
463
|
-
tla = tla.replace(/\btrue\b/g, "TRUE");
|
|
464
|
-
tla = tla.replace(/\bfalse\b/g, "FALSE");
|
|
465
|
-
|
|
466
|
-
// Replace null
|
|
467
|
-
tla = tla.replace(/\bnull\b/g, "NULL");
|
|
468
|
-
|
|
469
|
-
// Replace less than / greater than comparisons
|
|
470
|
-
tla = tla.replace(/</g, "<");
|
|
471
|
-
tla = tla.replace(/>/g, ">");
|
|
472
|
-
tla = tla.replace(/<=/g, "<=");
|
|
473
|
-
tla = tla.replace(/>=/g, ">=");
|
|
474
|
-
|
|
475
|
-
return tla;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private messageTypeToActionName(messageType: string): string {
|
|
479
|
-
// Convert USER_LOGIN -> HandleUserLogin
|
|
480
|
-
return (
|
|
481
|
-
"Handle" +
|
|
482
|
-
messageType
|
|
483
|
-
.split("_")
|
|
484
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
485
|
-
.join("")
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
private assignmentValueToTLA(value: string | boolean | number | null): string {
|
|
490
|
-
if (typeof value === "boolean") {
|
|
491
|
-
return value ? "TRUE" : "FALSE";
|
|
492
|
-
}
|
|
493
|
-
if (typeof value === "number") {
|
|
494
|
-
return String(value);
|
|
495
|
-
}
|
|
496
|
-
if (value === null) {
|
|
497
|
-
return "NULL"; // Will need to handle this based on type
|
|
498
|
-
}
|
|
499
|
-
if (typeof value === "string") {
|
|
500
|
-
return `"${value}"`;
|
|
501
|
-
}
|
|
502
|
-
return "NULL";
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
private addRouteWithHandlers(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
506
|
-
this.line("\\* =============================================================================");
|
|
507
|
-
this.line("\\* Message Routing with State Transitions");
|
|
508
|
-
this.line("\\* =============================================================================");
|
|
509
|
-
this.line("");
|
|
510
|
-
|
|
511
|
-
if (analysis.handlers.length === 0) {
|
|
512
|
-
// No handlers, just use base routing
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
this.line("\\* Route a message and invoke its handler");
|
|
517
|
-
this.line("UserRouteMessage(msgIndex) ==");
|
|
518
|
-
this.indent++;
|
|
519
|
-
this.line("/\\ msgIndex \\in 1..Len(messages)");
|
|
520
|
-
this.line("/\\ LET msg == messages[msgIndex]");
|
|
521
|
-
this.line(' IN /\\ msg.status = "pending"');
|
|
522
|
-
this.line(" /\\ routingDepth' = routingDepth + 1");
|
|
523
|
-
this.line(" /\\ routingDepth < 5");
|
|
524
|
-
this.line(" /\\ \\E target \\in msg.targets :");
|
|
525
|
-
this.line(' /\\ IF target \\in Contexts /\\ ports[target] = "connected"');
|
|
526
|
-
this.line(" THEN \\* Successful delivery - route AND invoke handler");
|
|
527
|
-
this.line(
|
|
528
|
-
' /\\ messages\' = [messages EXCEPT ![msgIndex].status = "delivered"]'
|
|
529
|
-
);
|
|
530
|
-
this.line(" /\\ delivered' = delivered \\union {msg.id}");
|
|
531
|
-
this.line(
|
|
532
|
-
" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->"
|
|
533
|
-
);
|
|
534
|
-
this.line(" pendingRequests[id]]");
|
|
535
|
-
this.line(" /\\ time' = time + 1");
|
|
536
|
-
this.line(" /\\ StateTransition(target, msg.msgType)");
|
|
537
|
-
this.line(" ELSE \\* Port not connected - message fails");
|
|
538
|
-
this.line(
|
|
539
|
-
' /\\ messages\' = [messages EXCEPT ![msgIndex].status = "failed"]'
|
|
540
|
-
);
|
|
541
|
-
this.line(
|
|
542
|
-
" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->"
|
|
543
|
-
);
|
|
544
|
-
this.line(" pendingRequests[id]]");
|
|
545
|
-
this.line(" /\\ time' = time + 1");
|
|
546
|
-
this.line(" /\\ UNCHANGED <<delivered, contextStates>>");
|
|
547
|
-
this.line(" /\\ UNCHANGED ports");
|
|
548
|
-
this.indent--;
|
|
549
|
-
this.line("");
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private addNext(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
553
|
-
this.line("\\* Next state relation (extends MessageRouter)");
|
|
554
|
-
this.line("UserNext ==");
|
|
555
|
-
this.indent++;
|
|
556
|
-
|
|
557
|
-
if (analysis.handlers.length > 0) {
|
|
558
|
-
// Use integrated routing + handlers
|
|
559
|
-
this.line("\\/ \\E c \\in Contexts : ConnectPort(c) /\\ UNCHANGED contextStates");
|
|
560
|
-
this.line("\\/ \\E c \\in Contexts : DisconnectPort(c) /\\ UNCHANGED contextStates");
|
|
561
|
-
this.line(
|
|
562
|
-
"\\/ \\E src \\in Contexts : \\E targetSet \\in (SUBSET Contexts \\ {{}}) : \\E tab \\in 0..MaxTabId : \\E msgType \\in UserMessageTypes :"
|
|
563
|
-
);
|
|
564
|
-
this.indent++;
|
|
565
|
-
this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED contextStates");
|
|
566
|
-
this.indent--;
|
|
567
|
-
this.line("\\/ \\E i \\in 1..Len(messages) : UserRouteMessage(i)");
|
|
568
|
-
this.line("\\/ CompleteRouting /\\ UNCHANGED contextStates");
|
|
569
|
-
this.line("\\/ \\E i \\in 1..Len(messages) : TimeoutMessage(i) /\\ UNCHANGED contextStates");
|
|
570
|
-
} else {
|
|
571
|
-
// No handlers, all actions preserve contextStates
|
|
572
|
-
this.line("\\/ Next /\\ UNCHANGED contextStates");
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
this.indent--;
|
|
576
|
-
this.line("");
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
private addSpec(): void {
|
|
580
|
-
this.line("\\* Specification");
|
|
581
|
-
this.line("UserSpec == UserInit /\\ [][UserNext]_allVars /\\ WF_allVars(UserNext)");
|
|
582
|
-
this.line("");
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
private addInvariants(config: VerificationConfig, analysis: CodebaseAnalysis): void {
|
|
586
|
-
this.line("\\* =============================================================================");
|
|
587
|
-
this.line("\\* Application Invariants");
|
|
588
|
-
this.line("\\* =============================================================================");
|
|
589
|
-
this.line("");
|
|
590
|
-
|
|
591
|
-
this.line("\\* TypeOK and NoRoutingLoops are inherited from MessageRouter");
|
|
592
|
-
this.line("");
|
|
593
|
-
|
|
594
|
-
this.line("\\* Application state type invariant");
|
|
595
|
-
this.line("UserStateTypeInvariant ==");
|
|
596
|
-
this.indent++;
|
|
597
|
-
this.line("\\A ctx \\in Contexts :");
|
|
598
|
-
this.indent++;
|
|
599
|
-
this.line("contextStates[ctx] \\in State");
|
|
600
|
-
this.indent--;
|
|
601
|
-
this.indent--;
|
|
602
|
-
this.line("");
|
|
603
|
-
|
|
604
|
-
this.line("\\* State constraint to bound state space");
|
|
605
|
-
this.line("StateConstraint ==");
|
|
606
|
-
this.indent++;
|
|
607
|
-
this.line("Len(messages) <= MaxMessages");
|
|
608
|
-
this.indent--;
|
|
609
|
-
this.line("");
|
|
610
|
-
|
|
611
|
-
this.line("=============================================================================");
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
private fieldConfigToTLAType(
|
|
615
|
-
fieldPath: string,
|
|
616
|
-
fieldConfig: any,
|
|
617
|
-
config: VerificationConfig
|
|
618
|
-
): string {
|
|
619
|
-
if ("type" in fieldConfig) {
|
|
620
|
-
if (fieldConfig.type === "boolean") {
|
|
621
|
-
return "BOOLEAN";
|
|
622
|
-
}
|
|
623
|
-
if (fieldConfig.type === "enum" && fieldConfig.values) {
|
|
624
|
-
const values = fieldConfig.values.map((v: string) => `"${v}"`).join(", ");
|
|
625
|
-
return `{${values}}`;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if ("maxLength" in fieldConfig) {
|
|
630
|
-
// Array type - represented as sequence with bounded length
|
|
631
|
-
const constName = this.fieldToConstName(fieldPath);
|
|
632
|
-
return `Seq(Value)`; // Simplified - would need element type
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if ("min" in fieldConfig && "max" in fieldConfig) {
|
|
636
|
-
// Number type
|
|
637
|
-
const constName = this.fieldToConstName(fieldPath);
|
|
638
|
-
const min = fieldConfig.min || 0;
|
|
639
|
-
const max = fieldConfig.max || 100;
|
|
640
|
-
return `${min}..${max}`;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if ("values" in fieldConfig) {
|
|
644
|
-
if (fieldConfig.values && Array.isArray(fieldConfig.values)) {
|
|
645
|
-
// String with concrete values
|
|
646
|
-
const values = fieldConfig.values.map((v: string) => `"${v}"`).join(", ");
|
|
647
|
-
return `{${values}}`;
|
|
648
|
-
}
|
|
649
|
-
if (fieldConfig.abstract) {
|
|
650
|
-
// Abstract string
|
|
651
|
-
return "STRING";
|
|
652
|
-
}
|
|
653
|
-
// Needs configuration
|
|
654
|
-
return "STRING";
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if ("maxSize" in fieldConfig) {
|
|
658
|
-
// Map with bounded key set
|
|
659
|
-
return "[Keys -> Value]";
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return "Value"; // Generic fallback
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
private getInitialValue(fieldConfig: any): string {
|
|
666
|
-
if ("type" in fieldConfig) {
|
|
667
|
-
if (fieldConfig.type === "boolean") {
|
|
668
|
-
return "FALSE";
|
|
669
|
-
}
|
|
670
|
-
if (fieldConfig.type === "enum" && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
671
|
-
return `"${fieldConfig.values[0]}"`;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if ("maxLength" in fieldConfig) {
|
|
676
|
-
return "<<>>"; // Empty sequence
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if ("min" in fieldConfig) {
|
|
680
|
-
return String(fieldConfig.min || 0);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
if ("values" in fieldConfig && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
684
|
-
return `"${fieldConfig.values[0]}"`;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if ("maxSize" in fieldConfig) {
|
|
688
|
-
// Map with all keys mapped to default value
|
|
689
|
-
return '[k \\in Keys |-> "v1"]';
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return "0"; // Default fallback
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
private fieldToConstName(fieldPath: string): string {
|
|
696
|
-
return fieldPath.replace(/\./g, "_").toUpperCase();
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
private sanitizeFieldName(fieldPath: string): string {
|
|
700
|
-
return fieldPath.replace(/\./g, "_");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
private line(content: string): void {
|
|
704
|
-
if (content === "") {
|
|
705
|
-
this.lines.push("");
|
|
706
|
-
} else {
|
|
707
|
-
const indentation = " ".repeat(this.indent);
|
|
708
|
-
this.lines.push(indentation + content);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
export function generateTLA(
|
|
714
|
-
config: VerificationConfig,
|
|
715
|
-
analysis: CodebaseAnalysis
|
|
716
|
-
): { spec: string; cfg: string } {
|
|
717
|
-
const generator = new TLAGenerator();
|
|
718
|
-
return generator.generate(config, analysis);
|
|
719
|
-
}
|