tla-ts 0.0.2 → 0.0.3-dev.763cc71
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 +56 -10
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +1 -1
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +20 -2
- package/dist/run-cli.js +20 -2
- package/dist/transfer.cjs +12 -12
- package/dist/transfer.js +12 -12
- package/package.json +3 -1
- package/src/ast.ts +43 -3
- package/src/check.ts +43 -10
- package/src/cli-options.ts +58 -0
- package/src/errors.ts +37 -1
- package/src/evaluate.ts +3 -1
- package/src/lexer.ts +6 -1
- package/src/obligation-action.ts +384 -0
- package/src/obligation-anon.ts +575 -0
- package/src/obligation-deref.ts +637 -0
- package/src/obligation-expression.ts +1060 -0
- package/src/obligation-format.ts +406 -0
- package/src/obligation-generate.ts +1074 -0
- package/src/obligation-levels.ts +329 -0
- package/src/obligation-module.ts +1042 -0
- package/src/obligation-printer.ts +889 -0
- package/src/obligation-proof.ts +76 -0
- package/src/obligation-source.ts +1084 -0
- package/src/obligation-tuples.ts +184 -0
- package/src/obligations.ts +316 -0
- package/src/parallel-worker.ts +26 -7
- package/src/parser.ts +285 -52
- package/src/run-cli.ts +295 -27
- package/src/standard-levels.ts +3 -3
- package/src/transfer.ts +22 -1
- package/src/values.ts +53 -20
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// tlapm's E_tuply_declarations: declarations such as <<x, y>> \in S become declarations of names.
|
|
2
|
+
|
|
3
|
+
import type { Bound, BoundDomain, Defn, Expr } from "./obligation-expression";
|
|
4
|
+
|
|
5
|
+
export type TuplyBound =
|
|
6
|
+
| { kind: "Name"; name: string; domain: BoundDomain }
|
|
7
|
+
| { kind: "Tuple"; names: string[]; domain: Expr };
|
|
8
|
+
|
|
9
|
+
const getOpaque = (name: string): Expr => ({ kind: "Opaque", name });
|
|
10
|
+
|
|
11
|
+
const getNamesTuple = (names: string[]): Expr => ({
|
|
12
|
+
kind: "Tuple",
|
|
13
|
+
elements: names.map(getOpaque),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const getEquality = (left: Expr, right: Expr): Expr => ({
|
|
17
|
+
kind: "Apply",
|
|
18
|
+
operator: { kind: "Internal", builtin: "Eq" },
|
|
19
|
+
args: [left, right],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const getUnbounded = (names: string[]): Bound[] =>
|
|
23
|
+
names.map((name) => ({ name, level: "Constant", domain: { kind: "NoDomain" } }));
|
|
24
|
+
|
|
25
|
+
const getFactors = (domain: Expr | undefined, names: string[]) =>
|
|
26
|
+
domain?.kind === "Product" && domain.factors.length === names.length ? domain.factors : undefined;
|
|
27
|
+
|
|
28
|
+
// Each name is the matching item of the tuple: x == t[1], y == t[2].
|
|
29
|
+
const getItemDefinitions = (tuple: string, names: string[]): Defn[] =>
|
|
30
|
+
names.map((name, index) => ({
|
|
31
|
+
kind: "Operator",
|
|
32
|
+
name,
|
|
33
|
+
body: {
|
|
34
|
+
kind: "FcnApp",
|
|
35
|
+
function: getOpaque(tuple),
|
|
36
|
+
args: [{ kind: "Num", integer: String(index + 1), fraction: "" }],
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const getFactorBounds = (names: string[], factors: Expr[]): Bound[] =>
|
|
41
|
+
names.map((name, index) => ({
|
|
42
|
+
name,
|
|
43
|
+
level: "Constant",
|
|
44
|
+
domain: { kind: "Domain", expression: factors[index] },
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const getNamedBound = (bound: Extract<TuplyBound, { kind: "Name" }>): Bound => ({
|
|
48
|
+
name: bound.name,
|
|
49
|
+
level: "Constant",
|
|
50
|
+
domain: bound.domain,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// \E <<x, y>> \in A \X B: P becomes \E x \in A, y \in B: P, and \E <<x, y>> \in S: P becomes
|
|
54
|
+
// \E x, y: <<x, y>> \in S /\ P. Each domain quantifies separately, the last one innermost.
|
|
55
|
+
export const expandQuant = (
|
|
56
|
+
quantifier: "Forall" | "Exists",
|
|
57
|
+
bounds: TuplyBound[],
|
|
58
|
+
body: Expr,
|
|
59
|
+
): Expr => {
|
|
60
|
+
const groups: TuplyBound[][] = [];
|
|
61
|
+
bounds.forEach((bound) => {
|
|
62
|
+
if (bound.kind === "Name" && bound.domain.kind === "Ditto") groups.at(-1)?.push(bound);
|
|
63
|
+
else groups.push([bound]);
|
|
64
|
+
});
|
|
65
|
+
return groups.reduceRight((inner: Expr, group): Expr => {
|
|
66
|
+
const first = group[0];
|
|
67
|
+
if (first.kind === "Name") {
|
|
68
|
+
return {
|
|
69
|
+
kind: "Quant",
|
|
70
|
+
quantifier,
|
|
71
|
+
bounds: group.flatMap((bound) => (bound.kind === "Name" ? [getNamedBound(bound)] : [])),
|
|
72
|
+
body: inner,
|
|
73
|
+
location: inner.location,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const factors = getFactors(first.domain, first.names);
|
|
77
|
+
if (factors) {
|
|
78
|
+
return {
|
|
79
|
+
kind: "Quant",
|
|
80
|
+
quantifier,
|
|
81
|
+
bounds: getFactorBounds(first.names, factors),
|
|
82
|
+
body: inner,
|
|
83
|
+
location: inner.location,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const membership: Expr = {
|
|
87
|
+
kind: "Apply",
|
|
88
|
+
operator: { kind: "Internal", builtin: quantifier === "Exists" ? "Mem" : "Notmem" },
|
|
89
|
+
args: [getNamesTuple(first.names), first.domain],
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
kind: "Quant",
|
|
93
|
+
quantifier,
|
|
94
|
+
bounds: getUnbounded(first.names),
|
|
95
|
+
body: {
|
|
96
|
+
kind: "List",
|
|
97
|
+
bullet: quantifier === "Exists" ? "And" : "Or",
|
|
98
|
+
items: [membership, inner],
|
|
99
|
+
},
|
|
100
|
+
location: inner.location,
|
|
101
|
+
};
|
|
102
|
+
}, body);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// CHOOSE <<x, y>> \in A \X B: P binds t \in A \X B with x == t[1], y == t[2], and any other
|
|
106
|
+
// domain binds t with \E x, y: t = <<x, y>> /\ P.
|
|
107
|
+
const getChooseBody = (
|
|
108
|
+
name: string,
|
|
109
|
+
names: string[],
|
|
110
|
+
domain: Expr | undefined,
|
|
111
|
+
body: Expr,
|
|
112
|
+
): Expr =>
|
|
113
|
+
getFactors(domain, names)
|
|
114
|
+
? { kind: "Let", definitions: getItemDefinitions(name, names), body }
|
|
115
|
+
: {
|
|
116
|
+
kind: "Quant",
|
|
117
|
+
quantifier: "Exists",
|
|
118
|
+
bounds: getUnbounded(names),
|
|
119
|
+
body: {
|
|
120
|
+
kind: "List",
|
|
121
|
+
bullet: "And",
|
|
122
|
+
items: [getEquality(getOpaque(name), getNamesTuple(names)), body],
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const expandChoose = (names: string[], domain: Expr | undefined, body: Expr): Expr => {
|
|
127
|
+
const name = `choose#${names[0]}`;
|
|
128
|
+
return { kind: "Choose", name, domain, body: getChooseBody(name, names, domain, body) };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const expandSetSt = (names: string[], domain: Expr, body: Expr): Expr => {
|
|
132
|
+
const name = `setst#${names[0]}`;
|
|
133
|
+
return { kind: "SetSt", name, domain, body: getChooseBody(name, names, domain, body) };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// A set or function constructor binds each tuple as fcn#x, with its names defined in a LET around
|
|
137
|
+
// the value. A set constructor over a matching product binds the names to its factors instead.
|
|
138
|
+
const expandConstructorBounds = (bounds: TuplyBound[], value: Expr, isSetOf: boolean) => {
|
|
139
|
+
const expanded = bounds.map((bound): { bounds: Bound[]; definitions: Defn[] } => {
|
|
140
|
+
if (bound.kind === "Name") return { bounds: [getNamedBound(bound)], definitions: [] };
|
|
141
|
+
const factors = getFactors(bound.domain, bound.names);
|
|
142
|
+
if (factors && isSetOf) {
|
|
143
|
+
return { bounds: getFactorBounds(bound.names, factors), definitions: [] };
|
|
144
|
+
}
|
|
145
|
+
const name = `fcn#${bound.names[0]}`;
|
|
146
|
+
const fcnBound: Bound = {
|
|
147
|
+
name,
|
|
148
|
+
level: "Constant",
|
|
149
|
+
domain: { kind: "Domain", expression: bound.domain },
|
|
150
|
+
};
|
|
151
|
+
if (factors) return { bounds: [fcnBound], definitions: getItemDefinitions(name, bound.names) };
|
|
152
|
+
const choice = `choose#${name}`;
|
|
153
|
+
return {
|
|
154
|
+
bounds: [fcnBound],
|
|
155
|
+
definitions: [
|
|
156
|
+
{
|
|
157
|
+
kind: "Operator",
|
|
158
|
+
name: choice,
|
|
159
|
+
body: expandChoose(
|
|
160
|
+
bound.names,
|
|
161
|
+
undefined,
|
|
162
|
+
getEquality(getOpaque(name), getNamesTuple(bound.names)),
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
...getItemDefinitions(choice, bound.names),
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const definitions = expanded.flatMap((item) => item.definitions);
|
|
170
|
+
return {
|
|
171
|
+
bounds: expanded.flatMap((item) => item.bounds),
|
|
172
|
+
value: definitions.length === 0 ? value : { kind: "Let" as const, definitions, body: value },
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const expandSetOf = (element: Expr, bounds: TuplyBound[]): Expr => {
|
|
177
|
+
const expanded = expandConstructorBounds(bounds, element, true);
|
|
178
|
+
return { kind: "SetOf", element: expanded.value, bounds: expanded.bounds };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const expandFcn = (bounds: TuplyBound[], body: Expr): Expr => {
|
|
182
|
+
const expanded = expandConstructorBounds(bounds, body, false);
|
|
183
|
+
return { kind: "Fcn", bounds: expanded.bounds, body: expanded.value };
|
|
184
|
+
};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Generates the proof obligations tlapm prints for a module, in tlapm's order and layout.
|
|
2
|
+
|
|
3
|
+
import { TlaProofError, TlaSyntaxError, type SyntaxFailure } from "./errors";
|
|
4
|
+
import { createAnonymizer } from "./obligation-anon";
|
|
5
|
+
import {
|
|
6
|
+
getAtPrefix,
|
|
7
|
+
getErrorNote,
|
|
8
|
+
getLocusPosition,
|
|
9
|
+
getLocusPrefix,
|
|
10
|
+
type Locus,
|
|
11
|
+
type ObligationWarning,
|
|
12
|
+
} from "./obligation-expression";
|
|
13
|
+
import { createGenerator, type LineRange } from "./obligation-generate";
|
|
14
|
+
import {
|
|
15
|
+
compareNames,
|
|
16
|
+
expandInstance,
|
|
17
|
+
flatten,
|
|
18
|
+
generate,
|
|
19
|
+
getDependencies,
|
|
20
|
+
normalize,
|
|
21
|
+
STANDARD_MODULES,
|
|
22
|
+
type ModuleSummary,
|
|
23
|
+
type Mule,
|
|
24
|
+
type TheoremSummary,
|
|
25
|
+
type Pipeline,
|
|
26
|
+
} from "./obligation-module";
|
|
27
|
+
import { formatObligation } from "./obligation-printer";
|
|
28
|
+
import type { Counter } from "./obligation-proof";
|
|
29
|
+
import { createSource } from "./obligation-source";
|
|
30
|
+
import { tokenize } from "./lexer";
|
|
31
|
+
import { parseTlapmModule } from "./parser";
|
|
32
|
+
import type { Module } from "./ast";
|
|
33
|
+
|
|
34
|
+
export interface ObligationSource {
|
|
35
|
+
file: string;
|
|
36
|
+
source: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PrintedObligation {
|
|
40
|
+
id: number;
|
|
41
|
+
location: string;
|
|
42
|
+
text: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ModuleObligations {
|
|
46
|
+
name: string;
|
|
47
|
+
location: Locus | undefined;
|
|
48
|
+
obligations: PrintedObligation[];
|
|
49
|
+
summary: ModuleSummary;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const WHOLE_MODULE: LineRange = { start: 0, end: Infinity };
|
|
53
|
+
|
|
54
|
+
interface ObligationOptions {
|
|
55
|
+
lines?: LineRange;
|
|
56
|
+
onWarning?: (warning: ObligationWarning) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const MODULE_HEADER = /-{4,} *MODULE/;
|
|
60
|
+
|
|
61
|
+
const END_FAILURE = "Unexpected EOF";
|
|
62
|
+
|
|
63
|
+
const formatLocus = (locus: Locus | undefined) =>
|
|
64
|
+
locus ? `${locus.startLine}:${locus.startColumn}:${locus.endLine}:${locus.endColumn}` : "";
|
|
65
|
+
|
|
66
|
+
const findTab = (source: string) => {
|
|
67
|
+
const tokens = tokenize(source);
|
|
68
|
+
const lines = source.split("\n");
|
|
69
|
+
const endLine = tokens.at(-1)?.line ?? lines.length;
|
|
70
|
+
const moduleEnd =
|
|
71
|
+
lines.slice(0, endLine - 1).reduce((total, line) => total + line.length + 1, 0) +
|
|
72
|
+
lines[endLine - 1].indexOf("====");
|
|
73
|
+
const offset = source.indexOf("\t", source.search(MODULE_HEADER));
|
|
74
|
+
if (offset === -1 || offset >= moduleEnd) return undefined;
|
|
75
|
+
const line = source.slice(0, offset).split("\n").length;
|
|
76
|
+
const column = offset - source.lastIndexOf("\n", offset - 1);
|
|
77
|
+
const last = tokens.findLast(
|
|
78
|
+
(token) =>
|
|
79
|
+
token.kind !== "End" && (token.line < line || (token.line === line && token.column < column)),
|
|
80
|
+
);
|
|
81
|
+
const end: SyntaxFailure = {
|
|
82
|
+
reason: END_FAILURE,
|
|
83
|
+
line: last?.line ?? line,
|
|
84
|
+
column: last ? last.endColumn + 1 : column,
|
|
85
|
+
isEnd: true,
|
|
86
|
+
};
|
|
87
|
+
return { offset, line, column, end };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const readModule = <Value>(read: () => Value): Value | SyntaxFailure => {
|
|
91
|
+
try {
|
|
92
|
+
return read();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!(error instanceof TlaSyntaxError) || !error.failure) throw error;
|
|
95
|
+
return error.failure;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isSyntaxFailure = (value: unknown): value is SyntaxFailure =>
|
|
100
|
+
typeof value === "object" && value !== null && "reason" in value;
|
|
101
|
+
|
|
102
|
+
const parseSource = (source: string): Module | SyntaxFailure[] => {
|
|
103
|
+
const tab = readModule(() => findTab(source));
|
|
104
|
+
if (isSyntaxFailure(tab)) return [tab];
|
|
105
|
+
const parsed = readModule(() => parseTlapmModule(tab ? source.slice(0, tab.offset) : source));
|
|
106
|
+
if (tab && (!isSyntaxFailure(parsed) || parsed.isEnd)) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
reason: "Unexpected TAB character. TLAPS does not handle TAB characters in source files.",
|
|
110
|
+
line: tab.line,
|
|
111
|
+
column: tab.column,
|
|
112
|
+
isEnd: false,
|
|
113
|
+
},
|
|
114
|
+
isSyntaxFailure(parsed) && parsed.reason === END_FAILURE ? parsed : tab.end,
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
return isSyntaxFailure(parsed) ? [parsed] : parsed;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Module.Save.parse_file warns when a module's name differs from its file's, then renames it.
|
|
121
|
+
const loadMule = (
|
|
122
|
+
input: ObligationSource,
|
|
123
|
+
counter: Counter,
|
|
124
|
+
isMain: boolean,
|
|
125
|
+
onWarning: (warning: ObligationWarning) => void,
|
|
126
|
+
citation: string,
|
|
127
|
+
): Mule => {
|
|
128
|
+
if (!MODULE_HEADER.test(input.source)) throw new TlaProofError("No tokens in file");
|
|
129
|
+
const parsed = parseSource(input.source);
|
|
130
|
+
if (Array.isArray(parsed)) {
|
|
131
|
+
parsed.forEach((failure) => {
|
|
132
|
+
const text = `File ${JSON.stringify(input.file)}, line ${failure.line}, character ${failure.column}\n${failure.reason}`;
|
|
133
|
+
onWarning({ text, channel: "stderr" });
|
|
134
|
+
onWarning({ text: `${text}\n`, channel: "toolboxOnly" });
|
|
135
|
+
});
|
|
136
|
+
const message = `Could not parse ${JSON.stringify(input.file)} successfully.`;
|
|
137
|
+
onWarning({ text: `${citation}Error: ${message}`, channel: "stderr" });
|
|
138
|
+
throw new TlaProofError(message, { failure: "Module.Parser.parse_file" });
|
|
139
|
+
}
|
|
140
|
+
const mule = createSource(input.file, input.source, counter, isMain).convertModule(parsed);
|
|
141
|
+
const fileName = input.file.slice(input.file.lastIndexOf("/") + 1);
|
|
142
|
+
const name = fileName.replace(/\.tla$/, "");
|
|
143
|
+
if (mule.name === name) return mule;
|
|
144
|
+
onWarning({
|
|
145
|
+
text: `${getLocusPrefix(mule.location)}Warning: module name ${JSON.stringify(mule.name)} does not match file name ${JSON.stringify(fileName)}.`,
|
|
146
|
+
channel: "toolbox",
|
|
147
|
+
});
|
|
148
|
+
return { ...mule, name };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Module.Dep.schedule: dependencies first, visiting modules and their dependencies by name.
|
|
152
|
+
const schedule = (modules: Map<string, Mule>) => {
|
|
153
|
+
const dependencies = new Map(
|
|
154
|
+
[...modules].map(([name, module]) => [name, getDependencies(module).dependencies]),
|
|
155
|
+
);
|
|
156
|
+
const seen = new Set<string>();
|
|
157
|
+
const order: Mule[] = [];
|
|
158
|
+
const visit = (name: string) => {
|
|
159
|
+
if (seen.has(name)) return;
|
|
160
|
+
seen.add(name);
|
|
161
|
+
const module = modules.get(name);
|
|
162
|
+
if (!module) throw new TlaProofError(`Module "${name}" not found`);
|
|
163
|
+
(dependencies.get(name) ?? []).forEach(visit);
|
|
164
|
+
order.push(module);
|
|
165
|
+
};
|
|
166
|
+
[...modules.keys()].sort(compareNames).forEach(visit);
|
|
167
|
+
return order;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const formatProofs = (kind: "missing" | "omitted", loci: Locus[], before: number) =>
|
|
171
|
+
loci.length === 0
|
|
172
|
+
? []
|
|
173
|
+
: [
|
|
174
|
+
` ${kind}_proofs_count = ${loci.length}`,
|
|
175
|
+
...loci.map(
|
|
176
|
+
(locus, index) => ` ${kind}_proof_${before + index + 1} at ${getLocusPosition(locus)}`,
|
|
177
|
+
),
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
const formatTheoremSummary = (theorem: TheoremSummary, missing: number, omitted: number) => {
|
|
181
|
+
const location = theorem.location;
|
|
182
|
+
const start = location
|
|
183
|
+
? getLocusPosition({
|
|
184
|
+
...location,
|
|
185
|
+
endLine: location.startLine,
|
|
186
|
+
endColumn: location.startColumn,
|
|
187
|
+
})
|
|
188
|
+
: "<unknown location>";
|
|
189
|
+
return [
|
|
190
|
+
` ---- incomplete proof of theorem at ${start} ----`,
|
|
191
|
+
...formatProofs("missing", theorem.absent, missing),
|
|
192
|
+
...formatProofs("omitted", theorem.omitted, omitted),
|
|
193
|
+
" ====",
|
|
194
|
+
];
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const formatSummary = (result: ModuleObligations) => {
|
|
198
|
+
const summary = result.summary;
|
|
199
|
+
const theorems = summary.theorems.reduce(
|
|
200
|
+
(state, theorem) =>
|
|
201
|
+
theorem.absent.length + theorem.omitted.length === 0
|
|
202
|
+
? state
|
|
203
|
+
: {
|
|
204
|
+
lines: [...state.lines, ...formatTheoremSummary(theorem, state.missing, state.omitted)],
|
|
205
|
+
missing: state.missing + theorem.absent.length,
|
|
206
|
+
omitted: state.omitted + theorem.omitted.length,
|
|
207
|
+
},
|
|
208
|
+
{ lines: new Array<string>(), missing: 0, omitted: 0 },
|
|
209
|
+
);
|
|
210
|
+
return [
|
|
211
|
+
`---- summary of module ${JSON.stringify(result.name)} ----`,
|
|
212
|
+
` obligations_count = ${summary.total}`,
|
|
213
|
+
...(summary.absent.length > 0 ? [` missing_proofs_count = ${summary.absent.length}`] : []),
|
|
214
|
+
...(summary.omitted.length > 0 ? [` omitted_proofs_count = ${summary.omitted.length}`] : []),
|
|
215
|
+
...theorems.lines,
|
|
216
|
+
"====",
|
|
217
|
+
].join("\n");
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Each main module is one file given to tlapm. They load into one context and report in schedule
|
|
221
|
+
// order, each numbering its obligations from 1.
|
|
222
|
+
export const getObligations = (
|
|
223
|
+
mains: ObligationSource[],
|
|
224
|
+
loadModule: (name: string) => ObligationSource | undefined,
|
|
225
|
+
options: ObligationOptions = {},
|
|
226
|
+
): ModuleObligations[] => {
|
|
227
|
+
const lines = options.lines ?? WHOLE_MODULE;
|
|
228
|
+
const onWarning = options.onWarning ?? (() => {});
|
|
229
|
+
const counter = { value: 0 };
|
|
230
|
+
const modules = new Map<string, Mule>(STANDARD_MODULES);
|
|
231
|
+
mains.forEach((main) => {
|
|
232
|
+
const module = {
|
|
233
|
+
...loadMule(main, counter, true, onWarning, 'File "<unknown>":\n'),
|
|
234
|
+
isImportant: true,
|
|
235
|
+
};
|
|
236
|
+
modules.set(module.name, module);
|
|
237
|
+
});
|
|
238
|
+
// Module.Save.complete_load: breadth first from every known module, by name.
|
|
239
|
+
const queue = [...modules.values()].sort((left, right) => compareNames(left.name, right.name));
|
|
240
|
+
// A missing module is fine only if some loaded module declares it as a submodule.
|
|
241
|
+
const requested = new Map<string, Locus | undefined>();
|
|
242
|
+
const found = new Set<string>();
|
|
243
|
+
for (let index = 0; index < queue.length; index++) {
|
|
244
|
+
const result = getDependencies(queue[index]);
|
|
245
|
+
result.submodules.forEach((submodule, name) => {
|
|
246
|
+
found.add(name);
|
|
247
|
+
if (!modules.has(name)) modules.set(name, submodule);
|
|
248
|
+
});
|
|
249
|
+
// A file named like a standard module replaces it, and is read again for each module citing it.
|
|
250
|
+
for (const name of result.dependencies) {
|
|
251
|
+
if (modules.has(name) && !STANDARD_MODULES.has(name)) continue;
|
|
252
|
+
const input = loadModule(name);
|
|
253
|
+
if (!input) {
|
|
254
|
+
if (!modules.has(name) && !requested.has(name))
|
|
255
|
+
requested.set(name, result.locations.get(name));
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const loaded = loadMule(
|
|
259
|
+
input,
|
|
260
|
+
counter,
|
|
261
|
+
false,
|
|
262
|
+
onWarning,
|
|
263
|
+
getAtPrefix(result.locations.get(name)),
|
|
264
|
+
);
|
|
265
|
+
modules.set(name, loaded);
|
|
266
|
+
queue.push(loaded);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const missing = [...requested.keys()].filter((name) => !found.has(name)).sort(compareNames);
|
|
270
|
+
missing.forEach((name) =>
|
|
271
|
+
onWarning(getErrorNote(requested.get(name), `Unknown module ${JSON.stringify(name)}`)),
|
|
272
|
+
);
|
|
273
|
+
if (missing.length > 0)
|
|
274
|
+
throw new TlaProofError(`Unknown module ${JSON.stringify(missing[0])}`, {
|
|
275
|
+
failure: "Module.Parser.load_module",
|
|
276
|
+
});
|
|
277
|
+
const pipeline: Pipeline = {
|
|
278
|
+
anonymizer: createAnonymizer(
|
|
279
|
+
counter,
|
|
280
|
+
(scope, definition) => expandInstance(pipeline, scope, definition),
|
|
281
|
+
onWarning,
|
|
282
|
+
),
|
|
283
|
+
generator: createGenerator(counter, new Set(mains.map((main) => main.file)), lines, onWarning),
|
|
284
|
+
salts: { value: 0 },
|
|
285
|
+
latestModules: new Map(),
|
|
286
|
+
onWarning,
|
|
287
|
+
};
|
|
288
|
+
const order = schedule(modules).map((module) =>
|
|
289
|
+
module.isStandard ? module : flatten(modules, module, new Set()),
|
|
290
|
+
);
|
|
291
|
+
// Instances see each module as flattened, and as normalized once its turn has come.
|
|
292
|
+
const normalized = new Map(order.map((module) => [module.name, module]));
|
|
293
|
+
return order.flatMap((module): ModuleObligations[] => {
|
|
294
|
+
if (module.isStandard) return [];
|
|
295
|
+
const result = normalize(pipeline, normalized, undefined, module);
|
|
296
|
+
if (!module.isImportant) return [];
|
|
297
|
+
const generated = generate(pipeline, [], result);
|
|
298
|
+
// tlapm numbers the obligations, then drops the omitted ones and those outside the lines.
|
|
299
|
+
const obligations = generated.obligations.flatMap((obligation, index): PrintedObligation[] => {
|
|
300
|
+
const location = obligation.location;
|
|
301
|
+
if (
|
|
302
|
+
obligation.kind.kind === "Omitted" ||
|
|
303
|
+
(location && (location.startLine < lines.start || location.endLine > lines.end))
|
|
304
|
+
)
|
|
305
|
+
return [];
|
|
306
|
+
const text =
|
|
307
|
+
obligation.kind.kind === "Error"
|
|
308
|
+
? obligation.kind.message
|
|
309
|
+
: `${formatObligation(obligation.sequent)}\n`;
|
|
310
|
+
return [{ id: index + 1, location: formatLocus(location), text }];
|
|
311
|
+
});
|
|
312
|
+
return [
|
|
313
|
+
{ name: module.name, location: module.location, obligations, summary: generated.summary },
|
|
314
|
+
];
|
|
315
|
+
});
|
|
316
|
+
};
|
package/src/parallel-worker.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface Pending {
|
|
|
15
15
|
states: Value[][];
|
|
16
16
|
origins: number[];
|
|
17
17
|
fingerprints: number[];
|
|
18
|
+
fingerprintStates: Value[][];
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface PeerMessage {
|
|
@@ -123,6 +124,9 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
123
124
|
const decode = createStateDecoder(environment.variables.length);
|
|
124
125
|
const table = createFingerprintTable();
|
|
125
126
|
const known = createFingerprintTable();
|
|
127
|
+
// Under symmetry, a successor the worker generated before has the representative it had then,
|
|
128
|
+
// so the worker skips the search for one.
|
|
129
|
+
const generatedStates = search.isSymmetric ? createFingerprintTable() : undefined;
|
|
126
130
|
const fingerprint = new Int32Array(2);
|
|
127
131
|
const inbox = createInbox();
|
|
128
132
|
const peers = start.ports.flatMap((port, index) => (port ? [{ port, index }] : []));
|
|
@@ -137,11 +141,12 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
137
141
|
const send = (index: number, message: PeerMessage, transfer: unknown[]) =>
|
|
138
142
|
start.ports[index]?.postMessage(message, transfer);
|
|
139
143
|
|
|
140
|
-
const expand = (states: Value[][], next: Value[][]) => {
|
|
144
|
+
const expand = (states: Value[][], next: Value[][], nextFingerprintStates: Value[][]) => {
|
|
141
145
|
const pending: Pending[] = Array.from({ length: count }, () => ({
|
|
142
146
|
states: [],
|
|
143
147
|
origins: [],
|
|
144
148
|
fingerprints: [],
|
|
149
|
+
fingerprintStates: [],
|
|
145
150
|
}));
|
|
146
151
|
const statistics = { generated: 0 };
|
|
147
152
|
const newStates = new Int32Array(states.length);
|
|
@@ -153,8 +158,13 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
153
158
|
for (const successor of successors) {
|
|
154
159
|
if (search.findViolatedActionProperty(state, successor.state)) return undefined;
|
|
155
160
|
if (!search.satisfiesActionConstraints(state, successor.state)) continue;
|
|
161
|
+
if (generatedStates) {
|
|
162
|
+
writeStateFingerprint(successor.state, fingerprint);
|
|
163
|
+
if (!generatedStates.insert(fingerprint[0], fingerprint[1])) continue;
|
|
164
|
+
}
|
|
156
165
|
canonicalize(successor.state);
|
|
157
|
-
|
|
166
|
+
const fingerprintState = search.getFingerprintState(successor.state);
|
|
167
|
+
writeStateFingerprint(fingerprintState, fingerprint);
|
|
158
168
|
const owner = getFingerprintOwner(fingerprint[1], count);
|
|
159
169
|
if (owner === self) {
|
|
160
170
|
if (!table.insert(fingerprint[0], fingerprint[1])) continue;
|
|
@@ -164,6 +174,7 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
164
174
|
continue;
|
|
165
175
|
}
|
|
166
176
|
next.push(successor.state);
|
|
177
|
+
nextFingerprintStates.push(fingerprintState);
|
|
167
178
|
newStates[origin]++;
|
|
168
179
|
continue;
|
|
169
180
|
}
|
|
@@ -171,6 +182,7 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
171
182
|
pending[owner].states.push(successor.state);
|
|
172
183
|
pending[owner].origins.push(origin);
|
|
173
184
|
pending[owner].fingerprints.push(fingerprint[0], fingerprint[1]);
|
|
185
|
+
pending[owner].fingerprintStates.push(fingerprintState);
|
|
174
186
|
}
|
|
175
187
|
}
|
|
176
188
|
return { pending, generated: statistics.generated, newStates };
|
|
@@ -184,7 +196,13 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
184
196
|
return bits;
|
|
185
197
|
};
|
|
186
198
|
|
|
187
|
-
const commit = (
|
|
199
|
+
const commit = (
|
|
200
|
+
pending: Pending,
|
|
201
|
+
bits: Uint8Array,
|
|
202
|
+
next: Value[][],
|
|
203
|
+
nextFingerprintStates: Value[][],
|
|
204
|
+
newStates: Int32Array,
|
|
205
|
+
) => {
|
|
188
206
|
const unmarks: number[] = [];
|
|
189
207
|
for (let index = 0; index < bits.length; index++) {
|
|
190
208
|
if (bits[index] === 0) continue;
|
|
@@ -193,6 +211,7 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
193
211
|
if (search.findViolatedInvariant(state, parentState)) return undefined;
|
|
194
212
|
if (search.satisfiesConstraints(state, parentState)) {
|
|
195
213
|
next.push(state);
|
|
214
|
+
nextFingerprintStates.push(pending.fingerprintStates[index]);
|
|
196
215
|
newStates[pending.origins[index]]++;
|
|
197
216
|
continue;
|
|
198
217
|
}
|
|
@@ -230,7 +249,8 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
230
249
|
|
|
231
250
|
const runLevel = async (level: number): Promise<LevelMessage | undefined> => {
|
|
232
251
|
const next: Value[][] = [];
|
|
233
|
-
const
|
|
252
|
+
const nextFingerprintStates: Value[][] = [];
|
|
253
|
+
const expanded = expand(frontier.states, next, nextFingerprintStates);
|
|
234
254
|
if (!expanded || getRankedStringCount() !== startupStrings) return undefined;
|
|
235
255
|
peers.forEach((peer) => {
|
|
236
256
|
const fingerprints = Int32Array.from(expanded.pending[peer.index].fingerprints);
|
|
@@ -247,6 +267,7 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
247
267
|
expanded.pending[reply.from],
|
|
248
268
|
reply.bits ?? new Uint8Array(),
|
|
249
269
|
next,
|
|
270
|
+
nextFingerprintStates,
|
|
250
271
|
expanded.newStates,
|
|
251
272
|
);
|
|
252
273
|
if (!unmarks) return undefined;
|
|
@@ -255,9 +276,7 @@ const run = (start: StartMessage, parent: Port) => {
|
|
|
255
276
|
const added = next.length;
|
|
256
277
|
const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
|
|
257
278
|
expanded.newStates.forEach((degree) => outDegrees[Math.min(degree, OUT_DEGREE_BUCKETS - 1)]++);
|
|
258
|
-
const fingerprints =
|
|
259
|
-
environment.runtime.getFingerprint(state),
|
|
260
|
-
);
|
|
279
|
+
const fingerprints = search.getStoredFingerprints(next, nextFingerprintStates);
|
|
261
280
|
const sizes = Array.from({ length: count }, () => 0);
|
|
262
281
|
sizes[self] = added;
|
|
263
282
|
peers.forEach((peer) =>
|