lemmascript 0.5.1 → 0.5.3
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 +13 -4
- package/package.json +3 -2
- package/tools/dist/autohavoc.js +536 -0
- package/tools/dist/dafny-emit.js +87 -13
- package/tools/dist/extract.js +175 -16
- package/tools/dist/lean-emit.js +69 -9
- package/tools/dist/lsc.js +5 -1
- package/tools/dist/narrow.js +32 -4
- package/tools/dist/peephole.js +3 -0
- package/tools/dist/resolve.js +103 -10
- package/tools/dist/transform.js +99 -10
- package/tools/dist/typedir.js +4 -1
- package/tools/dist/types.js +12 -2
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -45,11 +45,16 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
45
45
|
"codatatype", "nameonly", "twostate", "opaque", "replaceable", "colemma",
|
|
46
46
|
"copredicate", "inductive",
|
|
47
47
|
]);
|
|
48
|
+
// The Dafny out-parameter name for the method currently being emitted. Default
|
|
49
|
+
// `res`, but bumped (e.g. `res_`) when a parameter is named `res` — set by
|
|
50
|
+
// methodHeader and reset per decl. `\result` in an ensures must use the *same*
|
|
51
|
+
// name, so escapeName routes it here.
|
|
52
|
+
let _resultName = "res";
|
|
48
53
|
function escapeName(name) {
|
|
49
54
|
// \result is carried through the IR as the var name "\\result"; render it
|
|
50
|
-
// as
|
|
55
|
+
// as the current method's out-parameter name.
|
|
51
56
|
if (name === "\\result")
|
|
52
|
-
return
|
|
57
|
+
return _resultName;
|
|
53
58
|
if (DAFNY_KEYWORDS.has(name))
|
|
54
59
|
return `${name}_`;
|
|
55
60
|
// Dafny doesn't allow identifiers starting with _
|
|
@@ -66,7 +71,17 @@ function paramList(params) {
|
|
|
66
71
|
* `returns (res: ())` on a void method fails verification. */
|
|
67
72
|
function methodHeader(prefix, params, returnType) {
|
|
68
73
|
const sig = `${prefix}(${paramList(params)})`;
|
|
69
|
-
|
|
74
|
+
if (returnType.kind === "void")
|
|
75
|
+
return sig;
|
|
76
|
+
// The out-parameter is `res` by default, but a parameter named `res` (e.g. an
|
|
77
|
+
// Express handler's `(req, res)`) would collide; pick a fresh name and record
|
|
78
|
+
// it so `\result` references in the ensures/body resolve to the same name.
|
|
79
|
+
const taken = new Set(params.map(p => escapeName(p.name)));
|
|
80
|
+
let resName = "res";
|
|
81
|
+
while (taken.has(resName))
|
|
82
|
+
resName += "_";
|
|
83
|
+
_resultName = resName;
|
|
84
|
+
return `${sig} returns (${resName}: ${tyToDafny(returnType)})`;
|
|
70
85
|
}
|
|
71
86
|
// ── Lean op → Dafny op ─────────────────────────────────────
|
|
72
87
|
const OP_MAP = {
|
|
@@ -166,6 +181,12 @@ function emitExpr(e) {
|
|
|
166
181
|
return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
|
|
167
182
|
if (e.method === "filter")
|
|
168
183
|
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
184
|
+
// filterMap (synthesized in resolve): drop Nones and unwrap to seq<T>.
|
|
185
|
+
if (e.method === "filterSome") {
|
|
186
|
+
needPreamble("SeqFilterSome");
|
|
187
|
+
needPreamble("OptionType");
|
|
188
|
+
return `SeqFilterSome(${obj})`;
|
|
189
|
+
}
|
|
169
190
|
if (e.method === "every")
|
|
170
191
|
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
171
192
|
if (e.method === "findLast") {
|
|
@@ -350,16 +371,15 @@ function emitExpr(e) {
|
|
|
350
371
|
needPreamble("BitAnd");
|
|
351
372
|
return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
|
|
352
373
|
}
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const left = leftIsReal ? emitExpr(e.left) : `(${emitExpr(e.left)} as real)`;
|
|
359
|
-
const right = rightIsReal ? emitExpr(e.right) : `(${emitExpr(e.right)} as real)`;
|
|
360
|
-
return `(${left} ${op} ${right})`;
|
|
361
|
-
}
|
|
374
|
+
// x | y → BitOr(x, y) (recursive, mirrors BitAnd). Dafny has no `|` on int,
|
|
375
|
+
// only on bitvectors.
|
|
376
|
+
if (e.op === "|") {
|
|
377
|
+
needPreamble("BitOr");
|
|
378
|
+
return `BitOr(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
|
|
362
379
|
}
|
|
380
|
+
// int→real coercion is now injected upstream in transform (toReal nodes),
|
|
381
|
+
// which has full type information — including real-typed variables, not
|
|
382
|
+
// just literals — so no literal-based coercion is needed here.
|
|
363
383
|
return `(${wrapQuantifier(e.left)} ${op} ${emitExpr(e.right)})`;
|
|
364
384
|
}
|
|
365
385
|
case "implies": {
|
|
@@ -399,6 +419,8 @@ function emitExpr(e) {
|
|
|
399
419
|
needPreamble("MathMin");
|
|
400
420
|
needPreamble("MinOfSeq");
|
|
401
421
|
}
|
|
422
|
+
if (e.fn === "Perm")
|
|
423
|
+
needPreamble("Perm");
|
|
402
424
|
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
403
425
|
}
|
|
404
426
|
case "field": {
|
|
@@ -414,6 +436,8 @@ function emitExpr(e) {
|
|
|
414
436
|
case "toNat":
|
|
415
437
|
// Dafny doesn't need toNat — just emit the inner expression
|
|
416
438
|
return emitExpr(e.expr);
|
|
439
|
+
case "toReal":
|
|
440
|
+
return `(${emitExpr(e.expr)} as real)`;
|
|
417
441
|
case "index": {
|
|
418
442
|
const obj = emitExpr(e.arr);
|
|
419
443
|
const idx = emitExpr(e.idx);
|
|
@@ -522,6 +546,10 @@ function emitStmt(s, indent) {
|
|
|
522
546
|
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
523
547
|
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
524
548
|
case "assign":
|
|
549
|
+
// Transform.ts lowers a bare expression statement to an assign with target _
|
|
550
|
+
// so special case this to Dafny's anonymous binding.
|
|
551
|
+
if (s.target === "_")
|
|
552
|
+
return `${pad}var _ := ${emitExpr(s.value)};`;
|
|
525
553
|
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
526
554
|
case "ghostLet":
|
|
527
555
|
return `${pad}ghost var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
@@ -593,13 +621,33 @@ function emitStmt(s, indent) {
|
|
|
593
621
|
}
|
|
594
622
|
// ── Declaration emission ────────────────────────────────────
|
|
595
623
|
function emitDecl(d) {
|
|
624
|
+
_resultName = "res"; // default; methodHeader bumps it if a param is named `res`
|
|
596
625
|
switch (d.kind) {
|
|
597
626
|
case "inductive": {
|
|
598
627
|
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
628
|
+
// Dafny requires a destructor shared across constructors to have a single
|
|
629
|
+
// type. Two TS variants can legitimately share a field name with different
|
|
630
|
+
// types (e.g. label.targetId: string vs leaf.targetId: string?). Detect
|
|
631
|
+
// such collisions and make those destructors per-constructor unique. Safe:
|
|
632
|
+
// variant-field reads lower to positional match bindings, never named
|
|
633
|
+
// destructors on the union (a name shared with differing types isn't even
|
|
634
|
+
// accessible on the union type in TS), so nothing references the old name.
|
|
635
|
+
const typesByField = new Map();
|
|
636
|
+
for (const c of d.constructors)
|
|
637
|
+
for (const f of c.fields) {
|
|
638
|
+
let s = typesByField.get(f.name);
|
|
639
|
+
if (!s) {
|
|
640
|
+
s = new Set();
|
|
641
|
+
typesByField.set(f.name, s);
|
|
642
|
+
}
|
|
643
|
+
s.add(tyToDafny(f.type));
|
|
644
|
+
}
|
|
645
|
+
const collides = new Set([...typesByField].filter(([, s]) => s.size > 1).map(([n]) => n));
|
|
599
646
|
const ctors = d.constructors.map(c => {
|
|
600
647
|
if (c.fields.length === 0)
|
|
601
648
|
return escapeName(c.name);
|
|
602
|
-
|
|
649
|
+
const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name}` } : f);
|
|
650
|
+
return `${escapeName(c.name)}(${paramList(fields)})`;
|
|
603
651
|
});
|
|
604
652
|
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
605
653
|
}
|
|
@@ -609,6 +657,11 @@ function emitDecl(d) {
|
|
|
609
657
|
case "type-alias": {
|
|
610
658
|
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
611
659
|
}
|
|
660
|
+
case "opaque-type": {
|
|
661
|
+
// Abstract type — no definition. `(==)` so it can sit inside datatypes
|
|
662
|
+
// that derive structural equality. Never constructed or destructured.
|
|
663
|
+
return `type ${d.name}(==)`;
|
|
664
|
+
}
|
|
612
665
|
case "def": {
|
|
613
666
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
614
667
|
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
@@ -724,6 +777,14 @@ const BIT_AND = `function BitAnd(x: int, y: int): int
|
|
|
724
777
|
if x == 0 || y == 0 then 0
|
|
725
778
|
else 2 * BitAnd(x / 2, y / 2) + (if x % 2 == 1 && y % 2 == 1 then 1 else 0)
|
|
726
779
|
}`;
|
|
780
|
+
const BIT_OR = `function BitOr(x: int, y: int): int
|
|
781
|
+
requires x >= 0 && y >= 0
|
|
782
|
+
decreases x
|
|
783
|
+
{
|
|
784
|
+
if x == 0 then y
|
|
785
|
+
else if y == 0 then x
|
|
786
|
+
else 2 * BitOr(x / 2, y / 2) + (if x % 2 == 1 || y % 2 == 1 then 1 else 0)
|
|
787
|
+
}`;
|
|
727
788
|
const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
728
789
|
requires b != 0
|
|
729
790
|
{
|
|
@@ -743,6 +804,12 @@ const CEIL_REAL = `function CeilReal(x: real): int
|
|
|
743
804
|
if x == (x.Floor as real) then x.Floor
|
|
744
805
|
else x.Floor + 1
|
|
745
806
|
}`;
|
|
807
|
+
const SEQ_FILTER_SOME = `function SeqFilterSome<T>(xs: seq<Option<T>>): seq<T>
|
|
808
|
+
ensures |SeqFilterSome(xs)| <= |xs|
|
|
809
|
+
{
|
|
810
|
+
if |xs| == 0 then []
|
|
811
|
+
else (if xs[0].Some? then [xs[0].value] else []) + SeqFilterSome(xs[1..])
|
|
812
|
+
}`;
|
|
746
813
|
const SEQ_FIND_INDEX = `function SeqFindIndex<T>(s: seq<T>, p: T -> bool): int
|
|
747
814
|
ensures -1 <= SeqFindIndex(s, p) < |s|
|
|
748
815
|
ensures SeqFindIndex(s, p) >= 0 ==> p(s[SeqFindIndex(s, p)])
|
|
@@ -956,6 +1023,10 @@ const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
|
956
1023
|
else NatToString(n / 10) + [digit]
|
|
957
1024
|
}`;
|
|
958
1025
|
const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
|
|
1026
|
+
// perm(a, b) — `a` and `b` are reorderings of each other (equal as multisets).
|
|
1027
|
+
// Transparent (Dafny unfolds it), so hand-proofs can reason with `multiset`
|
|
1028
|
+
// directly. The `(==)` bound requires the element type to support equality.
|
|
1029
|
+
const PERM = `predicate Perm<T(==)>(a: seq<T>, b: seq<T>) { multiset(a) == multiset(b) }`;
|
|
959
1030
|
const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
960
1031
|
ensures forall x :: x in s <==> x in res
|
|
961
1032
|
ensures |res| == |s|
|
|
@@ -981,11 +1052,13 @@ const PREAMBLE_CODE = [
|
|
|
981
1052
|
["SetToSeq", SET_TO_SEQ],
|
|
982
1053
|
["Pow2", POW2],
|
|
983
1054
|
["BitAnd", BIT_AND],
|
|
1055
|
+
["BitOr", BIT_OR],
|
|
984
1056
|
["JSFloorDiv", JS_FLOOR_DIV],
|
|
985
1057
|
["CeilReal", CEIL_REAL],
|
|
986
1058
|
["FloorReal", FLOOR_REAL],
|
|
987
1059
|
["SeqIndexOf", SEQ_INDEX_OF],
|
|
988
1060
|
["SeqFindIndex", SEQ_FIND_INDEX],
|
|
1061
|
+
["SeqFilterSome", SEQ_FILTER_SOME],
|
|
989
1062
|
["SeqFindLast", SEQ_FIND_LAST],
|
|
990
1063
|
["SeqFlatten", SEQ_FLATTEN],
|
|
991
1064
|
["SeqJoin", SEQ_JOIN],
|
|
@@ -1001,6 +1074,7 @@ const PREAMBLE_CODE = [
|
|
|
1001
1074
|
["MathMax", MATH_MAX],
|
|
1002
1075
|
["MaxOfSeq", MAX_OF_SEQ],
|
|
1003
1076
|
["MinOfSeq", MIN_OF_SEQ],
|
|
1077
|
+
["Perm", PERM],
|
|
1004
1078
|
];
|
|
1005
1079
|
// ── Constructor and record helpers ───────────────────────────
|
|
1006
1080
|
let _recordCtors = new Map();
|
package/tools/dist/extract.js
CHANGED
|
@@ -69,6 +69,10 @@ function detectCrossFileExtern(callee, sourceFile) {
|
|
|
69
69
|
if (decls.length === 0)
|
|
70
70
|
return null;
|
|
71
71
|
const currentPath = sourceFile.getFilePath();
|
|
72
|
+
// A declaration in the current file is authoritative — don't resolve to a
|
|
73
|
+
// same-named definition in another file.
|
|
74
|
+
if (decls.some(d => d.getSourceFile().getFilePath() === currentPath))
|
|
75
|
+
return null;
|
|
72
76
|
const externalDecl = decls.find(d => d.getSourceFile().getFilePath() !== currentPath);
|
|
73
77
|
if (!externalDecl)
|
|
74
78
|
return null;
|
|
@@ -159,6 +163,26 @@ function _synthName(elemName, otherName) {
|
|
|
159
163
|
const sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
160
164
|
return `ArrayOf_${sanitize(elemName)}_Or_${sanitize(otherName)}`;
|
|
161
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Fall-through for a union LS can't model as a tagged union (no runtime type
|
|
168
|
+
* test maps to a tag — e.g. members are unreachable imports with no visible
|
|
169
|
+
* discriminant). Registers an opaque-type TypeDeclInfo and returns its name, so
|
|
170
|
+
* the union becomes one abstract `type` rather than invalid raw-union Dafny.
|
|
171
|
+
*
|
|
172
|
+
* Sound because an opaque type has no constructor and no tag predicate: any
|
|
173
|
+
* attempt to build or type-test the value fails to lower, so it can only be
|
|
174
|
+
* passed through — the one sound use of a union we can't discriminate. Distinct
|
|
175
|
+
* from dropping the field (which collapses values and is unsound): the value is
|
|
176
|
+
* preserved, just uninspectable.
|
|
177
|
+
*/
|
|
178
|
+
function _synthOpaque(memberNames) {
|
|
179
|
+
const sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
180
|
+
const name = `Opaque_${memberNames.map(sanitize).join("_or_")}`;
|
|
181
|
+
if (_synthArrayUnions !== null && !_synthArrayUnions.some(d => d.name === name)) {
|
|
182
|
+
_synthArrayUnions.push({ name, kind: "opaque" });
|
|
183
|
+
}
|
|
184
|
+
return name;
|
|
185
|
+
}
|
|
162
186
|
/**
|
|
163
187
|
* String-level fallback for synth detection on a `T[] | U` shape, used by
|
|
164
188
|
* declare-type field parsing (where no ts-morph TypeNode is available). The
|
|
@@ -277,10 +301,10 @@ function extractExpr(node) {
|
|
|
277
301
|
if (Node.isNumericLiteral(node)) {
|
|
278
302
|
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
279
303
|
}
|
|
280
|
-
// BigInt literal (e.g. 32n, 0xffffn) —
|
|
304
|
+
// BigInt literal (e.g. 32n, 0xffffn) — integer, with bigint division semantics
|
|
281
305
|
if (Node.isBigIntLiteral(node)) {
|
|
282
306
|
const text = node.getText().replace(/n$/, '');
|
|
283
|
-
return { kind: "num", value: Number(text) };
|
|
307
|
+
return { kind: "num", value: Number(text), big: true };
|
|
284
308
|
}
|
|
285
309
|
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
286
310
|
if (Node.isTemplateExpression(node)) {
|
|
@@ -431,12 +455,16 @@ function extractExpr(node) {
|
|
|
431
455
|
const typeNode = p.getTypeNode();
|
|
432
456
|
return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
|
|
433
457
|
});
|
|
458
|
+
// Capture an explicit return annotation (`(x): Out => …`) so resolve can type
|
|
459
|
+
// return-position record literals to their named type instead of a tuple.
|
|
460
|
+
const retNode = node.getReturnTypeNode();
|
|
461
|
+
const returnTsType = retNode ? typeToString(node.getReturnType()) : undefined;
|
|
434
462
|
const body = node.getBody();
|
|
435
463
|
if (Node.isExpression(body)) {
|
|
436
|
-
return { kind: "lambda", params, body: extractExpr(body) };
|
|
464
|
+
return { kind: "lambda", params, body: extractExpr(body), returnTsType };
|
|
437
465
|
}
|
|
438
466
|
if (Node.isBlock(body)) {
|
|
439
|
-
return { kind: "lambda", params, body: extractStmts(body.getStatements()) };
|
|
467
|
+
return { kind: "lambda", params, body: extractStmts(body.getStatements()), returnTsType };
|
|
440
468
|
}
|
|
441
469
|
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
442
470
|
}
|
|
@@ -552,6 +580,12 @@ function extractExpr(node) {
|
|
|
552
580
|
if (Node.isNullLiteral(node)) {
|
|
553
581
|
return { kind: "var", name: "undefined" };
|
|
554
582
|
}
|
|
583
|
+
// `typeof X` — only meaningful in a `typeof X === "string"` discriminator over
|
|
584
|
+
// a synth `U | T[]` union (see narrow's parseTypeofStringCheck); any other use
|
|
585
|
+
// survives to emit and errors there.
|
|
586
|
+
if (Node.isTypeOfExpression(node)) {
|
|
587
|
+
return { kind: "unop", op: "typeof", expr: extractExpr(node.getExpression()) };
|
|
588
|
+
}
|
|
555
589
|
throw new Error(`Unsupported expression: ${node.getText()}`);
|
|
556
590
|
}
|
|
557
591
|
// ── Annotation parsing ───────────────────────────────────────
|
|
@@ -796,7 +830,10 @@ function typeToString(type) {
|
|
|
796
830
|
return "bigint";
|
|
797
831
|
if (type.isString() || type.isStringLiteral())
|
|
798
832
|
return "string";
|
|
799
|
-
|
|
833
|
+
// TS expands `boolean` to the literal union `false | true`; normalize the
|
|
834
|
+
// literals back so the union dedupes to a single `boolean` rather than being
|
|
835
|
+
// mistaken for an unmodelable multi-member union.
|
|
836
|
+
if (type.isBoolean() || type.isBooleanLiteral())
|
|
800
837
|
return "boolean";
|
|
801
838
|
// Named type alias (e.g. Priority = "low" | "medium" | "high") — use the alias name
|
|
802
839
|
if (type.getAliasSymbol()) {
|
|
@@ -838,7 +875,15 @@ function typeToString(type) {
|
|
|
838
875
|
}
|
|
839
876
|
}
|
|
840
877
|
const parts = [...new Set(unionTypes.map(typeToString))];
|
|
841
|
-
|
|
878
|
+
// `undefined`/`null` are optional markers; a single real member with them
|
|
879
|
+
// is an Option, left as `X | undefined` for the optional lowering.
|
|
880
|
+
const real = parts.filter(p => p !== "undefined" && p !== "null");
|
|
881
|
+
if (real.length <= 1)
|
|
882
|
+
return parts.join(" | ");
|
|
883
|
+
// A genuine multi-member union with no tagged-union shape LS can model →
|
|
884
|
+
// a single opaque type. (See _synthOpaque.) Preserve an outer optional.
|
|
885
|
+
const opaque = _synthOpaque(real);
|
|
886
|
+
return real.length === parts.length ? opaque : `${opaque} | undefined`;
|
|
842
887
|
}
|
|
843
888
|
if (type.isTuple()) {
|
|
844
889
|
return `[${type.getTupleElements().map(t => typeToString(t)).join(", ")}]`;
|
|
@@ -1704,6 +1749,22 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1704
1749
|
return [{ name: p.getName(), tsType }];
|
|
1705
1750
|
}),
|
|
1706
1751
|
returnType: (() => {
|
|
1752
|
+
// `async` with no `await`: the `Promise<T>` wrapper is just the calling
|
|
1753
|
+
// convention (the body returns T-typed values), so unwrap to T. Gated on
|
|
1754
|
+
// no `await` — that's the suspension point we can't model, and it only
|
|
1755
|
+
// type-checks inside `async`, so the gate is self-justifying. With `await`
|
|
1756
|
+
// present we leave `Promise<...>` (unmodellable) rather than atomize it.
|
|
1757
|
+
const isAsync = fn.isAsync?.() ?? false;
|
|
1758
|
+
const noAwait = fn.getDescendantsOfKind(SyntaxKind.AwaitExpression).length === 0;
|
|
1759
|
+
if (isAsync && noAwait) {
|
|
1760
|
+
const args = fn.getReturnType().getTypeArguments();
|
|
1761
|
+
if (args.length === 1) {
|
|
1762
|
+
if (args[0].isAny())
|
|
1763
|
+
return "unknown";
|
|
1764
|
+
return _eraseGenerics(typeToString(args[0]));
|
|
1765
|
+
}
|
|
1766
|
+
return "void"; // Promise<void>
|
|
1767
|
+
}
|
|
1707
1768
|
const node = fn.getReturnTypeNode();
|
|
1708
1769
|
if (node && Node.isUnionTypeNode(node))
|
|
1709
1770
|
return _eraseGenerics(_tsTypeFromUnionNode(node));
|
|
@@ -1718,6 +1779,7 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1718
1779
|
ensures: annots.filter(a => a.kind === "ensures").map(a => a.expr),
|
|
1719
1780
|
decreases: annots.find(a => a.kind === "decreases")?.expr ?? null,
|
|
1720
1781
|
pure: hasPureAnnotation(fn, body && Node.isBlock(body) ? body.getStatements() : undefined),
|
|
1782
|
+
autohavoc: false, // set in extractModule (file-level directive or per-function)
|
|
1721
1783
|
typeAnnotations,
|
|
1722
1784
|
body: extractedBody,
|
|
1723
1785
|
line: fn.getStartLineNumber(),
|
|
@@ -1869,63 +1931,144 @@ export function extractModule(sourceFile) {
|
|
|
1869
1931
|
}
|
|
1870
1932
|
}
|
|
1871
1933
|
}
|
|
1934
|
+
// Top-level inline closures: a handler passed directly to a module-level call,
|
|
1935
|
+
// e.g. `app.get("/x", (req, res) => { //@ verify ... })`. "Move" each such
|
|
1936
|
+
// closure to the top level by extracting it as a synthetic named function.
|
|
1937
|
+
// Only top-level call arguments are considered (not nested lambdas), and only
|
|
1938
|
+
// closures carrying a //@ verify (so ordinary callbacks aren't pulled in). The
|
|
1939
|
+
// name is derived from the call's method and route literal (e.g. get_x).
|
|
1940
|
+
const usedNames = new Set(allFns.map(f => f.name));
|
|
1941
|
+
const sanitizeIdent = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "handler";
|
|
1942
|
+
for (const stmt of sourceFile.getStatements()) {
|
|
1943
|
+
if (!Node.isExpressionStatement(stmt))
|
|
1944
|
+
continue;
|
|
1945
|
+
const call = stmt.getExpression();
|
|
1946
|
+
if (!Node.isCallExpression(call))
|
|
1947
|
+
continue;
|
|
1948
|
+
const callee = call.getExpression();
|
|
1949
|
+
const method = Node.isPropertyAccessExpression(callee) ? callee.getName()
|
|
1950
|
+
: Node.isIdentifier(callee) ? callee.getText() : "handler";
|
|
1951
|
+
const routeArg = call.getArguments().find(a => Node.isStringLiteral(a));
|
|
1952
|
+
const route = routeArg && Node.isStringLiteral(routeArg) ? routeArg.getLiteralValue() : "";
|
|
1953
|
+
for (const arg of call.getArguments()) {
|
|
1954
|
+
if (!Node.isArrowFunction(arg))
|
|
1955
|
+
continue;
|
|
1956
|
+
if (!hasLineDirective(arg.getFullText(), "verify"))
|
|
1957
|
+
continue;
|
|
1958
|
+
const base = sanitizeIdent(route ? `${method}_${route}` : method);
|
|
1959
|
+
let name = base, n = 2;
|
|
1960
|
+
while (usedNames.has(name))
|
|
1961
|
+
name = `${base}_${n++}`;
|
|
1962
|
+
usedNames.add(name);
|
|
1963
|
+
allFns.push({ name, node: arg, parentStmt: stmt });
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1872
1966
|
// `//@ extern` on a same-file declaration: register the function as an
|
|
1873
1967
|
// opaque axiom (signature + any //@ requires/ensures), skip its body. Use
|
|
1874
1968
|
// when the function is outside LS's verification model — e.g., wraps a
|
|
1875
1969
|
// regex — but its callers should still be verifiable against an
|
|
1876
1970
|
// uninterpreted predicate. Parallel to auto-extern for cross-file calls,
|
|
1877
1971
|
// and emitted the same way (`function {:axiom} foo(...)` in Dafny).
|
|
1972
|
+
// Match a `//@ <kw>` directive only as the first non-whitespace on a line, so
|
|
1973
|
+
// a mention mid-line in prose or inside a block/JSDoc comment (e.g. "the
|
|
1974
|
+
// `//@ extern` annotation", or ` * //@ extern`) doesn't falsely trigger it.
|
|
1975
|
+
function hasLineDirective(text, kw) {
|
|
1976
|
+
return new RegExp(String.raw `^[ \t]*//@ ${kw}\b`, "m").test(text);
|
|
1977
|
+
}
|
|
1878
1978
|
function hasExtern(f) {
|
|
1879
|
-
if (f.node.getFullText()
|
|
1979
|
+
if (hasLineDirective(f.node.getFullText(), "extern"))
|
|
1880
1980
|
return true;
|
|
1881
1981
|
if (f.parentStmt) {
|
|
1882
1982
|
for (const r of f.parentStmt.getLeadingCommentRanges()) {
|
|
1883
|
-
if (r.getText()
|
|
1983
|
+
if (hasLineDirective(r.getText(), "extern"))
|
|
1884
1984
|
return true;
|
|
1885
1985
|
}
|
|
1886
1986
|
}
|
|
1887
1987
|
return false;
|
|
1888
1988
|
}
|
|
1989
|
+
// `//@ extern NS.method` registers the extern under a *dotted* qualified name,
|
|
1990
|
+
// so a real `NS.method(args)` call dispatches to it (resolve.ts) with no
|
|
1991
|
+
// wrapper — e.g. `//@ extern fs.readFileSync` lets you call `fs.readFileSync`
|
|
1992
|
+
// directly while still discharging its `//@ requires`. The function declaration
|
|
1993
|
+
// just carries the signature/contract; its own name is unused.
|
|
1994
|
+
function externName(f) {
|
|
1995
|
+
const re = /^[ \t]*\/\/@ extern[ \t]+(\S+)/m;
|
|
1996
|
+
const m = f.node.getFullText().match(re);
|
|
1997
|
+
if (m)
|
|
1998
|
+
return m[1];
|
|
1999
|
+
if (f.parentStmt) {
|
|
2000
|
+
for (const r of f.parentStmt.getLeadingCommentRanges()) {
|
|
2001
|
+
const m2 = r.getText().match(re);
|
|
2002
|
+
if (m2)
|
|
2003
|
+
return m2[1];
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
1889
2008
|
for (const f of allFns) {
|
|
1890
2009
|
if (!hasExtern(f))
|
|
1891
2010
|
continue;
|
|
1892
|
-
|
|
2011
|
+
const qualified = externName(f) ?? f.name;
|
|
2012
|
+
const flat = qualified.replace(/\./g, "_");
|
|
2013
|
+
if (_externs.has(qualified))
|
|
1893
2014
|
continue;
|
|
1894
2015
|
const sig = f.node.getType().getCallSignatures()[0];
|
|
1895
2016
|
if (!sig)
|
|
1896
2017
|
continue;
|
|
1897
2018
|
const typeParams = sig.getTypeParameters().map(tp => tp.getText());
|
|
2019
|
+
// Normalize via typeToString (not raw getText): resolves declare-type
|
|
2020
|
+
// shadows and yields bare names, so a param typed by an unreachable import
|
|
2021
|
+
// becomes `AgentMessage`, not `import("/abs/path").AgentMessage`.
|
|
1898
2022
|
const params = sig.getParameters().map(p => ({
|
|
1899
2023
|
name: p.getName(),
|
|
1900
|
-
tsType: p.getTypeAtLocation(f.node)
|
|
2024
|
+
tsType: typeToString(p.getTypeAtLocation(f.node)),
|
|
1901
2025
|
}));
|
|
1902
|
-
const returnType = sig.getReturnType()
|
|
2026
|
+
const returnType = typeToString(sig.getReturnType());
|
|
1903
2027
|
const annots = collectFunctionAnnotations(f.node);
|
|
1904
2028
|
const requires = annots.filter(a => a.kind === "requires").map(a => a.expr);
|
|
1905
2029
|
const ensures = annots.filter(a => a.kind === "ensures").map(a => a.expr);
|
|
1906
|
-
_externs.set(
|
|
2030
|
+
_externs.set(qualified, { qualified, flat, typeParams, params, returnType, requires, ensures });
|
|
1907
2031
|
}
|
|
1908
2032
|
// If any function has //@ verify, only extract those (brownfield mode).
|
|
1909
2033
|
// For expression-body arrows, //@ verify may be on the parent variable statement.
|
|
1910
2034
|
function hasVerify(f) {
|
|
1911
|
-
if (f.node.getFullText()
|
|
2035
|
+
if (hasLineDirective(f.node.getFullText(), "verify"))
|
|
1912
2036
|
return true;
|
|
1913
2037
|
if (f.parentStmt) {
|
|
1914
2038
|
for (const r of f.parentStmt.getLeadingCommentRanges()) {
|
|
1915
|
-
if (r.getText()
|
|
2039
|
+
if (hasLineDirective(r.getText(), "verify"))
|
|
1916
2040
|
return true;
|
|
1917
2041
|
}
|
|
1918
2042
|
}
|
|
1919
2043
|
return false;
|
|
1920
2044
|
}
|
|
1921
|
-
const hasVerifyDirective = sourceFile.getFullText()
|
|
2045
|
+
const hasVerifyDirective = hasLineDirective(sourceFile.getFullText(), "verify");
|
|
1922
2046
|
const nonExternFns = allFns.filter(f => !hasExtern(f));
|
|
1923
2047
|
const fnsToExtract = hasVerifyDirective ? nonExternFns.filter(hasVerify) : nonExternFns;
|
|
2048
|
+
// `//@ autohavoc` — enable the auto-havoc abstraction (see autohavoc.ts).
|
|
2049
|
+
// File-level: a directive at column 0 (top of file) enables it for every
|
|
2050
|
+
// function. Per-function: the annotation attached to a function (or its
|
|
2051
|
+
// parent variable statement), mirroring `//@ verify`.
|
|
2052
|
+
const fileAutohavoc = /^\/\/@ autohavoc\b/m.test(sourceFile.getFullText());
|
|
2053
|
+
function hasAutohavoc(f) {
|
|
2054
|
+
if (fileAutohavoc)
|
|
2055
|
+
return true;
|
|
2056
|
+
if (hasLineDirective(f.node.getFullText(), "autohavoc"))
|
|
2057
|
+
return true;
|
|
2058
|
+
if (f.parentStmt) {
|
|
2059
|
+
for (const r of f.parentStmt.getLeadingCommentRanges()) {
|
|
2060
|
+
if (hasLineDirective(r.getText(), "autohavoc"))
|
|
2061
|
+
return true;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
1924
2066
|
const functions = fnsToExtract.map(f => {
|
|
1925
2067
|
// For expression-body arrows, annotations come from the parent variable statement
|
|
1926
2068
|
const parentAnnots = f.parentStmt ? parseAnnotations(f.parentStmt) : undefined;
|
|
1927
2069
|
const raw = extractFunction(f.node, parentAnnots);
|
|
1928
2070
|
raw.name = f.name; // use the const name, not "<anonymous>"
|
|
2071
|
+
raw.autohavoc = hasAutohavoc(f);
|
|
1929
2072
|
return raw;
|
|
1930
2073
|
});
|
|
1931
2074
|
// Resolve type references in function signatures via ts-morph's type
|
|
@@ -2121,10 +2264,17 @@ export function extractModule(sourceFile) {
|
|
|
2121
2264
|
collectNamesExpr(e.else);
|
|
2122
2265
|
}
|
|
2123
2266
|
}
|
|
2267
|
+
// Signature types (params + return) get base-name stripping below; body /
|
|
2268
|
+
// spec references stay exact-match (so a body `let xs: Hunk[]` doesn't pull
|
|
2269
|
+
// `Hunk` into the filter early and reorder output that resolveType re-adds).
|
|
2270
|
+
const sigTypes = new Set();
|
|
2124
2271
|
for (const fn of functions) {
|
|
2125
|
-
for (const p of fn.params)
|
|
2272
|
+
for (const p of fn.params) {
|
|
2126
2273
|
referencedNames.add(p.tsType);
|
|
2274
|
+
sigTypes.add(p.tsType);
|
|
2275
|
+
}
|
|
2127
2276
|
referencedNames.add(fn.returnType);
|
|
2277
|
+
sigTypes.add(fn.returnType);
|
|
2128
2278
|
collectNames(fn.body);
|
|
2129
2279
|
// Also scan spec annotations for identifier references
|
|
2130
2280
|
for (const spec of [...fn.requires, ...fn.ensures]) {
|
|
@@ -2153,6 +2303,15 @@ export function extractModule(sourceFile) {
|
|
|
2153
2303
|
}
|
|
2154
2304
|
for (const name of referencedNames)
|
|
2155
2305
|
markType(name);
|
|
2306
|
+
// Signature types also mark their base after stripping array/optional
|
|
2307
|
+
// WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
|
|
2308
|
+
// a local `Out[]` keeps `Out`. Wrappers only — never dig into generic args
|
|
2309
|
+
// (`Omit<FilePathOptions, …>` must not pull in the inner type).
|
|
2310
|
+
for (const name of sigTypes) {
|
|
2311
|
+
const base = name.replace(/\s*\|\s*(undefined|null)\s*$/, "").replace(/(\[\])+$/, "").trim();
|
|
2312
|
+
if (base !== name && /^[A-Za-z_]\w*$/.test(base))
|
|
2313
|
+
markType(base);
|
|
2314
|
+
}
|
|
2156
2315
|
typeDecls.splice(0, typeDecls.length, ...typeDecls.filter(d => neededTypes.has(d.name) || declaredNames.has(d.name)));
|
|
2157
2316
|
}
|
|
2158
2317
|
// Resolve imported types: extract types referenced in function signatures but not in this file
|