lemmascript 0.5.18 → 0.5.19
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 +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +124 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +162 -35
- package/tools/dist/extract.js +108 -21
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +27 -16
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +296 -677
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +182 -203
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +298 -108
- package/tools/dist/typedecls.js +59 -0
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dafny emitter — IR → Dafny text.
|
|
3
3
|
*/
|
|
4
|
-
import { usesName, usesNameInDecl } from "./ir.js";
|
|
5
|
-
import {
|
|
4
|
+
import { exactIntegerLiteral, usesName, usesNameInDecl, usesNameInStmts } from "./ir.js";
|
|
5
|
+
import { freshNameWhere, userNames } from "./names.js";
|
|
6
6
|
import { renameFreeVar } from "./transform.js";
|
|
7
7
|
/** Fresh binder for a comprehension wrapping the given subexpressions: `base`
|
|
8
8
|
* verbatim unless one of them references it, then primed until free. A *local*
|
|
9
9
|
* check — a same-named name elsewhere in the module keeps the plain binder. */
|
|
10
10
|
function freshBinder(base, ...wrapped) {
|
|
11
|
-
return
|
|
11
|
+
return freshNameWhere(base, name => wrapped.some(w => usesName(w, name)));
|
|
12
12
|
}
|
|
13
13
|
/** Binder + body for lowering a single-return lambda to a comprehension whose
|
|
14
14
|
* receiver is emitted inside the binder's scope. TS scoping keeps the receiver
|
|
@@ -128,6 +128,13 @@ function resetDafnyNameCache() {
|
|
|
128
128
|
_takenDafnyNames.add(emitted);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/** Source-name → emitted-Dafny-name pairs from the most recent emitDafnyFile
|
|
132
|
+
* call (identity-mapped names included). Consumed by `lsc info --typed` so
|
|
133
|
+
* satellites (e.g. lemmascript-crosscheck) can address emitted declarations
|
|
134
|
+
* by their mangled names (`_Box` → `i_Box'`) without re-deriving the rules. */
|
|
135
|
+
export function emittedNameMap() {
|
|
136
|
+
return new Map([..._generatedDafnyNames, ..._userDafnyNames]);
|
|
137
|
+
}
|
|
131
138
|
function escapeName(name) {
|
|
132
139
|
// \result is carried through the IR as var "\\result"; render it as the
|
|
133
140
|
// current method's out-parameter name (chosen locally by methodHeader).
|
|
@@ -171,7 +178,7 @@ function methodHeader(prefix, params, returnType, scope) {
|
|
|
171
178
|
// name is recorded so `\result` references resolve to it.
|
|
172
179
|
const taken = (n) => params.some(p => escapeName(p.name) === n) ||
|
|
173
180
|
(scope !== undefined && usesNameInDecl(scope.requires, scope.ensures, scope.body, n));
|
|
174
|
-
const resName =
|
|
181
|
+
const resName = freshNameWhere("res", taken);
|
|
175
182
|
_resultName = resName;
|
|
176
183
|
return `${sig} returns (${resName}: ${tyToDafny(returnType)})`;
|
|
177
184
|
}
|
|
@@ -183,9 +190,8 @@ const OP_MAP = {
|
|
|
183
190
|
};
|
|
184
191
|
function mapOp(op) { return OP_MAP[op] ?? op; }
|
|
185
192
|
// ── Expression emission ─────────────────────────────────────
|
|
186
|
-
/** Emit a match scrutinee — either a variable name (string) or an expression. */
|
|
187
193
|
function emitScrutinee(s) {
|
|
188
|
-
return
|
|
194
|
+
return emitExpr(s);
|
|
189
195
|
}
|
|
190
196
|
/** Collapse nested forall/exists into a single quantifier with multiple bound vars. */
|
|
191
197
|
function emitQuantifier(e, keyword) {
|
|
@@ -212,6 +218,8 @@ function emitExpr(e) {
|
|
|
212
218
|
switch (e.kind) {
|
|
213
219
|
case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
|
|
214
220
|
case "num": return `${e.value}`;
|
|
221
|
+
// Already canonical decimal; Dafny's `int` is mathematical, so no `n` suffix.
|
|
222
|
+
case "bigint": return e.value;
|
|
215
223
|
case "bool": return e.value ? "true" : "false";
|
|
216
224
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
217
225
|
case "constructor": {
|
|
@@ -281,8 +289,43 @@ function emitExpr(e) {
|
|
|
281
289
|
}
|
|
282
290
|
return `${obj}[${args[0]}..${args[1]}]`;
|
|
283
291
|
}
|
|
284
|
-
if (e.method === "map")
|
|
285
|
-
|
|
292
|
+
if (e.method === "map") {
|
|
293
|
+
// Always a seq comprehension: Seq.Map would hide the element access
|
|
294
|
+
// behind a closure, defeating Dafny's termination checker for
|
|
295
|
+
// recursive rebuild walkers. A literal lambda argument is
|
|
296
|
+
// beta-reduced through a `var` binding — an applied closure defeats
|
|
297
|
+
// the checker too; any other argument is applied to the element
|
|
298
|
+
// directly. A non-variable receiver is bound once up front: the
|
|
299
|
+
// comprehension mentions it three times, and splicing would make
|
|
300
|
+
// any closure literal inside it (e.g. a Filter predicate) three
|
|
301
|
+
// distinct closures, unprovably equal through an opaque callee.
|
|
302
|
+
// Name freshness is a local IR-level check until a backend name
|
|
303
|
+
// allocator exists.
|
|
304
|
+
const lam = e.args[0];
|
|
305
|
+
const fresh = (base, taken) => {
|
|
306
|
+
let name = base;
|
|
307
|
+
for (let n = 2; taken(name); n++)
|
|
308
|
+
name = `${base}${n}`;
|
|
309
|
+
return name;
|
|
310
|
+
};
|
|
311
|
+
const taken = (n) => usesName(e.obj, n) || usesName(lam, n) ||
|
|
312
|
+
(lam.kind === "lambda" && (lam.params.some(pp => pp.name === n) ||
|
|
313
|
+
usesNameInStmts(lam.body, n)));
|
|
314
|
+
const bind = e.obj.kind !== "var";
|
|
315
|
+
const s = bind ? fresh("s_map", taken) : obj;
|
|
316
|
+
const idx = fresh("i_map", n => taken(n) || n === s);
|
|
317
|
+
let core;
|
|
318
|
+
if (lam.kind === "lambda" && lam.params.length === 1 &&
|
|
319
|
+
lam.body.length === 1 && lam.body[0].kind === "return") {
|
|
320
|
+
const p = escapeName(lam.params[0].name);
|
|
321
|
+
core = `var ${p} := ${s}[${idx}]; ${emitExpr(lam.body[0].value)}`;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
core = `(${args[0]})(${s}[${idx}])`;
|
|
325
|
+
}
|
|
326
|
+
const comp = `seq(|${s}|, ${idx} requires 0 <= ${idx} < |${s}| => ${core})`;
|
|
327
|
+
return bind ? `(var ${s} := ${obj}; ${comp})` : comp;
|
|
328
|
+
}
|
|
286
329
|
if (e.method === "filter")
|
|
287
330
|
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
288
331
|
// filterMap (synthesized in resolve): drop Nones and unwrap to seq<T>.
|
|
@@ -293,6 +336,11 @@ function emitExpr(e) {
|
|
|
293
336
|
}
|
|
294
337
|
if (e.method === "every")
|
|
295
338
|
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
339
|
+
if (e.method === "find") {
|
|
340
|
+
needPreamble("OptionType");
|
|
341
|
+
needPreamble("SeqFind");
|
|
342
|
+
return `SeqFind(${obj}, ${args[0]})`;
|
|
343
|
+
}
|
|
296
344
|
if (e.method === "findLast") {
|
|
297
345
|
needPreamble("OptionType");
|
|
298
346
|
needPreamble("SeqFindLast");
|
|
@@ -348,10 +396,14 @@ function emitExpr(e) {
|
|
|
348
396
|
return `StringSplit(${obj}, ${args[0]})`;
|
|
349
397
|
}
|
|
350
398
|
if (e.method === "slice") {
|
|
351
|
-
// JS negative index: arr.slice(0, -N) → arr[0..|arr|-N].
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
399
|
+
// JS negative index: arr.slice(0, -N) → arr[0..|arr|-N]. Transform
|
|
400
|
+
// folds unary minus on a numeric literal into a negative `num` node,
|
|
401
|
+
// but leaves a negated bigint structural — `exactIntegerLiteral`
|
|
402
|
+
// recognizes both.
|
|
403
|
+
const negVal = (a) => {
|
|
404
|
+
const v = exactIntegerLiteral(a);
|
|
405
|
+
return v !== null && v < 0n ? (-v).toString(10) : null;
|
|
406
|
+
};
|
|
355
407
|
const loN = negVal(e.args[0]);
|
|
356
408
|
const loEx = loN !== null ? `|${obj}|-${loN}` : args[0];
|
|
357
409
|
if (args.length === 1)
|
|
@@ -453,6 +505,8 @@ function emitExpr(e) {
|
|
|
453
505
|
return `!(${emitExpr(e.expr)})`;
|
|
454
506
|
if (e.op === "-" && e.expr.kind === "num")
|
|
455
507
|
return `(-(${e.expr.value}))`;
|
|
508
|
+
if (e.op === "-" && e.expr.kind === "bigint")
|
|
509
|
+
return `(-(${e.expr.value}))`;
|
|
456
510
|
if (e.op === "-")
|
|
457
511
|
return `(-(${emitExpr(e.expr)}))`;
|
|
458
512
|
return `${op}(${emitExpr(e.expr)})`;
|
|
@@ -468,27 +522,23 @@ function emitExpr(e) {
|
|
|
468
522
|
// Bitwise operators on int: translate to arithmetic
|
|
469
523
|
// x >> n → x / 2^n (right shift)
|
|
470
524
|
// x << n → x * 2^n (left shift)
|
|
471
|
-
if (e.op === ">>") {
|
|
472
|
-
|
|
473
|
-
|
|
525
|
+
if (e.op === ">>" || e.op === "<<") {
|
|
526
|
+
const shift = exactIntegerLiteral(e.right);
|
|
527
|
+
// Cap the fold: a huge literal shift would inline an absurd numeral.
|
|
528
|
+
if (shift !== null && shift >= 0n && shift <= 1024n) {
|
|
529
|
+
const factor = (1n << shift).toString(10);
|
|
530
|
+
return `(${emitExpr(e.left)} ${e.op === ">>" ? "/" : "*"} ${factor})`;
|
|
474
531
|
}
|
|
475
532
|
needPreamble("Pow2");
|
|
476
|
-
return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
|
|
477
|
-
}
|
|
478
|
-
if (e.op === "<<") {
|
|
479
|
-
if (e.right.kind === "num") {
|
|
480
|
-
return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
|
|
481
|
-
}
|
|
482
|
-
needPreamble("Pow2");
|
|
483
|
-
return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
|
|
533
|
+
return `(${emitExpr(e.left)} ${e.op === ">>" ? "/" : "*"} Pow2(${emitExpr(e.right)}))`;
|
|
484
534
|
}
|
|
485
535
|
// x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
|
|
486
536
|
if (e.op === "&") {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const modulus = mask +
|
|
490
|
-
if ((modulus & (modulus -
|
|
491
|
-
return `(${emitExpr(e.left)} % ${modulus})`;
|
|
537
|
+
const mask = exactIntegerLiteral(e.right);
|
|
538
|
+
if (mask !== null && mask >= 0n) {
|
|
539
|
+
const modulus = mask + 1n;
|
|
540
|
+
if ((modulus & (modulus - 1n)) === 0n) {
|
|
541
|
+
return `(${emitExpr(e.left)} % ${modulus.toString(10)})`;
|
|
492
542
|
}
|
|
493
543
|
}
|
|
494
544
|
needPreamble("BitAnd");
|
|
@@ -556,6 +606,15 @@ function emitExpr(e) {
|
|
|
556
606
|
needPreamble("Perm");
|
|
557
607
|
if (e.fn === "SetFromSeq")
|
|
558
608
|
needPreamble("SetFromSeq");
|
|
609
|
+
// A constructor application must spell the name the same way the
|
|
610
|
+
// datatype declaration does — `dafnyCtorName`, not `escapeName`, since
|
|
611
|
+
// tags come from source strings ("spec-pure") that escapeName leaves alone.
|
|
612
|
+
if (e.ctorOf) {
|
|
613
|
+
const ctor = dafnyCtorName(e.fn);
|
|
614
|
+
return _ambiguousCtors.has(e.fn)
|
|
615
|
+
? `${e.ctorOf}.${ctor}(${args.join(", ")})`
|
|
616
|
+
: `${ctor}(${args.join(", ")})`;
|
|
617
|
+
}
|
|
559
618
|
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
560
619
|
}
|
|
561
620
|
case "field": {
|
|
@@ -568,6 +627,11 @@ function emitExpr(e) {
|
|
|
568
627
|
return `${obj}.Keys`;
|
|
569
628
|
if (e.field === "toNat")
|
|
570
629
|
return obj;
|
|
630
|
+
if (e.ctor && e.fromUnion) {
|
|
631
|
+
const renamed = _ctorFieldRenames.get(`${e.fromUnion}.${e.ctor}.${e.field}`);
|
|
632
|
+
if (renamed)
|
|
633
|
+
return `${obj}.${escapeName(renamed)}`;
|
|
634
|
+
}
|
|
571
635
|
return `${obj}.${escapeName(e.field)}`;
|
|
572
636
|
}
|
|
573
637
|
case "toNat":
|
|
@@ -588,7 +652,10 @@ function emitExpr(e) {
|
|
|
588
652
|
if (e.fields.length === 0) {
|
|
589
653
|
return emitExpr(e.spread);
|
|
590
654
|
}
|
|
591
|
-
const updates = e.fields.map(f =>
|
|
655
|
+
const updates = e.fields.map(f => {
|
|
656
|
+
const renamed = e.ctor && e.ctorOf ? _ctorFieldRenames.get(`${e.ctorOf}.${e.ctor}.${f.name}`) : undefined;
|
|
657
|
+
return `${escapeName(renamed ?? f.name)} := ${emitExpr(f.value)}`;
|
|
658
|
+
});
|
|
592
659
|
return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
|
|
593
660
|
}
|
|
594
661
|
// Match constructor by field names — prefer exact match over first-field heuristic
|
|
@@ -792,9 +859,9 @@ function emitDecl(d) {
|
|
|
792
859
|
const collides = new Set([...typesByField].filter(([, s]) => s.size > 1).map(([n]) => n));
|
|
793
860
|
const ctors = d.constructors.map(c => {
|
|
794
861
|
if (c.fields.length === 0)
|
|
795
|
-
return
|
|
796
|
-
const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name}` } : f);
|
|
797
|
-
return `${
|
|
862
|
+
return dafnyCtorName(c.name);
|
|
863
|
+
const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name.replace(/[^A-Za-z0-9_'?]/g, "_")}` } : f);
|
|
864
|
+
return `${dafnyCtorName(c.name)}(${paramList(fields)})`;
|
|
798
865
|
});
|
|
799
866
|
return `datatype ${escapeName(d.name)}${tp} = ${ctors.join(" | ")}`;
|
|
800
867
|
}
|
|
@@ -1036,6 +1103,19 @@ const SEQ_FIND_LAST_INDEX = `function SeqFindLastIndex<T>(s: seq<T>, p: T -> boo
|
|
|
1036
1103
|
else if p(s[|s|-1]) then |s| - 1
|
|
1037
1104
|
else SeqFindLastIndex(s[..|s|-1], p)
|
|
1038
1105
|
}`;
|
|
1106
|
+
const SEQ_FIND = `function SeqFind<T>(s: seq<T>, p: T -> bool): Option<T>
|
|
1107
|
+
ensures SeqFind(s, p).Some? ==> p(SeqFind(s, p).value)
|
|
1108
|
+
ensures SeqFind(s, p).Some? ==> SeqFind(s, p).value in s
|
|
1109
|
+
ensures SeqFind(s, p).Some? ==>
|
|
1110
|
+
exists i: nat :: i < |s| && s[i] == SeqFind(s, p).value && p(s[i]) &&
|
|
1111
|
+
(forall j: nat :: j < i ==> !p(s[j]))
|
|
1112
|
+
ensures SeqFind(s, p).None? ==> forall i :: 0 <= i < |s| ==> !p(s[i])
|
|
1113
|
+
decreases |s|
|
|
1114
|
+
{
|
|
1115
|
+
if |s| == 0 then None
|
|
1116
|
+
else if p(s[0]) then Some(s[0])
|
|
1117
|
+
else SeqFind(s[1..], p)
|
|
1118
|
+
}`;
|
|
1039
1119
|
const SEQ_FIND_LAST = `function SeqFindLast<T>(s: seq<T>, p: T -> bool): Option<T>
|
|
1040
1120
|
ensures SeqFindLast(s, p).Some? ==> p(SeqFindLast(s, p).value)
|
|
1041
1121
|
ensures SeqFindLast(s, p).Some? ==> SeqFindLast(s, p).value in s
|
|
@@ -1294,6 +1374,7 @@ const PREAMBLE_CODE = [
|
|
|
1294
1374
|
["SeqFindIndex", SEQ_FIND_INDEX],
|
|
1295
1375
|
["SeqFindLastIndex", SEQ_FIND_LAST_INDEX],
|
|
1296
1376
|
["SeqFilterSome", SEQ_FILTER_SOME],
|
|
1377
|
+
["SeqFind", SEQ_FIND],
|
|
1297
1378
|
["SeqFindLast", SEQ_FIND_LAST],
|
|
1298
1379
|
["SeqFlatten", SEQ_FLATTEN],
|
|
1299
1380
|
["SeqJoin", SEQ_JOIN],
|
|
@@ -1319,10 +1400,18 @@ const PREAMBLE_CODE = [
|
|
|
1319
1400
|
let _recordCtors = new Map();
|
|
1320
1401
|
let _structureDecls = new Map();
|
|
1321
1402
|
let _declaredTypes = new Set();
|
|
1403
|
+
let _ambiguousCtors = new Set();
|
|
1404
|
+
// `"<union>.<ctor>.<field>"` → per-constructor destructor name, for fields the
|
|
1405
|
+
// inductive emission renames (shared name, differing types). Field reads and
|
|
1406
|
+
// datatype updates with a pinned ctor must use the renamed destructor.
|
|
1407
|
+
let _ctorFieldRenames = new Map();
|
|
1322
1408
|
function buildRecordCtorMap(decls) {
|
|
1323
1409
|
_recordCtors = new Map();
|
|
1324
1410
|
_structureDecls = new Map();
|
|
1325
1411
|
_declaredTypes = new Set();
|
|
1412
|
+
_ambiguousCtors = new Set();
|
|
1413
|
+
_ctorFieldRenames = new Map();
|
|
1414
|
+
const ctorSeen = new Set();
|
|
1326
1415
|
function collectDecl(d) {
|
|
1327
1416
|
if (d.kind === "structure") {
|
|
1328
1417
|
_declaredTypes.add(d.name);
|
|
@@ -1330,8 +1419,36 @@ function buildRecordCtorMap(decls) {
|
|
|
1330
1419
|
if (d.fields.length > 0)
|
|
1331
1420
|
_recordCtors.set(d.fields[0].name, d.name);
|
|
1332
1421
|
}
|
|
1333
|
-
if (d.kind === "inductive")
|
|
1422
|
+
if (d.kind === "inductive") {
|
|
1334
1423
|
_declaredTypes.add(d.name);
|
|
1424
|
+
// Constructor names shared by two datatypes in this module (Expr.let vs
|
|
1425
|
+
// Stmt.let) can't be used bare — emitters must qualify them.
|
|
1426
|
+
for (const c of d.constructors) {
|
|
1427
|
+
if (ctorSeen.has(c.name))
|
|
1428
|
+
_ambiguousCtors.add(c.name);
|
|
1429
|
+
ctorSeen.add(c.name);
|
|
1430
|
+
}
|
|
1431
|
+
// Mirror the destructor renaming the inductive case of emitDecl performs
|
|
1432
|
+
// (shared field name, differing types → per-constructor names), so reads
|
|
1433
|
+
// and updates can be translated to the renamed destructors.
|
|
1434
|
+
const typesByField = new Map();
|
|
1435
|
+
for (const c of d.constructors)
|
|
1436
|
+
for (const f of c.fields) {
|
|
1437
|
+
let s = typesByField.get(f.name);
|
|
1438
|
+
if (!s) {
|
|
1439
|
+
s = new Set();
|
|
1440
|
+
typesByField.set(f.name, s);
|
|
1441
|
+
}
|
|
1442
|
+
s.add(tyToDafny(f.type));
|
|
1443
|
+
}
|
|
1444
|
+
const collides = new Set([...typesByField].filter(([, s]) => s.size > 1).map(([n]) => n));
|
|
1445
|
+
for (const c of d.constructors)
|
|
1446
|
+
for (const f of c.fields) {
|
|
1447
|
+
if (!collides.has(f.name))
|
|
1448
|
+
continue;
|
|
1449
|
+
_ctorFieldRenames.set(`${d.name}.${c.name}.${f.name}`, `${f.name}_${c.name.replace(/[^A-Za-z0-9_'?]/g, "_")}`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1335
1452
|
if (d.kind === "type-alias")
|
|
1336
1453
|
_declaredTypes.add(d.name);
|
|
1337
1454
|
if (d.kind === "def")
|
|
@@ -1359,9 +1476,19 @@ function resolveTy(ty) {
|
|
|
1359
1476
|
return { kind: "set", elem: resolveTy(ty.elem) };
|
|
1360
1477
|
return ty;
|
|
1361
1478
|
}
|
|
1479
|
+
/** Constructor names come from source strings (string-union values like
|
|
1480
|
+
* "spec-pure", discriminated-union tags), which may contain characters no
|
|
1481
|
+
* TS identifier has; map those to `_` before the ordinary escaping. A
|
|
1482
|
+
* collision after mapping fails loudly in Dafny (duplicate constructor)
|
|
1483
|
+
* rather than silently merging. */
|
|
1484
|
+
function dafnyCtorName(name) {
|
|
1485
|
+
return escapeName(name.replace(/[^A-Za-z0-9_'?]/g, "_"));
|
|
1486
|
+
}
|
|
1362
1487
|
function qualifyCtor(name, type) {
|
|
1363
1488
|
const rawName = name.replace(/^\./, "");
|
|
1364
|
-
|
|
1489
|
+
// hasOwn: a ctor literally named "constructor" (the IR's own Expr variant)
|
|
1490
|
+
// must not hit Object.prototype.constructor through the bare index.
|
|
1491
|
+
const mapped = (Object.hasOwn(CTOR_MAP, rawName) ? CTOR_MAP[rawName] : undefined) ?? dafnyCtorName(rawName);
|
|
1365
1492
|
if (type)
|
|
1366
1493
|
return `${type}.${mapped}`;
|
|
1367
1494
|
return mapped;
|
|
@@ -1375,7 +1502,7 @@ const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
|
1375
1502
|
function translatePattern(p) {
|
|
1376
1503
|
if (p.kind === "wild")
|
|
1377
1504
|
return "_";
|
|
1378
|
-
const ctorName = CTOR_MAP[p.ctor] ??
|
|
1505
|
+
const ctorName = (Object.hasOwn(CTOR_MAP, p.ctor) ? CTOR_MAP[p.ctor] : undefined) ?? dafnyCtorName(p.ctor);
|
|
1379
1506
|
if (p.binders.length === 0)
|
|
1380
1507
|
return ctorName;
|
|
1381
1508
|
return `${ctorName}(${p.binders.map(escapeName).join(", ")})`;
|
package/tools/dist/extract.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Project, Node, SyntaxKind, ScriptTarget, ts } from "ts-morph";
|
|
8
8
|
import { initTypeParser } from "./types.js";
|
|
9
|
+
import { normalizeBigIntLiteral } from "./rawir.js";
|
|
9
10
|
import { setUserNames, freshName } from "./names.js";
|
|
10
11
|
// ── Expression extraction ────────────────────────────────────
|
|
11
12
|
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
@@ -15,6 +16,14 @@ let _havocKey = null;
|
|
|
15
16
|
* different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat>`.
|
|
16
17
|
* Cleared at the start of every `extractModule`. */
|
|
17
18
|
const _externs = new Map();
|
|
19
|
+
/** Signature types of *kept* externs, for the imported-type resolver: a type
|
|
20
|
+
* reachable ONLY through an imported function's signature (e.g. an extern
|
|
21
|
+
* returning PresentFact that no local signature mentions) must still be
|
|
22
|
+
* resolved into a full decl, or it synthesizes opaque. Filled only after the
|
|
23
|
+
* extern survives the `_externs` dedup — a cross-file signature superseded by a
|
|
24
|
+
* same-file `//@ extern` contributes nothing to the output, so resolving its
|
|
25
|
+
* types would emit decls that no emitted declaration mentions. */
|
|
26
|
+
const _externSigTypes = [];
|
|
18
27
|
let _currentSourceFile = null;
|
|
19
28
|
/** True only while extracting a function body. Module-level constants that
|
|
20
29
|
* reference cross-file callees (e.g., `BusEvent.define(...)` inside a
|
|
@@ -30,10 +39,14 @@ let _destrCounter = 0;
|
|
|
30
39
|
* lifted `requires`/`ensures` see all the symbols they reference). Idempotent
|
|
31
40
|
* via the `_externs` dedup. */
|
|
32
41
|
function registerExternIfCrossFile(callee, sourceFile) {
|
|
33
|
-
const
|
|
42
|
+
const sigTypes = [];
|
|
43
|
+
const ext = detectCrossFileExtern(callee, sourceFile, sigTypes);
|
|
34
44
|
if (!ext || _externs.has(ext.qualified))
|
|
35
45
|
return;
|
|
36
46
|
_externs.set(ext.qualified, ext);
|
|
47
|
+
// Only now that the extern is kept do its signature types become resolver
|
|
48
|
+
// seeds — see `_externSigTypes`.
|
|
49
|
+
_externSigTypes.push(...sigTypes);
|
|
37
50
|
// Recurse: scan the source decl's body for nested cross-file calls so any
|
|
38
51
|
// symbol referenced by the copied spec is itself declared in the output.
|
|
39
52
|
let symbol = callee.getSymbol();
|
|
@@ -56,7 +69,7 @@ function registerExternIfCrossFile(callee, sourceFile) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
|
-
function detectCrossFileExtern(callee, sourceFile) {
|
|
72
|
+
function detectCrossFileExtern(callee, sourceFile, sigTypesOut) {
|
|
60
73
|
let symbol = callee.getSymbol();
|
|
61
74
|
if (!symbol)
|
|
62
75
|
return null;
|
|
@@ -92,12 +105,32 @@ function detectCrossFileExtern(callee, sourceFile) {
|
|
|
92
105
|
// kept): a bare `TMsg`, not `import("/abs/path/transcript").TMsg` — the
|
|
93
106
|
// importing module declares the datatype locally, so the axiom must use the
|
|
94
107
|
// local name.
|
|
95
|
-
|
|
108
|
+
// NoTruncation: a wide expansion (e.g. an alias for a large string-literal
|
|
109
|
+
// union, not importable at the call site) must print whole — a truncated
|
|
110
|
+
// union is unparseable and synthesizes an opaque decl named by its own text.
|
|
111
|
+
const externTypeText = (t) => t.getText(callee, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts.TypeFormatFlags.NoTruncation);
|
|
96
112
|
const params = sig.getParameters().map(p => ({
|
|
97
113
|
name: p.getName(),
|
|
98
114
|
tsType: externTypeText(p.getTypeAtLocation(callee)),
|
|
99
115
|
}));
|
|
100
116
|
const returnType = externTypeText(sig.getReturnType());
|
|
117
|
+
for (const p of sig.getParameters()) {
|
|
118
|
+
sigTypesOut.push({ type: p.getTypeAtLocation(callee), node: callee });
|
|
119
|
+
}
|
|
120
|
+
sigTypesOut.push({ type: sig.getReturnType(), node: callee });
|
|
121
|
+
// Also seed from the source declaration's syntactic type nodes: symbol-based
|
|
122
|
+
// types can drop the alias symbol (the printed signature then names an alias
|
|
123
|
+
// like `TypeDecls` that would otherwise synthesize opaque), while a type
|
|
124
|
+
// node's type keeps it.
|
|
125
|
+
const srcDecl = externalDecl;
|
|
126
|
+
for (const sp of srcDecl.getParameters?.() ?? []) {
|
|
127
|
+
const tn = sp.getTypeNode?.();
|
|
128
|
+
if (tn)
|
|
129
|
+
sigTypesOut.push({ type: tn.getType(), node: tn });
|
|
130
|
+
}
|
|
131
|
+
const rtn = srcDecl.getReturnTypeNode?.();
|
|
132
|
+
if (rtn)
|
|
133
|
+
sigTypesOut.push({ type: rtn.getType(), node: rtn });
|
|
101
134
|
let qualified;
|
|
102
135
|
if (Node.isPropertyAccessExpression(callee)) {
|
|
103
136
|
qualified = `${callee.getExpression().getText()}.${callee.getName()}`;
|
|
@@ -307,10 +340,11 @@ function extractExpr(node) {
|
|
|
307
340
|
if (Node.isNumericLiteral(node)) {
|
|
308
341
|
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
309
342
|
}
|
|
310
|
-
// BigInt literal (e.g. 32n, 0xffffn) — integer, with bigint division
|
|
343
|
+
// BigInt literal (e.g. 32n, 0xffffn) — exact integer, with bigint division
|
|
344
|
+
// semantics. Kept as a decimal string: `getLiteralValue()`/`Number()` would
|
|
345
|
+
// round anything past 2^53 (`9007199254740993n` → `9007199254740992`).
|
|
311
346
|
if (Node.isBigIntLiteral(node)) {
|
|
312
|
-
|
|
313
|
-
return { kind: "num", value: Number(text), big: true };
|
|
347
|
+
return { kind: "bigint", value: normalizeBigIntLiteral(node.getText()) };
|
|
314
348
|
}
|
|
315
349
|
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
316
350
|
if (Node.isTemplateExpression(node)) {
|
|
@@ -462,10 +496,9 @@ function extractExpr(node) {
|
|
|
462
496
|
const typeNode = p.getTypeNode();
|
|
463
497
|
return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
|
|
464
498
|
});
|
|
465
|
-
//
|
|
466
|
-
// return-position record literals
|
|
467
|
-
const
|
|
468
|
-
const returnTsType = retNode ? typeToString(node.getReturnType()) : undefined;
|
|
499
|
+
// Return type from the checker — inferred when unannotated — so resolve can
|
|
500
|
+
// type return-position record literals and give the lambda a real fn type.
|
|
501
|
+
const returnTsType = typeToString(node.getReturnType());
|
|
469
502
|
const body = node.getBody();
|
|
470
503
|
if (Node.isExpression(body)) {
|
|
471
504
|
return { kind: "lambda", params, body: extractExpr(body), returnTsType };
|
|
@@ -688,8 +721,14 @@ function extractTypeDecl(decl, extraDecls) {
|
|
|
688
721
|
continue;
|
|
689
722
|
let tsType = typeToString(prop.getTypeAtLocation(decl));
|
|
690
723
|
const propDecl = prop.getDeclarations()[0];
|
|
691
|
-
|
|
692
|
-
|
|
724
|
+
tsType = declaredTypeTextIfBetter(propDecl, tsType);
|
|
725
|
+
if (propDecl && propDecl.hasQuestionToken?.()) {
|
|
726
|
+
// Normalize checker output that puts `undefined` first, then
|
|
727
|
+
// ensure exactly one trailing `| undefined`.
|
|
728
|
+
if (tsType.startsWith("undefined | "))
|
|
729
|
+
tsType = `${tsType.slice("undefined | ".length)} | undefined`;
|
|
730
|
+
else if (!tsType.includes(" | undefined"))
|
|
731
|
+
tsType = `${tsType} | undefined`;
|
|
693
732
|
}
|
|
694
733
|
fields.push({ name: prop.getName(), tsType });
|
|
695
734
|
}
|
|
@@ -780,12 +819,18 @@ function extractRecord(name, type, locationNode, overrides, extraDecls) {
|
|
|
780
819
|
}
|
|
781
820
|
const propType = prop.getTypeAtLocation(locationNode);
|
|
782
821
|
let tsType = typeToString(propType);
|
|
822
|
+
const propDecl = prop.getDeclarations()[0];
|
|
823
|
+
tsType = declaredTypeTextIfBetter(propDecl, tsType);
|
|
783
824
|
// Optional property: `foo?: T` reports as `T` (ts-morph strips the
|
|
784
825
|
// `| undefined` from a question-token type). Add it back so the field
|
|
785
826
|
// resolves to `Optional<T>`.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
827
|
+
if (propDecl && propDecl.hasQuestionToken?.()) {
|
|
828
|
+
// Normalize checker output that puts `undefined` first, then ensure
|
|
829
|
+
// exactly one trailing `| undefined`.
|
|
830
|
+
if (tsType.startsWith("undefined | "))
|
|
831
|
+
tsType = `${tsType.slice("undefined | ".length)} | undefined`;
|
|
832
|
+
else if (!tsType.includes(" | undefined"))
|
|
833
|
+
tsType = `${tsType} | undefined`;
|
|
789
834
|
}
|
|
790
835
|
// Inline anonymous object types: ts-morph names them __type.
|
|
791
836
|
// Generate a synthetic named record and reference it by name instead.
|
|
@@ -832,6 +877,23 @@ function findDiscriminant(members) {
|
|
|
832
877
|
}
|
|
833
878
|
return null;
|
|
834
879
|
}
|
|
880
|
+
/** Recover a field's *declared* type text when the semantic printer degraded
|
|
881
|
+
* to ts-morph's anonymous `__type` — a self-referential alias reached
|
|
882
|
+
* through a `| null` union expands structurally and loses its name. The
|
|
883
|
+
* syntactic node text preserves the alias spelling (`TExpr | null`). Only
|
|
884
|
+
* plain reference text is used: inline object literals (containing `{`)
|
|
885
|
+
* keep the `__type` marker so record synthesis can handle them. */
|
|
886
|
+
function declaredTypeTextIfBetter(propDecl, tsType) {
|
|
887
|
+
if (!tsType.includes("__type") || !propDecl)
|
|
888
|
+
return tsType;
|
|
889
|
+
const tn = propDecl.getTypeNode?.();
|
|
890
|
+
if (!tn)
|
|
891
|
+
return tsType;
|
|
892
|
+
const text = tn.getText();
|
|
893
|
+
if (text.includes("__type") || text.includes("{"))
|
|
894
|
+
return tsType;
|
|
895
|
+
return text;
|
|
896
|
+
}
|
|
835
897
|
function typeToString(type) {
|
|
836
898
|
if (type.isUndefined())
|
|
837
899
|
return "undefined";
|
|
@@ -1031,6 +1093,7 @@ function renameRawExpr(e, from, to) {
|
|
|
1031
1093
|
switch (e.kind) {
|
|
1032
1094
|
case "var": return e.name === from ? { kind: "var", name: to } : e;
|
|
1033
1095
|
case "num":
|
|
1096
|
+
case "bigint":
|
|
1034
1097
|
case "str":
|
|
1035
1098
|
case "bool":
|
|
1036
1099
|
case "result":
|
|
@@ -1870,6 +1933,7 @@ export function extractModule(sourceFile) {
|
|
|
1870
1933
|
// `extractExpr` during call extraction (only symbols *actually used* end up
|
|
1871
1934
|
// here), deduped by qualified name.
|
|
1872
1935
|
_externs.clear();
|
|
1936
|
+
_externSigTypes.length = 0;
|
|
1873
1937
|
// Share the module's ts-morph Project with parseTsType (scratch source file
|
|
1874
1938
|
// for type-string parsing). Done before declare-type parsing so any
|
|
1875
1939
|
// parseTsType call downstream uses the same Project.
|
|
@@ -2431,13 +2495,12 @@ export function extractModule(sourceFile) {
|
|
|
2431
2495
|
// Resolve imported types: extract types referenced in function signatures but not in this file
|
|
2432
2496
|
const knownTypes = new Set(typeDecls.map(d => d.name));
|
|
2433
2497
|
const builtins = new Set(["Map", "Set", "Array", "String", "Number", "Boolean", "Promise", "Date", "RegExp", "Error"]);
|
|
2498
|
+
const visitedTypes = new Set();
|
|
2434
2499
|
function resolveType(t, locationNode) {
|
|
2435
|
-
//
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
}
|
|
2440
|
-
// Resolve type aliases (e.g. string unions imported from other files)
|
|
2500
|
+
// Resolve type aliases (e.g. string unions imported from other files).
|
|
2501
|
+
// BEFORE the visited guard: the same interned compilerType can arrive both
|
|
2502
|
+
// with and without its alias symbol (getTypeAtLocation drops it), and an
|
|
2503
|
+
// aliasless first visit must not suppress the alias extraction.
|
|
2441
2504
|
const alias = t.getAliasSymbol();
|
|
2442
2505
|
if (alias) {
|
|
2443
2506
|
const aliasName = alias.getName();
|
|
@@ -2470,6 +2533,18 @@ export function extractModule(sourceFile) {
|
|
|
2470
2533
|
}
|
|
2471
2534
|
}
|
|
2472
2535
|
}
|
|
2536
|
+
// Recursion guard: recursive unions (Expr → variant → body: Expr) are
|
|
2537
|
+
// reachable now that anonymous variant fields are walked below. Keyed on
|
|
2538
|
+
// the compiler's interned Type object — alias names are not enough,
|
|
2539
|
+
// because getTypeAtLocation can drop the alias symbol.
|
|
2540
|
+
if (visitedTypes.has(t.compilerType))
|
|
2541
|
+
return;
|
|
2542
|
+
visitedTypes.add(t.compilerType);
|
|
2543
|
+
// Unwrap arrays and generics to find user-defined types
|
|
2544
|
+
if (t.isArray()) {
|
|
2545
|
+
resolveType(t.getArrayElementTypeOrThrow(), locationNode);
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2473
2548
|
if (t.isUnion()) {
|
|
2474
2549
|
for (const u of t.getUnionTypes())
|
|
2475
2550
|
resolveType(u, locationNode);
|
|
@@ -2492,6 +2567,15 @@ export function extractModule(sourceFile) {
|
|
|
2492
2567
|
}
|
|
2493
2568
|
}
|
|
2494
2569
|
}
|
|
2570
|
+
else if (t.isObject() && (!name || name.startsWith("__")) && t.getCallSignatures().length === 0) {
|
|
2571
|
+
// Anonymous object type — typically a variant of an imported
|
|
2572
|
+
// discriminated union. There is no decl to extract, but its fields can
|
|
2573
|
+
// reference named types (`arms: MatchArm[]`) that downstream passes
|
|
2574
|
+
// need declared, so walk them.
|
|
2575
|
+
for (const prop of t.getProperties()) {
|
|
2576
|
+
resolveType(prop.getTypeAtLocation(locationNode), locationNode);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2495
2579
|
}
|
|
2496
2580
|
for (let i = 0; i < fnsToExtract.length; i++) {
|
|
2497
2581
|
const f = fnsToExtract[i];
|
|
@@ -2507,6 +2591,9 @@ export function extractModule(sourceFile) {
|
|
|
2507
2591
|
resolveType(p.getType(), p);
|
|
2508
2592
|
}
|
|
2509
2593
|
}
|
|
2594
|
+
// Extern signature types: see _externSigTypes.
|
|
2595
|
+
for (const r of _externSigTypes)
|
|
2596
|
+
resolveType(r.type, r.node);
|
|
2510
2597
|
// Resolve anonymous object return types into synthetic named types
|
|
2511
2598
|
for (let i = 0; i < fnsToExtract.length; i++) {
|
|
2512
2599
|
const f = fnsToExtract[i];
|