lemmascript 0.5.19 → 0.5.20
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/package.json +1 -1
- package/tools/dist/builtins.js +1 -0
- package/tools/dist/dafny-emit.js +67 -3
- package/tools/dist/extract.js +67 -16
- package/tools/dist/lean-emit.js +2 -2
- package/tools/dist/resolve.js +89 -49
- package/tools/dist/transform.js +113 -23
package/package.json
CHANGED
package/tools/dist/builtins.js
CHANGED
|
@@ -87,6 +87,7 @@ export const BUILTINS = {
|
|
|
87
87
|
"string.trimStart": { ret: STRING, pure: true },
|
|
88
88
|
"string.toLowerCase": { ret: STRING, pure: true },
|
|
89
89
|
"string.toUpperCase": { ret: STRING, pure: true },
|
|
90
|
+
"string.repeat": { ret: STRING, pure: true },
|
|
90
91
|
"string.slice": { ret: STRING, pure: true },
|
|
91
92
|
"string.substring": { ret: STRING, pure: true },
|
|
92
93
|
"string.split": { pure: true,
|
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -80,6 +80,24 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
80
80
|
// by methodHeader and reset per decl. `\result` in an ensures must use the
|
|
81
81
|
// *same* name, so escapeName routes it here.
|
|
82
82
|
let _resultName = "res";
|
|
83
|
+
// User-type names appearing as the type of a havoc anywhere in the module —
|
|
84
|
+
// populated per file by emitDafnyFile, read by the opaque-type case.
|
|
85
|
+
const _havocedTypeNames = new Set();
|
|
86
|
+
/** Collect the user-type names of every havoc in a decl tree. */
|
|
87
|
+
function collectHavocedTypeNames(v, out) {
|
|
88
|
+
if (Array.isArray(v)) {
|
|
89
|
+
for (const x of v)
|
|
90
|
+
collectHavocedTypeNames(x, out);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (v === null || typeof v !== "object")
|
|
94
|
+
return;
|
|
95
|
+
const n = v;
|
|
96
|
+
if (n.kind === "havoc" && n.type?.kind === "user")
|
|
97
|
+
out.add(n.type.name);
|
|
98
|
+
for (const x of Object.values(v))
|
|
99
|
+
collectHavocedTypeNames(x, out);
|
|
100
|
+
}
|
|
83
101
|
// ── Dafny name allocation ──────────────────────────────────
|
|
84
102
|
//
|
|
85
103
|
// freshName (names.ts) freshens in the *raw TS* namespace — but that is not the
|
|
@@ -145,6 +163,13 @@ function escapeName(name) {
|
|
|
145
163
|
return user;
|
|
146
164
|
return escapeGeneratedName(name);
|
|
147
165
|
}
|
|
166
|
+
function isEmittedUserName(name) {
|
|
167
|
+
for (const emitted of _userDafnyNames.values()) {
|
|
168
|
+
if (emitted === name)
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
148
173
|
/** Allocate a toolchain-generated name (an ANF temp, a comprehension binder, a
|
|
149
174
|
* companion `_ensures` lemma). Escapes to a base, then freshens in the Dafny
|
|
150
175
|
* namespace so it can't collapse onto an escaped user name. Bypasses the user
|
|
@@ -447,6 +472,10 @@ function emitExpr(e) {
|
|
|
447
472
|
return `(|${obj}| >= |${args[0]}| && ${obj}[..|${args[0]}|] == ${args[0]})`;
|
|
448
473
|
if (e.method === "charCodeAt")
|
|
449
474
|
return `(${obj}[${args[0]}] as int)`;
|
|
475
|
+
if (e.method === "repeat") {
|
|
476
|
+
needPreamble("StringRepeat");
|
|
477
|
+
return `StringRepeat(${obj}, ${args[0]})`;
|
|
478
|
+
}
|
|
450
479
|
}
|
|
451
480
|
// Map methods
|
|
452
481
|
if (ty === "map") {
|
|
@@ -515,7 +544,7 @@ function emitExpr(e) {
|
|
|
515
544
|
// Discriminant check: x == .Ctor → x.Ctor?
|
|
516
545
|
const op = mapOp(e.op);
|
|
517
546
|
if ((op === "==" || op === "!=") && e.right.kind === "constructor") {
|
|
518
|
-
const ctorName =
|
|
547
|
+
const ctorName = dafnyCtorName(e.right.name.replace(/^\./, ""));
|
|
519
548
|
const pred = `${emitExpr(e.left)}.${ctorName}?`;
|
|
520
549
|
return op === "!=" ? `(!${pred})` : pred;
|
|
521
550
|
}
|
|
@@ -582,6 +611,8 @@ function emitExpr(e) {
|
|
|
582
611
|
needPreamble("CeilReal");
|
|
583
612
|
if (e.fn === "FloorReal")
|
|
584
613
|
needPreamble("FloorReal");
|
|
614
|
+
if (e.fn === "StringFromCharCode")
|
|
615
|
+
needPreamble("StringFromCharCode");
|
|
585
616
|
if (e.fn === "NatToString")
|
|
586
617
|
needPreamble("NatToString");
|
|
587
618
|
if (e.fn === "IntToString") {
|
|
@@ -611,7 +642,12 @@ function emitExpr(e) {
|
|
|
611
642
|
// tags come from source strings ("spec-pure") that escapeName leaves alone.
|
|
612
643
|
if (e.ctorOf) {
|
|
613
644
|
const ctor = dafnyCtorName(e.fn);
|
|
614
|
-
|
|
645
|
+
// A source local may have the same name as a discriminated-union
|
|
646
|
+
// variant (`const error = ...; return { kind: "error", error }`).
|
|
647
|
+
// The bare `error(error)` is then parsed as a call through the local.
|
|
648
|
+
// Qualify whenever the emitted constructor spelling is already claimed
|
|
649
|
+
// in the user namespace, as well as when two datatypes share it.
|
|
650
|
+
return _ambiguousCtors.has(e.fn) || isEmittedUserName(ctor)
|
|
615
651
|
? `${e.ctorOf}.${ctor}(${args.join(", ")})`
|
|
616
652
|
: `${ctor}(${args.join(", ")})`;
|
|
617
653
|
}
|
|
@@ -875,7 +911,10 @@ function emitDecl(d) {
|
|
|
875
911
|
case "opaque-type": {
|
|
876
912
|
// Abstract type — no definition. `(==)` so it can sit inside datatypes
|
|
877
913
|
// that derive structural equality. Never constructed or destructured.
|
|
878
|
-
|
|
914
|
+
// `0` (auto-init) only when a havoc of this type needs a witness to
|
|
915
|
+
// satisfy definite assignment — `var x: T := *` requires it.
|
|
916
|
+
const autoInit = _havocedTypeNames.has(d.name) ? ", 0" : "";
|
|
917
|
+
return `type ${escapeName(d.name)}(==${autoInit})`;
|
|
879
918
|
}
|
|
880
919
|
case "def": {
|
|
881
920
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
@@ -1269,6 +1308,27 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
|
|
|
1269
1308
|
var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
|
|
1270
1309
|
[upper] + StringToUpper(s[1..])
|
|
1271
1310
|
}`;
|
|
1311
|
+
// `String.fromCharCode(n)` — the inverse of `s.charCodeAt(i)`'s `(s[i] as int)`.
|
|
1312
|
+
// Dafny's `char` is a Unicode scalar value, so the argument must miss the
|
|
1313
|
+
// surrogate range; that is the `requires`, discharged at each call site. The two
|
|
1314
|
+
// `ensures` give callers the round-trip law without unfolding the body.
|
|
1315
|
+
const STRING_FROM_CHAR_CODE = `function StringFromCharCode(n: int): string
|
|
1316
|
+
requires 0 <= n < 0xD800 || 0xE000 <= n < 0x110000
|
|
1317
|
+
ensures |StringFromCharCode(n)| == 1
|
|
1318
|
+
ensures (StringFromCharCode(n)[0] as int) == n
|
|
1319
|
+
{
|
|
1320
|
+
[n as char]
|
|
1321
|
+
}`;
|
|
1322
|
+
// `s.repeat(n)` — n copies of s, concatenated. The per-index ensures is stated
|
|
1323
|
+
// for the single-character receiver (the common case: padding with one digit).
|
|
1324
|
+
const STRING_REPEAT = `function StringRepeat(s: string, n: int): string
|
|
1325
|
+
requires n >= 0
|
|
1326
|
+
ensures |StringRepeat(s, n)| == |s| * n
|
|
1327
|
+
ensures |s| == 1 ==> forall i :: 0 <= i < n ==> StringRepeat(s, n)[i] == s[0]
|
|
1328
|
+
decreases n
|
|
1329
|
+
{
|
|
1330
|
+
if n == 0 then "" else s + StringRepeat(s, n - 1)
|
|
1331
|
+
}`;
|
|
1272
1332
|
const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
|
|
1273
1333
|
const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
|
|
1274
1334
|
const MAX_OF_SEQ = `function MaxOfSeq(s: seq<int>): int
|
|
@@ -1386,6 +1446,8 @@ const PREAMBLE_CODE = [
|
|
|
1386
1446
|
["StringTrim", STRING_TRIM],
|
|
1387
1447
|
["StringToLower", STRING_TO_LOWER],
|
|
1388
1448
|
["StringToUpper", STRING_TO_UPPER],
|
|
1449
|
+
["StringFromCharCode", STRING_FROM_CHAR_CODE],
|
|
1450
|
+
["StringRepeat", STRING_REPEAT],
|
|
1389
1451
|
["NatToString", NAT_TO_STRING],
|
|
1390
1452
|
["IntToString", INT_TO_STRING],
|
|
1391
1453
|
["MathAbs", MATH_ABS],
|
|
@@ -1512,6 +1574,8 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1512
1574
|
resetDafnyNameCache();
|
|
1513
1575
|
buildRecordCtorMap(file.decls);
|
|
1514
1576
|
_neededPreambles.clear();
|
|
1577
|
+
_havocedTypeNames.clear();
|
|
1578
|
+
collectHavocedTypeNames(file.decls, _havocedTypeNames);
|
|
1515
1579
|
// Track successfully emitted pure defs — method wrappers are only
|
|
1516
1580
|
// skipped when the corresponding pure def was actually emitted.
|
|
1517
1581
|
const emittedPureDefs = new Set();
|
package/tools/dist/extract.js
CHANGED
|
@@ -11,6 +11,25 @@ import { setUserNames, freshName } from "./names.js";
|
|
|
11
11
|
// ── Expression extraction ────────────────────────────────────
|
|
12
12
|
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
13
13
|
let _havocKey = null;
|
|
14
|
+
/** A leading `//@ havoc`, `//@ havoc : Type`, or `//@ havoc <key>` directive. */
|
|
15
|
+
function havocDirective(s) {
|
|
16
|
+
const m = s.getLeadingCommentRanges()
|
|
17
|
+
.map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
|
|
18
|
+
.find(m => m !== null);
|
|
19
|
+
return m ? { type: m[1]?.trim() ?? null, key: m[2] ?? null } : null;
|
|
20
|
+
}
|
|
21
|
+
/** Run `fn` with `key` as the subexpression havoc key, restoring the outer one. */
|
|
22
|
+
function withHavocKey(key, fn) {
|
|
23
|
+
const saved = _havocKey;
|
|
24
|
+
if (key)
|
|
25
|
+
_havocKey = key;
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
_havocKey = saved;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
/** Auto-detected cross-file calls. Populated by `extractExpr` whenever it sees
|
|
15
34
|
* a call `Obj.method(...)` or `foo(...)` whose ts-morph symbol resolves to a
|
|
16
35
|
* different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat>`.
|
|
@@ -326,14 +345,18 @@ function _eraseGenerics(tsType) {
|
|
|
326
345
|
return tsType;
|
|
327
346
|
}
|
|
328
347
|
function extractExpr(node) {
|
|
329
|
-
// Havoc key matching: replace matching calls with havoc expression
|
|
330
|
-
if (_havocKey && Node.isCallExpression(node)) {
|
|
348
|
+
// Havoc key matching: replace matching calls or new expressions with havoc expression
|
|
349
|
+
if (_havocKey && (Node.isCallExpression(node) || Node.isNewExpression(node))) {
|
|
331
350
|
const fnExpr = node.getExpression();
|
|
332
351
|
const name = Node.isPropertyAccessExpression(fnExpr) ? fnExpr.getName()
|
|
333
352
|
: Node.isIdentifier(fnExpr) ? fnExpr.getText()
|
|
334
353
|
: null;
|
|
335
354
|
if (name === _havocKey) {
|
|
336
|
-
|
|
355
|
+
// Prefer the contextual type: the abstracted value stands in where the
|
|
356
|
+
// surrounding code expects it, and a subclass (`new SdkError` into an
|
|
357
|
+
// `Error` field) has no subtyping relation once both are opaque.
|
|
358
|
+
const ty = node.getContextualType() ?? node.getType();
|
|
359
|
+
return { kind: "havoc", tsType: typeToString(ty) };
|
|
337
360
|
}
|
|
338
361
|
}
|
|
339
362
|
// Numeric literal
|
|
@@ -1242,12 +1265,10 @@ function extractStmts(stmts) {
|
|
|
1242
1265
|
continue;
|
|
1243
1266
|
}
|
|
1244
1267
|
if (Node.isVariableStatement(s)) {
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const
|
|
1249
|
-
const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
|
|
1250
|
-
const isHavoc = !!havocMatch;
|
|
1268
|
+
const havoc = havocDirective(s);
|
|
1269
|
+
const havocType = havoc?.type ?? null; // //@ havoc : Type
|
|
1270
|
+
const havocKey = havoc?.key ?? null; // //@ havoc key
|
|
1271
|
+
const isHavoc = !!havoc;
|
|
1251
1272
|
for (const d of s.getDeclarations()) {
|
|
1252
1273
|
// Havoc on destructuring: emit each named binding as a separate havoced variable
|
|
1253
1274
|
const nameNode = d.getNameNode();
|
|
@@ -1688,7 +1709,11 @@ function extractStmts(stmts) {
|
|
|
1688
1709
|
// functions this would emit the wrong shape, but lsc has no current
|
|
1689
1710
|
// examples of explicit bare return in void functions; revisit if one
|
|
1690
1711
|
// appears.
|
|
1691
|
-
|
|
1712
|
+
// `//@ havoc <key>` on a return abstracts the matching calls or new
|
|
1713
|
+
// expressions inside the returned expression — there is no variable to
|
|
1714
|
+
// hang a whole-value havoc on, so only the key form applies here.
|
|
1715
|
+
const value = withHavocKey(havocDirective(s)?.key ?? null, () => expr ? extractExpr(expr) : { kind: "var", name: "undefined" });
|
|
1716
|
+
result.push({ kind: "return", value, line });
|
|
1692
1717
|
continue;
|
|
1693
1718
|
}
|
|
1694
1719
|
if (Node.isBreakStatement(s)) {
|
|
@@ -1704,14 +1729,12 @@ function extractStmts(stmts) {
|
|
|
1704
1729
|
// //@ havoc before `x = e` — discard the RHS, assign a nondeterministic
|
|
1705
1730
|
// value of x's type. Only applies to plain `=` with an identifier LHS;
|
|
1706
1731
|
// compound assigns, `arr[i] = v`, and `x++` fall through to desugaring.
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
.find(m => m !== null);
|
|
1710
|
-
if (havocMatch && Node.isBinaryExpression(expr)
|
|
1732
|
+
const havoc = havocDirective(s);
|
|
1733
|
+
if (havoc && !havoc.key && Node.isBinaryExpression(expr)
|
|
1711
1734
|
&& expr.getOperatorToken().getText() === "="
|
|
1712
1735
|
&& Node.isIdentifier(expr.getLeft())) {
|
|
1713
1736
|
const target = expr.getLeft().getText();
|
|
1714
|
-
const tsType =
|
|
1737
|
+
const tsType = havoc.type ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
|
|
1715
1738
|
result.push({ kind: "assign", target, value: { kind: "havoc", tsType }, line });
|
|
1716
1739
|
continue;
|
|
1717
1740
|
}
|
|
@@ -1901,6 +1924,12 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1901
1924
|
return "void"; // Promise<void>
|
|
1902
1925
|
}
|
|
1903
1926
|
const node = fn.getReturnTypeNode();
|
|
1927
|
+
// A type predicate (`x is T` / `asserts x is T`) is a `boolean` at
|
|
1928
|
+
// runtime; the narrowing it performs is a TS-only refinement with no
|
|
1929
|
+
// counterpart in the model. Without this, `getText()` yields "x is T"
|
|
1930
|
+
// and the type mapper reads the subject name as an opaque type.
|
|
1931
|
+
if (node && node.getKind() === SyntaxKind.TypePredicate)
|
|
1932
|
+
return "boolean";
|
|
1904
1933
|
if (node && Node.isUnionTypeNode(node))
|
|
1905
1934
|
return _eraseGenerics(_tsTypeFromUnionNode(node));
|
|
1906
1935
|
if (node)
|
|
@@ -2461,6 +2490,28 @@ export function extractModule(sourceFile) {
|
|
|
2461
2490
|
}
|
|
2462
2491
|
}
|
|
2463
2492
|
}
|
|
2493
|
+
// A constant's initializer can reference other constants
|
|
2494
|
+
// (`const ZERO_NINE = ZERO + nthDigit(-1)`). Close over those initializers
|
|
2495
|
+
// before filtering — mirroring the transitive type filter below — or a
|
|
2496
|
+
// constant reachable only from another constant is dropped and the backend
|
|
2497
|
+
// sees an undefined name.
|
|
2498
|
+
// Snapshot first: what the closure adds are VALUE references, and a value
|
|
2499
|
+
// and a type can share a name (`const Action = Schema.Literals(…)` next to
|
|
2500
|
+
// `type Action = Schema.Schema.Type<typeof Action>`). Keeping the constant
|
|
2501
|
+
// alive must not also drag in the unrelated type alias, so the type filter
|
|
2502
|
+
// below runs off the pre-closure set.
|
|
2503
|
+
const typeReferencedNames = new Set(referencedNames);
|
|
2504
|
+
for (let grew = true; grew;) {
|
|
2505
|
+
grew = false;
|
|
2506
|
+
for (const c of constants) {
|
|
2507
|
+
if (!referencedNames.has(c.name))
|
|
2508
|
+
continue;
|
|
2509
|
+
const before = referencedNames.size;
|
|
2510
|
+
collectNamesExpr(c.value);
|
|
2511
|
+
if (referencedNames.size !== before)
|
|
2512
|
+
grew = true;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2464
2515
|
constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
|
|
2465
2516
|
// Filter types to only those referenced by verified functions (transitive)
|
|
2466
2517
|
const neededTypes = new Set();
|
|
@@ -2479,7 +2530,7 @@ export function extractModule(sourceFile) {
|
|
|
2479
2530
|
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
2480
2531
|
markType(m[1]);
|
|
2481
2532
|
}
|
|
2482
|
-
for (const name of
|
|
2533
|
+
for (const name of typeReferencedNames)
|
|
2483
2534
|
markType(name);
|
|
2484
2535
|
// Signature types also mark their base after stripping array/optional
|
|
2485
2536
|
// WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -385,7 +385,7 @@ function emitExpr(e, parentPrec) {
|
|
|
385
385
|
const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
|
|
386
386
|
if (ctor && ctor.fields.length > 0) {
|
|
387
387
|
const [yes, no] = e.op === "=" ? ["true", "false"] : ["false", "true"];
|
|
388
|
-
return `(match ${emitExpr(e.left)} with | .${
|
|
388
|
+
return `(match ${emitExpr(e.left)} with | .${leanCtorName(rhs.name)} .. => ${yes} | _ => ${no})`;
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
// `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
|
|
@@ -471,7 +471,7 @@ function emitExpr(e, parentPrec) {
|
|
|
471
471
|
const idx = owner.fields.findIndex(f => f.name === e.field);
|
|
472
472
|
const pats = owner.fields.map((_, i) => (i === idx ? "_v" : "_")).join(" ");
|
|
473
473
|
const fty = tyToLean(owner.fields[idx].type);
|
|
474
|
-
return `(match ${emitExpr(e.obj)} with | .${
|
|
474
|
+
return `(match ${emitExpr(e.obj)} with | .${leanCtorName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
477
|
const obj = emitExpr(e.obj);
|
package/tools/dist/resolve.js
CHANGED
|
@@ -55,10 +55,36 @@ function resolveTsType(tsType, overrides, varName) {
|
|
|
55
55
|
}
|
|
56
56
|
return parseTsType(tsType);
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
58
|
+
/** Infer a conditional's result type after its branches have been resolved or
|
|
59
|
+
* contextually coerced. A void branch is the source-level null/undefined arm. */
|
|
60
|
+
function conditionalResultTy(thenTy, elseTy) {
|
|
61
|
+
let ty = thenTy.kind !== "unknown" ? thenTy : elseTy;
|
|
62
|
+
if (thenTy.kind === "void" && elseTy.kind !== "void" && elseTy.kind !== "unknown") {
|
|
63
|
+
ty = { kind: "optional", inner: elseTy };
|
|
64
|
+
}
|
|
65
|
+
else if (elseTy.kind === "void" && thenTy.kind !== "void" && thenTy.kind !== "unknown") {
|
|
66
|
+
ty = { kind: "optional", inner: thenTy };
|
|
67
|
+
}
|
|
68
|
+
else if (thenTy.kind === "optional" && elseTy.kind !== "optional" && elseTy.kind !== "unknown") {
|
|
69
|
+
ty = thenTy;
|
|
70
|
+
}
|
|
71
|
+
else if (elseTy.kind === "optional" && thenTy.kind !== "optional" && thenTy.kind !== "unknown") {
|
|
72
|
+
ty = elseTy;
|
|
73
|
+
}
|
|
74
|
+
return ty;
|
|
75
|
+
}
|
|
76
|
+
/** Contextually type string literals as constructors. Ternary branches inherit
|
|
77
|
+
* the ternary's target, and an optional target contributes its payload type —
|
|
78
|
+
* the caller adds the Some wrapper only after the payload has been coerced. */
|
|
59
79
|
function coerceStr(expr, targetTy) {
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
const payloadTy = targetTy.kind === "optional" ? targetTy.inner : targetTy;
|
|
81
|
+
if (expr.kind === "str" && payloadTy.kind === "user")
|
|
82
|
+
return { ...expr, ty: payloadTy };
|
|
83
|
+
if (expr.kind === "conditional") {
|
|
84
|
+
const then_ = coerceStr(expr.then, payloadTy);
|
|
85
|
+
const else_ = coerceStr(expr.else, payloadTy);
|
|
86
|
+
return { ...expr, then: then_, else: else_, ty: conditionalResultTy(then_.ty, else_.ty) };
|
|
87
|
+
}
|
|
62
88
|
return expr;
|
|
63
89
|
}
|
|
64
90
|
// ── Helpers ──────────────────────────────────────────────────
|
|
@@ -84,6 +110,7 @@ function findSynthArrayUnion(name, typeDecls) {
|
|
|
84
110
|
* Returns `value` unchanged if no coercion applies (types already match,
|
|
85
111
|
* source is unknown, or no rule matches). */
|
|
86
112
|
function coerceToTargetTy(value, targetTy, typeDecls) {
|
|
113
|
+
value = coerceStr(value, targetTy);
|
|
87
114
|
if (value.ty.kind === "unknown" || value.ty.kind === "void")
|
|
88
115
|
return value;
|
|
89
116
|
if (targetTy.kind === "optional" && value.ty.kind !== "optional") {
|
|
@@ -328,6 +355,17 @@ function isUnmodeledTy(ty, typeDecls) {
|
|
|
328
355
|
function isStringUnionTy(ty, typeDecls) {
|
|
329
356
|
return declOfTy(typeDecls, ty)?.kind === "string-union";
|
|
330
357
|
}
|
|
358
|
+
/** Whether ts-morph widened a string-union initializer to the declared string
|
|
359
|
+
* shape. Inferred optional locals need the same rescue as bare locals:
|
|
360
|
+
* `Option<string>` from TS must not erase an `Option<Color>` initializer. */
|
|
361
|
+
function isWidenedStringUnionTy(declTy, initTy, typeDecls) {
|
|
362
|
+
if (declTy.kind === "string" && isStringUnionTy(initTy, typeDecls))
|
|
363
|
+
return true;
|
|
364
|
+
if (declTy.kind === "optional" && initTy.kind === "optional") {
|
|
365
|
+
return isWidenedStringUnionTy(declTy.inner, initTy.inner, typeDecls);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
331
369
|
/** Infer quantifier variable type from usage in body.
|
|
332
370
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
333
371
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
@@ -399,6 +437,8 @@ function classifyCall(fn, ctx) {
|
|
|
399
437
|
return "pure";
|
|
400
438
|
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray")
|
|
401
439
|
return "pure";
|
|
440
|
+
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode")
|
|
441
|
+
return "pure";
|
|
402
442
|
if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
|
|
403
443
|
return "spec-pure";
|
|
404
444
|
// Bare-name `//@ extern` declarations are emitted as `function {:axiom}` —
|
|
@@ -515,7 +555,7 @@ function isDefinedCheckRawLambda(raw) {
|
|
|
515
555
|
const isUndef = (x) => x.kind === "var" && x.name === "undefined";
|
|
516
556
|
return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
|
|
517
557
|
}
|
|
518
|
-
/** Coerce call arguments
|
|
558
|
+
/** Coerce call arguments to their declared parameter slots and pad missing optional args. */
|
|
519
559
|
function coerceCallArgs(args, fn, ctx) {
|
|
520
560
|
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
521
561
|
return args;
|
|
@@ -523,11 +563,7 @@ function coerceCallArgs(args, fn, ctx) {
|
|
|
523
563
|
args = args.map((a, i) => {
|
|
524
564
|
if (i >= paramTys.length)
|
|
525
565
|
return a;
|
|
526
|
-
|
|
527
|
-
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
528
|
-
return wrapSome(a, paramTys[i]);
|
|
529
|
-
}
|
|
530
|
-
return a;
|
|
566
|
+
return coerceToTargetTy(a, paramTys[i], ctx.typeDecls);
|
|
531
567
|
});
|
|
532
568
|
// Pad missing optional args with None
|
|
533
569
|
for (let i = args.length; i < paramTys.length; i++) {
|
|
@@ -546,6 +582,11 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
546
582
|
if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
|
|
547
583
|
return { kind: "bool" };
|
|
548
584
|
}
|
|
585
|
+
// `String.fromCharCode(n)` is the inverse of `s.charCodeAt(i)`: an int code
|
|
586
|
+
// point in, a one-character string out.
|
|
587
|
+
if (fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode") {
|
|
588
|
+
return { kind: "string" };
|
|
589
|
+
}
|
|
549
590
|
// Math.* numeric builtins: abs/min/max preserve the operand's numeric type
|
|
550
591
|
// (real if any operand is real); floor/ceil/round/trunc return an integer.
|
|
551
592
|
if (fn.obj.kind === "var" && fn.obj.name === "Math") {
|
|
@@ -803,7 +844,7 @@ function resolveExpr(e, ctx) {
|
|
|
803
844
|
if (ext) {
|
|
804
845
|
const args = e.args.map(a => resolveExpr(a, ctx));
|
|
805
846
|
const fn = { kind: "var", name: ext.flat, ty: { kind: "unknown" } };
|
|
806
|
-
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure" };
|
|
847
|
+
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure", paramTys: ext.params };
|
|
807
848
|
}
|
|
808
849
|
}
|
|
809
850
|
const fn = resolveExpr(e.fn, ctx);
|
|
@@ -819,7 +860,8 @@ function resolveExpr(e, ctx) {
|
|
|
819
860
|
const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
|
|
820
861
|
let args = coerceCallArgs(rawArgs.map((a, i) => {
|
|
821
862
|
let aCtx = argCtx;
|
|
822
|
-
if (paramTypes && i < paramTypes.length &&
|
|
863
|
+
if (paramTypes && i < paramTypes.length &&
|
|
864
|
+
(paramTypes[i].kind === "user" || paramTypes[i].kind === "array" || paramTypes[i].kind === "optional")) {
|
|
823
865
|
aCtx = { ...aCtx, returnTy: paramTypes[i] };
|
|
824
866
|
}
|
|
825
867
|
return resolveExpr(a, aCtx);
|
|
@@ -854,7 +896,8 @@ function resolveExpr(e, ctx) {
|
|
|
854
896
|
}
|
|
855
897
|
const builtinId = fn.kind === "field" ? recognizeBuiltin(fn.obj.ty, fn.field) : null;
|
|
856
898
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx),
|
|
857
|
-
...(builtinId ? { builtinId } : {})
|
|
899
|
+
...(builtinId ? { builtinId } : {}),
|
|
900
|
+
...(paramTypes ? { paramTys: paramTypes } : {}) };
|
|
858
901
|
}
|
|
859
902
|
case "index": {
|
|
860
903
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -935,7 +978,7 @@ function resolveExpr(e, ctx) {
|
|
|
935
978
|
const inner = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
936
979
|
// The default shares the result type, so coerce a string literal to a
|
|
937
980
|
// string-union enum (e.g. `availableLevels[0] ?? "off"`).
|
|
938
|
-
const right =
|
|
981
|
+
const right = coerceToTargetTy(resolveExpr(e.right, ctx), inner, ctx.typeDecls);
|
|
939
982
|
// `??` is only total when its default is: with a nullable right operand
|
|
940
983
|
// (rule-chain style `ruleA(e) ?? ruleB(e) ?? null`), the result stays
|
|
941
984
|
// optional — otherwise the enclosing chain level loses its optionality
|
|
@@ -1047,7 +1090,6 @@ function resolveExpr(e, ctx) {
|
|
|
1047
1090
|
let value = resolveExpr(f.value, valueCtx);
|
|
1048
1091
|
if (fieldDecl) {
|
|
1049
1092
|
const declTy = fieldDecl.type;
|
|
1050
|
-
value = coerceStr(value, declTy);
|
|
1051
1093
|
// Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
|
|
1052
1094
|
if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
|
|
1053
1095
|
value = { kind: "arrayLiteral", elems: [], ty: declTy };
|
|
@@ -1088,7 +1130,7 @@ function resolveExpr(e, ctx) {
|
|
|
1088
1130
|
const elems = e.elems.map((el, i) => {
|
|
1089
1131
|
const slot = slots[i];
|
|
1090
1132
|
const r = resolveExpr(el, slot ? { ...ctx, returnTy: slot } : ctx);
|
|
1091
|
-
return slot ?
|
|
1133
|
+
return slot ? coerceToTargetTy(r, slot, ctx.typeDecls) : r;
|
|
1092
1134
|
});
|
|
1093
1135
|
return { kind: "arrayLiteral", elems, ty: { kind: "tuple", elems: elems.map(x => x.ty) } };
|
|
1094
1136
|
}
|
|
@@ -1105,7 +1147,7 @@ function resolveExpr(e, ctx) {
|
|
|
1105
1147
|
const r = resolveExpr(el, elemCtx);
|
|
1106
1148
|
// Coerce a bare string-literal element to a string-union enum (e.g.
|
|
1107
1149
|
// `["off", …]: ModelThinkingLevel[]`), like return/arg positions.
|
|
1108
|
-
return expectedElem ?
|
|
1150
|
+
return expectedElem ? coerceToTargetTy(r, expectedElem, ctx.typeDecls) : r;
|
|
1109
1151
|
});
|
|
1110
1152
|
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
1111
1153
|
// No expected collection type: infer array vs tuple from the elements —
|
|
@@ -1173,21 +1215,7 @@ function resolveExpr(e, ctx) {
|
|
|
1173
1215
|
let else_ = resolveExpr(e.else, elseCtx);
|
|
1174
1216
|
then_ = coerceStr(then_, else_.ty);
|
|
1175
1217
|
else_ = coerceStr(else_, then_.ty);
|
|
1176
|
-
|
|
1177
|
-
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
1178
|
-
ty = { kind: "optional", inner: else_.ty };
|
|
1179
|
-
}
|
|
1180
|
-
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
1181
|
-
ty = { kind: "optional", inner: then_.ty };
|
|
1182
|
-
}
|
|
1183
|
-
else if (then_.ty.kind === "optional" && else_.ty.kind !== "optional" && else_.ty.kind !== "unknown") {
|
|
1184
|
-
// Asymmetric optional: one branch returns Option<T>, the other returns T.
|
|
1185
|
-
// Widen to Option<T> so callers/return-coercion see the wider type.
|
|
1186
|
-
ty = then_.ty;
|
|
1187
|
-
}
|
|
1188
|
-
else if (else_.ty.kind === "optional" && then_.ty.kind !== "optional" && then_.ty.kind !== "unknown") {
|
|
1189
|
-
ty = else_.ty;
|
|
1190
|
-
}
|
|
1218
|
+
const ty = conditionalResultTy(then_.ty, else_.ty);
|
|
1191
1219
|
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
1192
1220
|
}
|
|
1193
1221
|
case "emptyCollection": {
|
|
@@ -1284,7 +1312,16 @@ function resolveStmt(s, ctx) {
|
|
|
1284
1312
|
// one optional level when consulting returnTy.
|
|
1285
1313
|
const initCtx = (declTy.kind === "user" || declTy.kind === "array" || declTy.kind === "optional")
|
|
1286
1314
|
? { ...ctx, returnTy: declTy } : ctx;
|
|
1287
|
-
|
|
1315
|
+
let init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
1316
|
+
// Under noUncheckedIndexedAccess, TS gives `const e = arr[i]` type T | undefined
|
|
1317
|
+
// while the index expression itself resolves to T. Leave that mismatch intact:
|
|
1318
|
+
// narrow.ts's ruleOptionalIndexBinding adds the runtime bounds guard and the
|
|
1319
|
+
// corresponding Some/None branches. Wrapping here would turn it into an
|
|
1320
|
+
// unconditional Some(arr[i]) and prevent that JS-semantics rewrite from firing.
|
|
1321
|
+
const deferOptionalIndex = declTy.kind === "optional" && init.kind === "index" &&
|
|
1322
|
+
init.obj.ty.kind === "array" && init.ty.kind !== "optional";
|
|
1323
|
+
if (!deferOptionalIndex)
|
|
1324
|
+
init = coerceToTargetTy(init, declTy, ctx.typeDecls);
|
|
1288
1325
|
let ty;
|
|
1289
1326
|
if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
|
|
1290
1327
|
// ts-morph's declared type is opaque to us (an expanded union it made
|
|
@@ -1295,7 +1332,7 @@ function resolveStmt(s, ctx) {
|
|
|
1295
1332
|
? { kind: "optional", inner: init.ty }
|
|
1296
1333
|
: init.ty;
|
|
1297
1334
|
}
|
|
1298
|
-
else if (declTy
|
|
1335
|
+
else if (isWidenedStringUnionTy(declTy, init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
|
|
1299
1336
|
// ts-morph widened a string-union to `string`; keep the initializer's
|
|
1300
1337
|
// datatype so `local === "lit"` lowers to a discriminant test.
|
|
1301
1338
|
ty = init.ty;
|
|
@@ -1321,22 +1358,11 @@ function resolveStmt(s, ctx) {
|
|
|
1321
1358
|
// to their named datatypes.
|
|
1322
1359
|
const valueCtx = (targetTy.kind === "user" || targetTy.kind === "array" || targetTy.kind === "optional")
|
|
1323
1360
|
? { ...ctx, returnTy: targetTy } : ctx;
|
|
1324
|
-
|
|
1325
|
-
// Auto-wrap non-optional value in Some when target is optional
|
|
1326
|
-
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
1327
|
-
if (targetTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "unknown" && !isUndef) {
|
|
1328
|
-
value = wrapSome(value, targetTy);
|
|
1329
|
-
}
|
|
1361
|
+
const value = coerceToTargetTy(resolveExpr(s.value, valueCtx), targetTy, ctx.typeDecls);
|
|
1330
1362
|
return [{ kind: "assign", target: s.target, value }, ctx.env];
|
|
1331
1363
|
}
|
|
1332
1364
|
case "return": {
|
|
1333
|
-
|
|
1334
|
-
// Wrap non-optional return value in Some when function returns optional
|
|
1335
|
-
// Skip if already optional, void, or undefined (which maps to None)
|
|
1336
|
-
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
1337
|
-
if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
|
|
1338
|
-
value = wrapSome(value, ctx.returnTy);
|
|
1339
|
-
}
|
|
1365
|
+
const value = coerceToTargetTy(resolveExpr(s.value, ctx), ctx.returnTy, ctx.typeDecls);
|
|
1340
1366
|
return [{ kind: "return", value }, ctx.env];
|
|
1341
1367
|
}
|
|
1342
1368
|
case "break":
|
|
@@ -1452,14 +1478,27 @@ function resolveStmt(s, ctx) {
|
|
|
1452
1478
|
}
|
|
1453
1479
|
}
|
|
1454
1480
|
// ── Pure / return-in-loop detection ──────────────────────────
|
|
1455
|
-
/**
|
|
1481
|
+
/** Whether a raw statement or expression tree contains a havoc anywhere. */
|
|
1482
|
+
function containsHavoc(v) {
|
|
1483
|
+
if (Array.isArray(v))
|
|
1484
|
+
return v.some(containsHavoc);
|
|
1485
|
+
if (v === null || typeof v !== "object")
|
|
1486
|
+
return false;
|
|
1487
|
+
if (v.kind === "havoc")
|
|
1488
|
+
return true;
|
|
1489
|
+
return Object.values(v).some(containsHavoc);
|
|
1490
|
+
}
|
|
1491
|
+
/** Syntactic purity: no while, no for-of, no mutable let, no havoc. */
|
|
1456
1492
|
function isSyntacticallyPure(stmts) {
|
|
1457
1493
|
for (const s of stmts) {
|
|
1494
|
+
// Havoc lowers to Dafny's `*`, which is only valid in a method.
|
|
1495
|
+
if (containsHavoc(s))
|
|
1496
|
+
return false;
|
|
1458
1497
|
switch (s.kind) {
|
|
1459
1498
|
case "while":
|
|
1460
1499
|
case "forof": return false;
|
|
1461
1500
|
case "let":
|
|
1462
|
-
if (s.mutable
|
|
1501
|
+
if (s.mutable)
|
|
1463
1502
|
return false;
|
|
1464
1503
|
break;
|
|
1465
1504
|
case "if":
|
|
@@ -1779,7 +1818,8 @@ export function resolveModule(raw) {
|
|
|
1779
1818
|
// record literals on map-typed constants (e.g. `Record<string, number>`)
|
|
1780
1819
|
// get their `ty` set to `map<...>` rather than `user("...")`.
|
|
1781
1820
|
const valueCtx = { ...emptyCtx, returnTy: ty };
|
|
1782
|
-
|
|
1821
|
+
const value = coerceToTargetTy(resolveExpr(c.value, valueCtx), ty, raw.typeDecls);
|
|
1822
|
+
return { name: c.name, ty, value };
|
|
1783
1823
|
});
|
|
1784
1824
|
const moduleConstants = new Map(constants.map(c => [c.name, c.ty]));
|
|
1785
1825
|
return {
|
package/tools/dist/transform.js
CHANGED
|
@@ -234,7 +234,10 @@ function kindHelperDecl(decl) {
|
|
|
234
234
|
* arm body would still be captured, so prime on any module-wide collision.
|
|
235
235
|
* Deterministic, so the pattern binder and its body substitutions agree. */
|
|
236
236
|
function matchBinder(fieldName, prefix) {
|
|
237
|
-
|
|
237
|
+
const safePrefix = prefix === "\\result"
|
|
238
|
+
? "result"
|
|
239
|
+
: prefix?.replace(/[^A-Za-z0-9_]/g, "_");
|
|
240
|
+
return freshName(safePrefix ? `_${safePrefix}_${fieldName}` : `_${fieldName}`);
|
|
238
241
|
}
|
|
239
242
|
/** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
|
|
240
243
|
function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
@@ -358,20 +361,33 @@ function flattenLambdaBody(stmts) {
|
|
|
358
361
|
* field, index, record, forall, or exists sub-expressions.
|
|
359
362
|
*/
|
|
360
363
|
/** JS truthiness coercion for `if`/`while`/`?:` conditions.
|
|
361
|
-
* Dafny requires bool; coerce number
|
|
364
|
+
* Dafny requires bool; coerce number→`!== 0`, string→non-empty, array→`true`
|
|
362
365
|
* (every array, even `[]`, is truthy in JS).
|
|
363
|
-
*
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
366
|
+
*
|
|
367
|
+
* Rewrites the typed tree, before lowering, and distributes over `&&`/`||`:
|
|
368
|
+
* each operand of a logical connective is itself in condition position, and the
|
|
369
|
+
* operands need not share a type — `i >= 0 && carry`, with `carry` an int, is a
|
|
370
|
+
* bool conjoined with a number. A conjunction takes its type from its right
|
|
371
|
+
* operand (resolve), so coercing the whole expression by its type would emit
|
|
372
|
+
* `((i >= 0) && carry) != 0`, which is not well-typed.
|
|
373
|
+
*
|
|
374
|
+
* Optional conds never arrive here — narrow.ts rewrites them to someMatch. */
|
|
375
|
+
function asCondition(e) {
|
|
376
|
+
const bool = { kind: "bool" };
|
|
377
|
+
if (e.ty.kind === "bool")
|
|
378
|
+
return e;
|
|
379
|
+
if (e.kind === "binop" && (e.op === "&&" || e.op === "||"))
|
|
380
|
+
return { ...e, left: asCondition(e.left), right: asCondition(e.right), ty: bool };
|
|
381
|
+
if (e.ty.kind === "int" || e.ty.kind === "nat")
|
|
382
|
+
return { kind: "binop", op: "!==", left: e, right: { kind: "num", value: 0, ty: e.ty }, ty: bool };
|
|
383
|
+
if (e.ty.kind === "string")
|
|
384
|
+
return { kind: "binop", op: ">",
|
|
385
|
+
left: { kind: "field", obj: e, field: "length", ty: { kind: "nat" } },
|
|
386
|
+
right: { kind: "num", value: 0, ty: { kind: "nat" } }, ty: bool };
|
|
371
387
|
// Arrays, objects, maps, sets, tuples are always truthy in JS (even `[]`/`{}`).
|
|
372
|
-
if (["array", "user", "map", "set", "tuple"].includes(ty.kind))
|
|
373
|
-
return { kind: "bool", value: true };
|
|
374
|
-
return
|
|
388
|
+
if (["array", "user", "map", "set", "tuple"].includes(e.ty.kind))
|
|
389
|
+
return { kind: "bool", value: true, ty: bool };
|
|
390
|
+
return e;
|
|
375
391
|
}
|
|
376
392
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
377
393
|
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
@@ -802,6 +818,12 @@ function lowerExpr(e, binds) {
|
|
|
802
818
|
}
|
|
803
819
|
}
|
|
804
820
|
}
|
|
821
|
+
// String.fromCharCode(n) → preamble function (inverse of charCodeAt,
|
|
822
|
+
// which lowers to `(s[i] as int)`).
|
|
823
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "String" &&
|
|
824
|
+
e.fn.field === "fromCharCode" && e.args.length === 1) {
|
|
825
|
+
return { kind: "app", fn: "StringFromCharCode", args: [lowerExpr(e.args[0], binds)] };
|
|
826
|
+
}
|
|
805
827
|
// Math.abs/min/max → preamble functions
|
|
806
828
|
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
|
|
807
829
|
if (e.fn.field === "abs" && e.args.length === 1)
|
|
@@ -1032,7 +1054,7 @@ function lowerExpr(e, binds) {
|
|
|
1032
1054
|
// JS truthiness coercion (string/array/int → ... > 0). Matches SPEC §3.1
|
|
1033
1055
|
// negation forms (`!s` → `s == ""`). Optional conds are already
|
|
1034
1056
|
// rewritten to someMatch by narrow.ts.
|
|
1035
|
-
const cond =
|
|
1057
|
+
const cond = lowerExpr(asCondition(e.cond), binds);
|
|
1036
1058
|
let thenExpr = lowerExpr(e.then, binds);
|
|
1037
1059
|
let elseExpr = lowerExpr(e.else, binds);
|
|
1038
1060
|
if (e.ty.kind === "optional") {
|
|
@@ -1372,6 +1394,11 @@ function matchToIfChains(stmts) {
|
|
|
1372
1394
|
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1373
1395
|
const scrutExpr = s.scrutinee;
|
|
1374
1396
|
const defaultArm = arms.find(a => a.pattern.kind === "wild");
|
|
1397
|
+
const declaredCtors = decl.kind === "discriminated-union"
|
|
1398
|
+
? decl.variants?.map(v => v.name)
|
|
1399
|
+
: decl.kind === "string-union" ? decl.values : undefined;
|
|
1400
|
+
const coveredCtors = new Set(ctorArms.map(a => patternCtor(a.pattern)).filter((c) => !!c));
|
|
1401
|
+
const exhaustiveWithoutDefault = !defaultArm && !!declaredCtors && declaredCtors.every(c => coveredCtors.has(c));
|
|
1375
1402
|
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1376
1403
|
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
1377
1404
|
const armBody = ctorArms[k].body;
|
|
@@ -1401,6 +1428,13 @@ function matchToIfChains(stmts) {
|
|
|
1401
1428
|
value: { kind: "field", obj: scrutExpr, field: f.name, fromUnion: decl.name, ctor },
|
|
1402
1429
|
});
|
|
1403
1430
|
});
|
|
1431
|
+
// An exhaustive source match has no fallthrough. Use its final arm as
|
|
1432
|
+
// the unconditional else branch; emitting `else pure ()` would force a
|
|
1433
|
+
// Unit result even when every arm returns the method's result type.
|
|
1434
|
+
if (exhaustiveWithoutDefault && k === ctorArms.length - 1) {
|
|
1435
|
+
elseBranch = [...lets, ...armBody];
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1404
1438
|
elseBranch = [{ kind: "if", cond, then: [...lets, ...armBody], else: elseBranch }];
|
|
1405
1439
|
}
|
|
1406
1440
|
return elseBranch;
|
|
@@ -1754,13 +1788,13 @@ function transformStmt(s, typeDecls) {
|
|
|
1754
1788
|
}
|
|
1755
1789
|
case "if": {
|
|
1756
1790
|
// Lift from condition only (Lean rule: don't lift from branches).
|
|
1757
|
-
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
1758
|
-
return [...binds, { kind: "if", cond
|
|
1791
|
+
const { binds, expr: cond } = liftMethodCalls(asCondition(s.cond));
|
|
1792
|
+
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
1759
1793
|
}
|
|
1760
1794
|
case "while":
|
|
1761
1795
|
return [{
|
|
1762
1796
|
kind: "while",
|
|
1763
|
-
cond:
|
|
1797
|
+
cond: transformExpr(asCondition(s.cond)),
|
|
1764
1798
|
invariants: s.invariants.map(transformExpr),
|
|
1765
1799
|
decreasing: s.decreases ? transformExpr(s.decreases) : null,
|
|
1766
1800
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
@@ -1967,14 +2001,67 @@ function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
|
1967
2001
|
}));
|
|
1968
2002
|
}
|
|
1969
2003
|
/** Replace all variant fields of obj → match binder vars in typed IR.
|
|
1970
|
-
*
|
|
2004
|
+
* Before replacing field reads, realize TypeScript's structural argument
|
|
2005
|
+
* conversion for calls such as `helper(outcome)` inside a narrowed union arm.
|
|
2006
|
+
* The source value is still the enclosing union in typed IR, while Dafny and
|
|
2007
|
+
* Lean expect the helper's nominal record. Rebuild that record solely from
|
|
2008
|
+
* the fields bound by this arm's match pattern. */
|
|
1971
2009
|
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
1972
|
-
|
|
2010
|
+
const projected = projectStructuralCallArgsInTStmts(stmts, varName, fields);
|
|
2011
|
+
return replaceFieldsInTStmts(projected, varName, fields.map(f => ({
|
|
1973
2012
|
fieldName: f.name,
|
|
1974
2013
|
newName: matchBinder(f.name, varName),
|
|
1975
2014
|
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1976
2015
|
})));
|
|
1977
2016
|
}
|
|
2017
|
+
/** Project a narrowed union scrutinee into a named structural record expected
|
|
2018
|
+
* by a same-module/extern call. Resolution stamps named calls with paramTys;
|
|
2019
|
+
* this pass fires only when every target record field has an identically typed
|
|
2020
|
+
* match-bound source field. That deliberately avoids inventing a broad cast:
|
|
2021
|
+
* it is the nominal-backend witness for the structural call TS already accepts. */
|
|
2022
|
+
function projectStructuralCallArgsInTStmts(stmts, varName, fields) {
|
|
2023
|
+
const sourceFields = fields.map(f => ({
|
|
2024
|
+
...f,
|
|
2025
|
+
resolvedTy: f.type ?? parseTsType(f.tsType),
|
|
2026
|
+
}));
|
|
2027
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
2028
|
+
if (e.kind !== "call" || !e.paramTys)
|
|
2029
|
+
return null;
|
|
2030
|
+
const paramTys = e.paramTys;
|
|
2031
|
+
let changed = false;
|
|
2032
|
+
const args = e.args.map((arg, i) => {
|
|
2033
|
+
if (arg.kind !== "var" || arg.name !== varName || i >= paramTys.length)
|
|
2034
|
+
return arg;
|
|
2035
|
+
const targetTy = paramTys[i];
|
|
2036
|
+
const targetDecl = declOfTy(_typeDecls, targetTy);
|
|
2037
|
+
if (targetTy.kind !== "user" || targetDecl?.kind !== "record" || !targetDecl.fields)
|
|
2038
|
+
return arg;
|
|
2039
|
+
const matched = [];
|
|
2040
|
+
for (const targetField of targetDecl.fields) {
|
|
2041
|
+
const sourceField = sourceFields.find(f => f.name === targetField.name);
|
|
2042
|
+
const targetFieldTy = targetField.type ?? parseTsType(targetField.tsType);
|
|
2043
|
+
if (!sourceField || !tyEqual(sourceField.resolvedTy, targetFieldTy))
|
|
2044
|
+
return arg;
|
|
2045
|
+
matched.push({ targetField, sourceField });
|
|
2046
|
+
}
|
|
2047
|
+
changed = true;
|
|
2048
|
+
return {
|
|
2049
|
+
kind: "record",
|
|
2050
|
+
spread: null,
|
|
2051
|
+
fields: matched.map(m => ({
|
|
2052
|
+
name: m.targetField.name,
|
|
2053
|
+
value: {
|
|
2054
|
+
kind: "var",
|
|
2055
|
+
name: matchBinder(m.sourceField.name, varName),
|
|
2056
|
+
ty: m.sourceField.resolvedTy,
|
|
2057
|
+
},
|
|
2058
|
+
})),
|
|
2059
|
+
ty: targetTy,
|
|
2060
|
+
};
|
|
2061
|
+
});
|
|
2062
|
+
return changed ? { ...e, args } : null;
|
|
2063
|
+
}));
|
|
2064
|
+
}
|
|
1978
2065
|
/** Replace obj.field → replacement var in typed IR expressions (before lowering).
|
|
1979
2066
|
* Mirrors replaceFieldsInTStmts but operates on a single TExpr tree. */
|
|
1980
2067
|
function replaceFieldInTExpr(expr, objName, replacements) {
|
|
@@ -2062,7 +2149,7 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
2062
2149
|
const elseExpr = transformPureBody(elseStmts, typeDecls);
|
|
2063
2150
|
if (!elseExpr)
|
|
2064
2151
|
return null;
|
|
2065
|
-
return { kind: "if", cond:
|
|
2152
|
+
return { kind: "if", cond: transformExpr(asCondition(s.cond)), then: thenExpr, else: elseExpr };
|
|
2066
2153
|
}
|
|
2067
2154
|
case "switch": return transformPureSwitch(s, typeDecls);
|
|
2068
2155
|
case "someMatch": {
|
|
@@ -2112,7 +2199,8 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2112
2199
|
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
2113
2200
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2114
2201
|
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2115
|
-
|
|
2202
|
+
const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
|
|
2203
|
+
let result = transformPureBody(projected, typeDecls);
|
|
2116
2204
|
if (!result)
|
|
2117
2205
|
return null;
|
|
2118
2206
|
if (fields.length > 0 && vn)
|
|
@@ -2139,7 +2227,8 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2139
2227
|
// the statement-level counterpart of this substitution.
|
|
2140
2228
|
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
2141
2229
|
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2142
|
-
|
|
2230
|
+
const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
|
|
2231
|
+
let result = transformPureBody(projected, typeDecls);
|
|
2143
2232
|
if (!result)
|
|
2144
2233
|
return null;
|
|
2145
2234
|
if (fields.length > 0 && vn)
|
|
@@ -2159,7 +2248,8 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2159
2248
|
const remaining = remainingVariant(chain.typeName, chain.cases, typeDecls);
|
|
2160
2249
|
if (remaining) {
|
|
2161
2250
|
// Exactly one variant left — destructure for variant-specific field access.
|
|
2162
|
-
|
|
2251
|
+
const projected = projectStructuralCallArgsInTStmts(chain.fallthrough, chain.varName, remaining.fields);
|
|
2252
|
+
let body = transformPureBody(projected, typeDecls);
|
|
2163
2253
|
if (!body)
|
|
2164
2254
|
return null;
|
|
2165
2255
|
if (remaining.fields.length > 0)
|