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.
@@ -0,0 +1,384 @@
1
+ // tlapm's Expr.Action.lambdify, which instantiation applies to each definition and theorem. Each
2
+ // ENABLED and \cdot takes a LAMBDA over the variables it binds, after expanding the definitions
3
+ // in its scope that soundness needs expanded.
4
+
5
+ import { TlaProofError, TlapmBugError } from "./errors";
6
+ import type { Anonymizer } from "./obligation-anon";
7
+ import {
8
+ adjoin,
9
+ applyBuiltin,
10
+ at,
11
+ bumpBy,
12
+ getAtPrefix,
13
+ getErrorNote,
14
+ getLocusText,
15
+ getScopeAt,
16
+ mapScoped,
17
+ normalizeApply,
18
+ shift,
19
+ stripProps,
20
+ substituteExpr,
21
+ type Expr,
22
+ type ObligationWarning,
23
+ type Scope,
24
+ type Substitution,
25
+ } from "./obligation-expression";
26
+ import { getLevelInfo, LEVEL_NUMBERS } from "./obligation-levels";
27
+
28
+ interface FreshNames {
29
+ names: string[];
30
+ depth: number;
31
+ }
32
+
33
+ const getHyp = (scope: Scope | undefined, index: number) => {
34
+ const hyp = getScopeAt(scope, index - 1)?.hyp;
35
+ if (!hyp) throw new TlapmBugError();
36
+ return hyp;
37
+ };
38
+
39
+ const isActionOperator = (
40
+ expression: Expr,
41
+ ): expression is Expr & { kind: "Internal"; builtin: "ENABLED" | "Cdot" } =>
42
+ expression.kind === "Internal" &&
43
+ (expression.builtin === "ENABLED" || expression.builtin === "Cdot");
44
+
45
+ const isLambdified = (expression: Expr) =>
46
+ expression.kind === "Apply" &&
47
+ expression.args.length === 1 &&
48
+ expression.args[0].kind === "Lambda" &&
49
+ (isActionOperator(expression.operator) ||
50
+ (expression.operator.kind === "Opaque" &&
51
+ (expression.operator.name === "EnabledWrapper" ||
52
+ expression.operator.name === "CdotWrapper")));
53
+
54
+ const getInitialLevel = (level: number | undefined) =>
55
+ level === undefined ? 2 : Math.min(2, level);
56
+
57
+ const getDecrementedLevel = (level: number | undefined) =>
58
+ level === undefined ? undefined : Math.max(1, level - 1);
59
+
60
+ // auto_expansion_of_defs. ENABLED and the first argument of \cdot need every definition of action
61
+ // level expanded, a prime lowers that bound to 1, and so does the second argument of \cdot.
62
+ const expandDefinitions = (
63
+ onWarning: (warning: ObligationWarning) => void,
64
+ scope: Scope | undefined,
65
+ expression: Expr,
66
+ level: number | undefined,
67
+ ): Expr => {
68
+ const recurse = (inner: Expr, innerLevel = level) =>
69
+ expandDefinitions(onWarning, scope, inner, innerLevel);
70
+ if (isLambdified(expression)) return expression;
71
+ if (expression.kind === "Sub") {
72
+ return {
73
+ ...expression,
74
+ action: recurse(expression.action),
75
+ subscript: recurse(expression.subscript, getDecrementedLevel(level)),
76
+ };
77
+ }
78
+ if (expression.kind === "Apply" && expression.operator.kind === "Internal") {
79
+ const builtin = expression.operator.builtin;
80
+ const [first, second] = expression.args;
81
+ if (builtin === "Cdot" && expression.args.length === 2) {
82
+ return { ...expression, args: [recurse(first, getInitialLevel(level)), recurse(second, 1)] };
83
+ }
84
+ if (builtin === "ENABLED" && expression.args.length === 1) {
85
+ return { ...expression, args: [recurse(first, getInitialLevel(level))] };
86
+ }
87
+ if ((builtin === "Prime" || builtin === "UNCHANGED") && expression.args.length === 1) {
88
+ return { ...expression, args: [recurse(first, getDecrementedLevel(level))] };
89
+ }
90
+ }
91
+ const head =
92
+ expression.kind === "Apply" && expression.operator.kind === "Ix"
93
+ ? expression.operator
94
+ : expression;
95
+ if (head.kind !== "Ix") {
96
+ return mapScoped(
97
+ scope,
98
+ expression,
99
+ (inner, child) => expandDefinitions(onWarning, inner, child, level),
100
+ adjoin,
101
+ );
102
+ }
103
+ if (level === undefined) return expression;
104
+ const expressionLevel = getLevelInfo(scope, expression).level;
105
+ const hyp = getHyp(scope, head.index);
106
+ const getDeclaration = (name: string) =>
107
+ `The operator \`${name}\` is declared at:\n${hyp.location ? getLocusText(hyp.location) : "<unknown location>"}`;
108
+ if (hyp.kind === "Fresh" && hyp.level === "Unknown") {
109
+ onWarning(
110
+ getErrorNote(
111
+ expression.location,
112
+ `Declared operator \`${hyp.name}\` has unknown level and occurs within the scope of \`ENABLED\` or \`\\cdot\` where soundness requires expanding expressions of level >= ${level}.\nIf a constant operator is substituted for this operator, then the expression that results from applying this declared operator has expression level ${expressionLevel}.\n May need expansion. A declared operator cannot be expanded.\n${getDeclaration(hyp.name)}\n and has arity: ${hyp.shape}`,
113
+ ),
114
+ );
115
+ throw new TlapmBugError(
116
+ 'File "src/expr/e_action.ml", line 367, characters 20-26: Assertion failed',
117
+ );
118
+ }
119
+ if (expressionLevel < level) return expression;
120
+ if (expressionLevel > 2 || hyp.kind === "Fact") throw new TlapmBugError();
121
+ if (hyp.kind === "Flex") {
122
+ if (expression.kind !== "Ix") throw new TlapmBugError();
123
+ return expression;
124
+ }
125
+ if (hyp.kind === "Fresh") {
126
+ const message = `The expression that results from applying the declared operator \`${hyp.name}\` has expression level ${expressionLevel} within the scope of \`ENABLED\` or \`\\cdot\` where soundness requires expanding expressions of level >= ${level}. A declared operator cannot be expanded.\n${getDeclaration(hyp.name)}\n and has level: ${LEVEL_NUMBERS[hyp.level]}\n and arity: ${hyp.shape}`;
127
+ onWarning(getErrorNote(expression.location, message));
128
+ throw new TlaProofError(message);
129
+ }
130
+ const definition = hyp.definition;
131
+ if (definition.kind !== "Operator" && definition.kind !== "Bpragma") {
132
+ throw new TlapmBugError();
133
+ }
134
+ onWarning({
135
+ text: `${getAtPrefix(expression.location)}[INFO]: Auto-expanding the definition of operator: ${definition.name}\n`,
136
+ channel: "stdout",
137
+ });
138
+ const body = substituteExpr(shift(head.index), definition.body);
139
+ return recurse(
140
+ stripProps(expression.kind === "Apply" ? normalizeApply(body, expression.args) : body),
141
+ );
142
+ };
143
+
144
+ const getEquality = (expression: Expr): Expr =>
145
+ applyBuiltin("Eq", [applyBuiltin("Prime", [expression], expression), expression], expression);
146
+
147
+ // E_tla_norm.rewrite_unch: UNCHANGED <<x, <<y>>>> as x' = x /\ y' = y.
148
+ const rewriteUnchanged = (expression: Expr): Expr => {
149
+ const flatten = (inner: Expr): Expr[] =>
150
+ inner.kind === "Tuple" ? inner.elements.flatMap(flatten) : [inner];
151
+ const items = flatten(expression);
152
+ if (items.length === 0) return at({ kind: "Internal", builtin: "TRUE" }, expression);
153
+ if (items.length === 1) return getEquality(items[0]);
154
+ return at({ kind: "List", bullet: "And", items: items.map(getEquality) }, expression);
155
+ };
156
+
157
+ // expand_propositional_action_operators: UNCHANGED v, [A]_v and <<A>>_v in terms of primes.
158
+ const expandPropositional = (expression: Expr): Expr => {
159
+ if (expression.kind === "Apply" && expression.args.length === 1) {
160
+ return rewriteUnchanged(expression.args[0]);
161
+ }
162
+ if (expression.kind !== "Sub") throw new TlapmBugError();
163
+ const unchanged = rewriteUnchanged(expression.subscript);
164
+ const second =
165
+ expression.modal === "Box" ? unchanged : applyBuiltin("Neg", [unchanged], expression);
166
+ return applyBuiltin(
167
+ expression.modal === "Box" ? "Disj" : "Conj",
168
+ [expression.action, second],
169
+ expression,
170
+ );
171
+ };
172
+
173
+ // OCaml's Hashtbl.hash of a string: caml_hash's MurmurHash3 over the bytes.
174
+ const getHash = (text: string) => {
175
+ const rotate = (value: number, amount: number) => (value << amount) | (value >>> (32 - amount));
176
+ const mix = (hash: number, data: number) => {
177
+ const word = Math.imul(rotate(Math.imul(data, 0xcc9e2d51), 15), 0x1b873593);
178
+ return (Math.imul(rotate(hash ^ word, 13), 5) + 0xe6546b64) | 0;
179
+ };
180
+ const bytes = new TextEncoder().encode(text);
181
+ const blocks = bytes.length - (bytes.length % 4);
182
+ let hash = 0;
183
+ for (let index = 0; index < blocks; index += 4) {
184
+ hash = mix(
185
+ hash,
186
+ bytes[index] | (bytes[index + 1] << 8) | (bytes[index + 2] << 16) | (bytes[index + 3] << 24),
187
+ );
188
+ }
189
+ const tail = bytes.subarray(blocks).reduceRight((word, byte) => (word << 8) | byte, 0);
190
+ if (bytes.length > blocks) hash = mix(hash, tail);
191
+ hash ^= bytes.length;
192
+ hash = Math.imul(hash ^ (hash >>> 16), 0x85ebca6b);
193
+ hash = Math.imul(hash ^ (hash >>> 13), 0xc2b2ae35);
194
+ return (hash ^ (hash >>> 16)) & 0x3fffffff;
195
+ };
196
+
197
+ // The keys of an OCaml Hashtbl.create 16 in Hashtbl.to_seq_keys order. Hashtbl.add prepends to a
198
+ // bucket and doubles the buckets once the table holds more than twice their count.
199
+ const getHashtblKeys = (names: string[]) => {
200
+ let buckets: string[][] = Array.from({ length: 16 }, () => []);
201
+ names.forEach((name, index) => {
202
+ buckets[getHash(name) & (buckets.length - 1)].unshift(name);
203
+ if (index + 1 <= buckets.length * 2) return;
204
+ const resized: string[][] = Array.from({ length: buckets.length * 2 }, () => []);
205
+ buckets.flat().forEach((key) => resized[getHash(key) & (resized.length - 1)].push(key));
206
+ buckets = resized;
207
+ });
208
+ return buckets.flat();
209
+ };
210
+
211
+ // lambdify_action_operators. A variable met directly binds into current, and one met under a prime
212
+ // into primed. ENABLED A binds the primes of A, and A \cdot B binds those of A and the unprimed
213
+ // variables of B, each as an opaque name such as x#enabled#prime1.
214
+ const bindVariables = (
215
+ onWarning: (warning: ObligationWarning) => void,
216
+ current: FreshNames | undefined,
217
+ primed: FreshNames | undefined,
218
+ scope: Scope | undefined,
219
+ expression: Expr,
220
+ ): Expr => {
221
+ const recurse = (inner: Expr) => bindVariables(onWarning, current, primed, scope, inner);
222
+ const isInScope = current !== undefined || primed !== undefined;
223
+ const lambdifyOperator = (operator: Expr, args: Expr[]) => {
224
+ const fresh: FreshNames = {
225
+ names: [],
226
+ depth: Math.max(current?.depth ?? 0, primed?.depth ?? 0) + 1,
227
+ };
228
+ const [first, second] = args;
229
+ const bound = [
230
+ bindVariables(onWarning, current, fresh, scope, first),
231
+ ...(second ? [bindVariables(onWarning, fresh, primed, scope, second)] : []),
232
+ ];
233
+ const params = fresh.names.length === 0 ? ["r#0"] : getHashtblKeys(fresh.names);
234
+ return at(
235
+ {
236
+ kind: "Apply",
237
+ operator,
238
+ args: [
239
+ {
240
+ kind: "Lambda",
241
+ params: ["_", ...params].map((name) => ({ name, shape: 0 })),
242
+ body: bound.length === 1 ? bound[0] : { kind: "List", bullet: "And", items: bound },
243
+ },
244
+ ],
245
+ },
246
+ expression,
247
+ );
248
+ };
249
+ if (isLambdified(expression)) return expression;
250
+ if (expression.kind === "Apply" && expression.operator.kind === "Internal") {
251
+ const builtin = expression.operator.builtin;
252
+ if (
253
+ (builtin === "ENABLED" && expression.args.length === 1) ||
254
+ (builtin === "Cdot" && expression.args.length === 2)
255
+ ) {
256
+ return lambdifyOperator(expression.operator, expression.args);
257
+ }
258
+ if (builtin === "Prime" && expression.args.length === 1) {
259
+ const arg = bindVariables(onWarning, primed, undefined, scope, expression.args[0]);
260
+ return primed ? arg : { ...expression, args: [arg] };
261
+ }
262
+ if (builtin === "UNCHANGED" && isInScope) return recurse(expandPropositional(expression));
263
+ }
264
+ if (expression.kind === "Sub" && isInScope) return recurse(expandPropositional(expression));
265
+ if (expression.kind === "Ix") {
266
+ if (!current) return expression;
267
+ const hyp = getHyp(scope, expression.index);
268
+ if (hyp.kind === "Fact") throw new TlapmBugError();
269
+ if (hyp.kind !== "Flex") return expression;
270
+ const name = `${hyp.name}#enabled#prime${current.depth}`;
271
+ if (!current.names.includes(name)) current.names.push(name);
272
+ return at({ kind: "Opaque", name }, expression);
273
+ }
274
+ if (expression.kind === "Apply" && expression.operator.kind === "Opaque") {
275
+ onWarning(
276
+ getErrorNote(
277
+ expression.location,
278
+ `Named operator \`${expression.operator.name}\` unexpected after anonymization.`,
279
+ ),
280
+ );
281
+ throw new TlapmBugError(
282
+ 'File "src/expr/e_action.ml", line 1048, characters 12-18: Assertion failed',
283
+ );
284
+ }
285
+ return mapScoped(
286
+ scope,
287
+ expression,
288
+ (inner, child) => bindVariables(onWarning, current, primed, inner, child),
289
+ adjoin,
290
+ );
291
+ };
292
+
293
+ // shift_indices_after_lambdify, a Subst.map_visible_hyp that makes room in each new LAMBDA body
294
+ // for its parameters and drops the placeholder parameter.
295
+ const shiftIndices = (substitution: Substitution, expression: Expr): Expr => {
296
+ if (expression.kind === "Apply" && isActionOperator(expression.operator)) {
297
+ const [lambda] = expression.args;
298
+ if (expression.args.length !== 1 || lambda.kind !== "Lambda") {
299
+ throw new TlapmBugError();
300
+ }
301
+ const [placeholder, ...params] = lambda.params;
302
+ if (placeholder.name !== "_") return expression;
303
+ const body = shiftIndices(
304
+ { kind: "Compose", outer: substitution, inner: shift(params.length) },
305
+ lambda.body,
306
+ );
307
+ return {
308
+ kind: "Apply",
309
+ operator: expression.operator,
310
+ args: [{ kind: "Lambda", params, body }],
311
+ };
312
+ }
313
+ if (expression.kind === "Ix") return substituteExpr(substitution, expression);
314
+ if (expression.kind === "Apply") {
315
+ return at(
316
+ normalizeApply(
317
+ shiftIndices(substitution, expression.operator),
318
+ expression.args.map((arg) => shiftIndices(substitution, arg)),
319
+ ),
320
+ expression,
321
+ );
322
+ }
323
+ return mapScoped(substitution, expression, shiftIndices, (current) => bumpBy(1, current));
324
+ };
325
+
326
+ // normalize_lambda_param_names: the parameters become r#0, r#1, ... and the operator
327
+ // EnabledWrapper or CdotWrapper.
328
+ const renameParams = (expression: Expr): Expr => {
329
+ if (expression.kind === "Apply" && isActionOperator(expression.operator)) {
330
+ const [lambda] = expression.args;
331
+ if (expression.args.length !== 1 || lambda.kind !== "Lambda" || lambda.params[0].name === "_") {
332
+ throw new TlapmBugError();
333
+ }
334
+ const name = expression.operator.builtin === "Cdot" ? "CdotWrapper" : "EnabledWrapper";
335
+ return {
336
+ kind: "Apply",
337
+ operator: at({ kind: "Opaque", name }, expression.operator),
338
+ args: [
339
+ {
340
+ kind: "Lambda",
341
+ params: lambda.params.map((_param, index) => ({ name: `r#${index}`, shape: 0 })),
342
+ body: renameParams(lambda.body),
343
+ },
344
+ ],
345
+ };
346
+ }
347
+ return mapScoped(
348
+ undefined,
349
+ expression,
350
+ (_state, child) => renameParams(child),
351
+ () => undefined,
352
+ );
353
+ };
354
+
355
+ // check_arity: no \cdot keeps a single argument that is not a LAMBDA.
356
+ const checkArity = (expression: Expr): Expr => {
357
+ const isInvalid =
358
+ expression.kind === "Apply" &&
359
+ expression.operator.kind === "Internal" &&
360
+ expression.operator.builtin === "Cdot" &&
361
+ expression.args.length === 1 &&
362
+ expression.args[0].kind !== "Lambda";
363
+ if (isInvalid) throw new TlapmBugError();
364
+ return mapScoped(
365
+ undefined,
366
+ expression,
367
+ (_state, child) => checkArity(child),
368
+ () => undefined,
369
+ );
370
+ };
371
+
372
+ export const lambdify = (
373
+ anonymizer: Anonymizer,
374
+ onWarning: (warning: ObligationWarning) => void,
375
+ scope: Scope | undefined,
376
+ expression: Expr,
377
+ ) => {
378
+ const expanded = expandDefinitions(onWarning, scope, expression, undefined);
379
+ const bound = shiftIndices(
380
+ shift(0),
381
+ bindVariables(onWarning, undefined, undefined, scope, expanded),
382
+ );
383
+ return checkArity(renameParams(anonymizer.expr(scope, bound)));
384
+ };