lemmascript 0.5.19 → 0.5.21
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 +71 -4
- package/tools/dist/extract.js +139 -23
- package/tools/dist/ir.js +1 -0
- package/tools/dist/lean-emit.js +32 -5
- package/tools/dist/resolve.js +101 -49
- package/tools/dist/transform.js +181 -40
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],
|
|
@@ -1493,15 +1555,18 @@ function qualifyCtor(name, type) {
|
|
|
1493
1555
|
return `${type}.${mapped}`;
|
|
1494
1556
|
return mapped;
|
|
1495
1557
|
}
|
|
1496
|
-
/** Translate a
|
|
1558
|
+
/** Translate a backend-neutral match pattern to Dafny syntax.
|
|
1497
1559
|
* ".ctorName field1 field2" → "ctorName(field1, field2)"
|
|
1498
1560
|
* ".ctorName" → "ctorName"
|
|
1561
|
+
* literal value → quoted Dafny string
|
|
1499
1562
|
* "_" → "_"
|
|
1500
1563
|
*/
|
|
1501
1564
|
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
1502
1565
|
function translatePattern(p) {
|
|
1503
1566
|
if (p.kind === "wild")
|
|
1504
1567
|
return "_";
|
|
1568
|
+
if (p.kind === "literal")
|
|
1569
|
+
return emitExpr({ kind: "str", value: p.value });
|
|
1505
1570
|
const ctorName = (Object.hasOwn(CTOR_MAP, p.ctor) ? CTOR_MAP[p.ctor] : undefined) ?? dafnyCtorName(p.ctor);
|
|
1506
1571
|
if (p.binders.length === 0)
|
|
1507
1572
|
return ctorName;
|
|
@@ -1512,6 +1577,8 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1512
1577
|
resetDafnyNameCache();
|
|
1513
1578
|
buildRecordCtorMap(file.decls);
|
|
1514
1579
|
_neededPreambles.clear();
|
|
1580
|
+
_havocedTypeNames.clear();
|
|
1581
|
+
collectHavocedTypeNames(file.decls, _havocedTypeNames);
|
|
1515
1582
|
// Track successfully emitted pure defs — method wrappers are only
|
|
1516
1583
|
// skipped when the corresponding pure def was actually emitted.
|
|
1517
1584
|
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
|
|
@@ -492,19 +515,78 @@ function extractExpr(node) {
|
|
|
492
515
|
}
|
|
493
516
|
// Arrow function: (x) => expr or (x) => { stmts }
|
|
494
517
|
if (Node.isArrowFunction(node)) {
|
|
495
|
-
|
|
518
|
+
// A binding pattern is not an identifier in either backend. Keep the
|
|
519
|
+
// callback arity unchanged by replacing a flat object pattern with one
|
|
520
|
+
// synthetic parameter and binding its fields at the start of the body:
|
|
521
|
+
// ({ x, y: z }) => e
|
|
522
|
+
// becomes, in Raw IR, the equivalent of
|
|
523
|
+
// (_lambdaParam0) => { const x = _lambdaParam0.x;
|
|
524
|
+
// const z = _lambdaParam0.y; return e; }
|
|
525
|
+
// transform's lambda flattener later turns these immutable lets back into
|
|
526
|
+
// the expression form required by Dafny lambdas. More involved binding
|
|
527
|
+
// semantics stay explicit errors rather than leaking pattern text into a
|
|
528
|
+
// generated backend identifier.
|
|
529
|
+
const destructureBindings = [];
|
|
530
|
+
const params = node.getParameters().map((p, paramIndex) => {
|
|
496
531
|
const typeNode = p.getTypeNode();
|
|
497
|
-
|
|
532
|
+
const tsType = typeNode ? typeNode.getText() : undefined;
|
|
533
|
+
const nameNode = p.getNameNode();
|
|
534
|
+
if (Node.isIdentifier(nameNode))
|
|
535
|
+
return { name: nameNode.getText(), tsType };
|
|
536
|
+
if (Node.isArrayBindingPattern(nameNode)) {
|
|
537
|
+
throw new Error(`array binding pattern in arrow parameter not yet supported: ${nameNode.getText()}`);
|
|
538
|
+
}
|
|
539
|
+
if (!Node.isObjectBindingPattern(nameNode)) {
|
|
540
|
+
throw new Error(`unsupported arrow parameter binding: ${p.getName()}`);
|
|
541
|
+
}
|
|
542
|
+
if (p.getInitializer()) {
|
|
543
|
+
throw new Error(`defaulted object binding pattern in arrow parameter not yet supported: ${p.getText()}`);
|
|
544
|
+
}
|
|
545
|
+
const paramName = freshName(`_lambdaParam${paramIndex}`);
|
|
546
|
+
for (const el of nameNode.getElements()) {
|
|
547
|
+
if (el.getDotDotDotToken()) {
|
|
548
|
+
throw new Error(`rest property in arrow parameter destructuring not yet supported: ${el.getText()}`);
|
|
549
|
+
}
|
|
550
|
+
if (el.getInitializer()) {
|
|
551
|
+
throw new Error(`default value in arrow parameter destructuring not yet supported: ${el.getText()}`);
|
|
552
|
+
}
|
|
553
|
+
const localNode = el.getNameNode();
|
|
554
|
+
if (!Node.isIdentifier(localNode)) {
|
|
555
|
+
throw new Error(`nested binding pattern in arrow parameter destructuring not yet supported: ${el.getText()}`);
|
|
556
|
+
}
|
|
557
|
+
const localName = localNode.getText();
|
|
558
|
+
const propNode = el.getPropertyNameNode();
|
|
559
|
+
if (propNode && !Node.isIdentifier(propNode)) {
|
|
560
|
+
throw new Error(`computed or literal property in arrow parameter destructuring not yet supported: ${el.getText()}`);
|
|
561
|
+
}
|
|
562
|
+
destructureBindings.push({
|
|
563
|
+
kind: "let",
|
|
564
|
+
name: localName,
|
|
565
|
+
mutable: false,
|
|
566
|
+
tsType: null,
|
|
567
|
+
init: {
|
|
568
|
+
kind: "field",
|
|
569
|
+
obj: { kind: "var", name: paramName },
|
|
570
|
+
field: propNode ? propNode.getText() : localName,
|
|
571
|
+
},
|
|
572
|
+
line: p.getStartLineNumber(),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return { name: paramName, tsType };
|
|
498
576
|
});
|
|
499
577
|
// Return type from the checker — inferred when unannotated — so resolve can
|
|
500
578
|
// type return-position record literals and give the lambda a real fn type.
|
|
501
579
|
const returnTsType = typeToString(node.getReturnType());
|
|
502
580
|
const body = node.getBody();
|
|
503
581
|
if (Node.isExpression(body)) {
|
|
504
|
-
|
|
582
|
+
const expr = extractExpr(body);
|
|
583
|
+
const loweredBody = destructureBindings.length === 0
|
|
584
|
+
? expr
|
|
585
|
+
: [...destructureBindings, { kind: "return", value: expr, line: body.getStartLineNumber() }];
|
|
586
|
+
return { kind: "lambda", params, body: loweredBody, returnTsType };
|
|
505
587
|
}
|
|
506
588
|
if (Node.isBlock(body)) {
|
|
507
|
-
return { kind: "lambda", params, body: extractStmts(body.getStatements()), returnTsType };
|
|
589
|
+
return { kind: "lambda", params, body: [...destructureBindings, ...extractStmts(body.getStatements())], returnTsType };
|
|
508
590
|
}
|
|
509
591
|
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
510
592
|
}
|
|
@@ -1242,12 +1324,10 @@ function extractStmts(stmts) {
|
|
|
1242
1324
|
continue;
|
|
1243
1325
|
}
|
|
1244
1326
|
if (Node.isVariableStatement(s)) {
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const
|
|
1249
|
-
const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
|
|
1250
|
-
const isHavoc = !!havocMatch;
|
|
1327
|
+
const havoc = havocDirective(s);
|
|
1328
|
+
const havocType = havoc?.type ?? null; // //@ havoc : Type
|
|
1329
|
+
const havocKey = havoc?.key ?? null; // //@ havoc key
|
|
1330
|
+
const isHavoc = !!havoc;
|
|
1251
1331
|
for (const d of s.getDeclarations()) {
|
|
1252
1332
|
// Havoc on destructuring: emit each named binding as a separate havoced variable
|
|
1253
1333
|
const nameNode = d.getNameNode();
|
|
@@ -1654,10 +1734,16 @@ function extractStmts(stmts) {
|
|
|
1654
1734
|
// B: ...`) — the stripped breaks are the switch exits.
|
|
1655
1735
|
const clauseInfos = s.getClauses().map(clause => {
|
|
1656
1736
|
const stmts = extractStmts(clause.getStatements());
|
|
1737
|
+
let label = null;
|
|
1738
|
+
if (Node.isCaseClause(clause)) {
|
|
1739
|
+
const labelExpr = extractExpr(clause.getExpression());
|
|
1740
|
+
if (labelExpr.kind !== "str") {
|
|
1741
|
+
throw new Error(`Unsupported switch case at line ${clause.getStartLineNumber()}: expected a string literal`);
|
|
1742
|
+
}
|
|
1743
|
+
label = labelExpr.value;
|
|
1744
|
+
}
|
|
1657
1745
|
return {
|
|
1658
|
-
label
|
|
1659
|
-
? clause.getExpression().getText().replace(/^["']|["']$/g, "")
|
|
1660
|
-
: null,
|
|
1746
|
+
label,
|
|
1661
1747
|
stmts,
|
|
1662
1748
|
exits: isExit(stmts[stmts.length - 1]),
|
|
1663
1749
|
};
|
|
@@ -1688,7 +1774,11 @@ function extractStmts(stmts) {
|
|
|
1688
1774
|
// functions this would emit the wrong shape, but lsc has no current
|
|
1689
1775
|
// examples of explicit bare return in void functions; revisit if one
|
|
1690
1776
|
// appears.
|
|
1691
|
-
|
|
1777
|
+
// `//@ havoc <key>` on a return abstracts the matching calls or new
|
|
1778
|
+
// expressions inside the returned expression — there is no variable to
|
|
1779
|
+
// hang a whole-value havoc on, so only the key form applies here.
|
|
1780
|
+
const value = withHavocKey(havocDirective(s)?.key ?? null, () => expr ? extractExpr(expr) : { kind: "var", name: "undefined" });
|
|
1781
|
+
result.push({ kind: "return", value, line });
|
|
1692
1782
|
continue;
|
|
1693
1783
|
}
|
|
1694
1784
|
if (Node.isBreakStatement(s)) {
|
|
@@ -1704,14 +1794,12 @@ function extractStmts(stmts) {
|
|
|
1704
1794
|
// //@ havoc before `x = e` — discard the RHS, assign a nondeterministic
|
|
1705
1795
|
// value of x's type. Only applies to plain `=` with an identifier LHS;
|
|
1706
1796
|
// 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)
|
|
1797
|
+
const havoc = havocDirective(s);
|
|
1798
|
+
if (havoc && !havoc.key && Node.isBinaryExpression(expr)
|
|
1711
1799
|
&& expr.getOperatorToken().getText() === "="
|
|
1712
1800
|
&& Node.isIdentifier(expr.getLeft())) {
|
|
1713
1801
|
const target = expr.getLeft().getText();
|
|
1714
|
-
const tsType =
|
|
1802
|
+
const tsType = havoc.type ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
|
|
1715
1803
|
result.push({ kind: "assign", target, value: { kind: "havoc", tsType }, line });
|
|
1716
1804
|
continue;
|
|
1717
1805
|
}
|
|
@@ -1901,6 +1989,12 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1901
1989
|
return "void"; // Promise<void>
|
|
1902
1990
|
}
|
|
1903
1991
|
const node = fn.getReturnTypeNode();
|
|
1992
|
+
// A type predicate (`x is T` / `asserts x is T`) is a `boolean` at
|
|
1993
|
+
// runtime; the narrowing it performs is a TS-only refinement with no
|
|
1994
|
+
// counterpart in the model. Without this, `getText()` yields "x is T"
|
|
1995
|
+
// and the type mapper reads the subject name as an opaque type.
|
|
1996
|
+
if (node && node.getKind() === SyntaxKind.TypePredicate)
|
|
1997
|
+
return "boolean";
|
|
1904
1998
|
if (node && Node.isUnionTypeNode(node))
|
|
1905
1999
|
return _eraseGenerics(_tsTypeFromUnionNode(node));
|
|
1906
2000
|
if (node)
|
|
@@ -2461,6 +2555,28 @@ export function extractModule(sourceFile) {
|
|
|
2461
2555
|
}
|
|
2462
2556
|
}
|
|
2463
2557
|
}
|
|
2558
|
+
// A constant's initializer can reference other constants
|
|
2559
|
+
// (`const ZERO_NINE = ZERO + nthDigit(-1)`). Close over those initializers
|
|
2560
|
+
// before filtering — mirroring the transitive type filter below — or a
|
|
2561
|
+
// constant reachable only from another constant is dropped and the backend
|
|
2562
|
+
// sees an undefined name.
|
|
2563
|
+
// Snapshot first: what the closure adds are VALUE references, and a value
|
|
2564
|
+
// and a type can share a name (`const Action = Schema.Literals(…)` next to
|
|
2565
|
+
// `type Action = Schema.Schema.Type<typeof Action>`). Keeping the constant
|
|
2566
|
+
// alive must not also drag in the unrelated type alias, so the type filter
|
|
2567
|
+
// below runs off the pre-closure set.
|
|
2568
|
+
const typeReferencedNames = new Set(referencedNames);
|
|
2569
|
+
for (let grew = true; grew;) {
|
|
2570
|
+
grew = false;
|
|
2571
|
+
for (const c of constants) {
|
|
2572
|
+
if (!referencedNames.has(c.name))
|
|
2573
|
+
continue;
|
|
2574
|
+
const before = referencedNames.size;
|
|
2575
|
+
collectNamesExpr(c.value);
|
|
2576
|
+
if (referencedNames.size !== before)
|
|
2577
|
+
grew = true;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2464
2580
|
constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
|
|
2465
2581
|
// Filter types to only those referenced by verified functions (transitive)
|
|
2466
2582
|
const neededTypes = new Set();
|
|
@@ -2479,7 +2595,7 @@ export function extractModule(sourceFile) {
|
|
|
2479
2595
|
for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
|
|
2480
2596
|
markType(m[1]);
|
|
2481
2597
|
}
|
|
2482
|
-
for (const name of
|
|
2598
|
+
for (const name of typeReferencedNames)
|
|
2483
2599
|
markType(name);
|
|
2484
2600
|
// Signature types also mark their base after stripping array/optional
|
|
2485
2601
|
// WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
|
package/tools/dist/ir.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const pWild = () => ({ kind: "wild" });
|
|
8
8
|
export const pCtor = (c, ...binders) => ({ kind: "ctor", ctor: c, binders });
|
|
9
|
+
export const pLiteral = (value) => ({ kind: "literal", value });
|
|
9
10
|
/** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
|
|
10
11
|
export function patternBinders(p) {
|
|
11
12
|
return p.kind === "ctor" ? p.binders : [];
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -196,9 +196,13 @@ let _boolCtx = false;
|
|
|
196
196
|
function leanCtorName(name) {
|
|
197
197
|
return /^[A-Za-z_][A-Za-z0-9_'!?]*$/.test(name) ? name : `«${name}»`;
|
|
198
198
|
}
|
|
199
|
-
/** Render a match pattern to Lean syntax: `_`,
|
|
199
|
+
/** Render a match pattern to Lean syntax: `_`, a quoted literal, or `.ctor args`. */
|
|
200
200
|
function renderLeanPattern(p) {
|
|
201
|
-
|
|
201
|
+
if (p.kind === "wild")
|
|
202
|
+
return "_";
|
|
203
|
+
if (p.kind === "literal")
|
|
204
|
+
return emitExpr({ kind: "str", value: p.value });
|
|
205
|
+
return "." + [leanCtorName(p.ctor), ...p.binders].join(" ");
|
|
202
206
|
}
|
|
203
207
|
// A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
|
|
204
208
|
// (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
|
|
@@ -354,7 +358,30 @@ function emitExpr(e, parentPrec) {
|
|
|
354
358
|
const obj = emitExpr(e.obj);
|
|
355
359
|
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall" || e.obj.kind === "if" || e.obj.kind === "let";
|
|
356
360
|
const receiver = wrap ? `(${obj})` : obj;
|
|
357
|
-
|
|
361
|
+
// Predicate-taking array methods require a Bool-valued callback even when
|
|
362
|
+
// the surrounding expression returns an array/option rather than Bool.
|
|
363
|
+
// Usually Lean can coerce a decidable Prop-valued predicate. A predicate
|
|
364
|
+
// containing a raw Bool match cannot be coerced through ∧/∨/¬, though, so
|
|
365
|
+
// switch just that callback to computational connectives. This is local:
|
|
366
|
+
// setting the enclosing definition's Bool context would incorrectly
|
|
367
|
+
// affect unrelated arguments and would still miss predicates inside an
|
|
368
|
+
// Array-returning definition such as `xs.filter(...)`.
|
|
369
|
+
const predicateMethod = e.objTy.kind === "array" &&
|
|
370
|
+
["filter", "every", "some", "find"].includes(e.method);
|
|
371
|
+
const args = e.args.map((a, i) => {
|
|
372
|
+
const prevBoolCtx = _boolCtx;
|
|
373
|
+
if (predicateMethod && i === 0)
|
|
374
|
+
_boolCtx = prevBoolCtx || needsBoolConnectives(a);
|
|
375
|
+
let rendered;
|
|
376
|
+
try {
|
|
377
|
+
rendered = emitExpr(a);
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
_boolCtx = prevBoolCtx;
|
|
381
|
+
}
|
|
382
|
+
return (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall")
|
|
383
|
+
? `(${rendered})` : rendered;
|
|
384
|
+
});
|
|
358
385
|
return emitMethodCall(e.objTy.kind, e.method, e.monadic, receiver, args);
|
|
359
386
|
}
|
|
360
387
|
case "lambda": {
|
|
@@ -385,7 +412,7 @@ function emitExpr(e, parentPrec) {
|
|
|
385
412
|
const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
|
|
386
413
|
if (ctor && ctor.fields.length > 0) {
|
|
387
414
|
const [yes, no] = e.op === "=" ? ["true", "false"] : ["false", "true"];
|
|
388
|
-
return `(match ${emitExpr(e.left)} with | .${
|
|
415
|
+
return `(match ${emitExpr(e.left)} with | .${leanCtorName(rhs.name)} .. => ${yes} | _ => ${no})`;
|
|
389
416
|
}
|
|
390
417
|
}
|
|
391
418
|
// `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
|
|
@@ -471,7 +498,7 @@ function emitExpr(e, parentPrec) {
|
|
|
471
498
|
const idx = owner.fields.findIndex(f => f.name === e.field);
|
|
472
499
|
const pats = owner.fields.map((_, i) => (i === idx ? "_v" : "_")).join(" ");
|
|
473
500
|
const fty = tyToLean(owner.fields[idx].type);
|
|
474
|
-
return `(match ${emitExpr(e.obj)} with | .${
|
|
501
|
+
return `(match ${emitExpr(e.obj)} with | .${leanCtorName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
|
|
475
502
|
}
|
|
476
503
|
}
|
|
477
504
|
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,10 +355,33 @@ 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). */
|
|
334
372
|
function inferQuantVarType(varName, body, ctx) {
|
|
373
|
+
// Spec membership uses a binary node (`x in collection`), unlike the
|
|
374
|
+
// executable `.has(x)` / `.includes(x)` calls handled below. Infer the
|
|
375
|
+
// quantified variable from the RHS collection so documented forms such as
|
|
376
|
+
// `forall(x, x in \result ==> ...)` do not silently fall back to `int`.
|
|
377
|
+
if (body.kind === "binop" && body.op === "in" &&
|
|
378
|
+
body.left.kind === "var" && body.left.name === varName) {
|
|
379
|
+
const collectionTy = resolveExpr(body.right, ctx).ty;
|
|
380
|
+
if (collectionTy.kind === "map")
|
|
381
|
+
return collectionTy.key;
|
|
382
|
+
if (collectionTy.kind === "set" || collectionTy.kind === "array")
|
|
383
|
+
return collectionTy.elem;
|
|
384
|
+
}
|
|
335
385
|
// Look for membership/lookup builtins (map.has(k), map.get(k),
|
|
336
386
|
// array.includes(k) — registry `argIsKey`) where k is our variable
|
|
337
387
|
if (body.kind === "call" && body.fn.kind === "field" &&
|
|
@@ -399,6 +449,8 @@ function classifyCall(fn, ctx) {
|
|
|
399
449
|
return "pure";
|
|
400
450
|
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray")
|
|
401
451
|
return "pure";
|
|
452
|
+
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode")
|
|
453
|
+
return "pure";
|
|
402
454
|
if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
|
|
403
455
|
return "spec-pure";
|
|
404
456
|
// Bare-name `//@ extern` declarations are emitted as `function {:axiom}` —
|
|
@@ -515,7 +567,7 @@ function isDefinedCheckRawLambda(raw) {
|
|
|
515
567
|
const isUndef = (x) => x.kind === "var" && x.name === "undefined";
|
|
516
568
|
return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
|
|
517
569
|
}
|
|
518
|
-
/** Coerce call arguments
|
|
570
|
+
/** Coerce call arguments to their declared parameter slots and pad missing optional args. */
|
|
519
571
|
function coerceCallArgs(args, fn, ctx) {
|
|
520
572
|
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
521
573
|
return args;
|
|
@@ -523,11 +575,7 @@ function coerceCallArgs(args, fn, ctx) {
|
|
|
523
575
|
args = args.map((a, i) => {
|
|
524
576
|
if (i >= paramTys.length)
|
|
525
577
|
return a;
|
|
526
|
-
|
|
527
|
-
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
528
|
-
return wrapSome(a, paramTys[i]);
|
|
529
|
-
}
|
|
530
|
-
return a;
|
|
578
|
+
return coerceToTargetTy(a, paramTys[i], ctx.typeDecls);
|
|
531
579
|
});
|
|
532
580
|
// Pad missing optional args with None
|
|
533
581
|
for (let i = args.length; i < paramTys.length; i++) {
|
|
@@ -546,6 +594,11 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
546
594
|
if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
|
|
547
595
|
return { kind: "bool" };
|
|
548
596
|
}
|
|
597
|
+
// `String.fromCharCode(n)` is the inverse of `s.charCodeAt(i)`: an int code
|
|
598
|
+
// point in, a one-character string out.
|
|
599
|
+
if (fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode") {
|
|
600
|
+
return { kind: "string" };
|
|
601
|
+
}
|
|
549
602
|
// Math.* numeric builtins: abs/min/max preserve the operand's numeric type
|
|
550
603
|
// (real if any operand is real); floor/ceil/round/trunc return an integer.
|
|
551
604
|
if (fn.obj.kind === "var" && fn.obj.name === "Math") {
|
|
@@ -803,7 +856,7 @@ function resolveExpr(e, ctx) {
|
|
|
803
856
|
if (ext) {
|
|
804
857
|
const args = e.args.map(a => resolveExpr(a, ctx));
|
|
805
858
|
const fn = { kind: "var", name: ext.flat, ty: { kind: "unknown" } };
|
|
806
|
-
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure" };
|
|
859
|
+
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure", paramTys: ext.params };
|
|
807
860
|
}
|
|
808
861
|
}
|
|
809
862
|
const fn = resolveExpr(e.fn, ctx);
|
|
@@ -819,7 +872,8 @@ function resolveExpr(e, ctx) {
|
|
|
819
872
|
const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
|
|
820
873
|
let args = coerceCallArgs(rawArgs.map((a, i) => {
|
|
821
874
|
let aCtx = argCtx;
|
|
822
|
-
if (paramTypes && i < paramTypes.length &&
|
|
875
|
+
if (paramTypes && i < paramTypes.length &&
|
|
876
|
+
(paramTypes[i].kind === "user" || paramTypes[i].kind === "array" || paramTypes[i].kind === "optional")) {
|
|
823
877
|
aCtx = { ...aCtx, returnTy: paramTypes[i] };
|
|
824
878
|
}
|
|
825
879
|
return resolveExpr(a, aCtx);
|
|
@@ -854,7 +908,8 @@ function resolveExpr(e, ctx) {
|
|
|
854
908
|
}
|
|
855
909
|
const builtinId = fn.kind === "field" ? recognizeBuiltin(fn.obj.ty, fn.field) : null;
|
|
856
910
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx),
|
|
857
|
-
...(builtinId ? { builtinId } : {})
|
|
911
|
+
...(builtinId ? { builtinId } : {}),
|
|
912
|
+
...(paramTypes ? { paramTys: paramTypes } : {}) };
|
|
858
913
|
}
|
|
859
914
|
case "index": {
|
|
860
915
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -935,7 +990,7 @@ function resolveExpr(e, ctx) {
|
|
|
935
990
|
const inner = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
936
991
|
// The default shares the result type, so coerce a string literal to a
|
|
937
992
|
// string-union enum (e.g. `availableLevels[0] ?? "off"`).
|
|
938
|
-
const right =
|
|
993
|
+
const right = coerceToTargetTy(resolveExpr(e.right, ctx), inner, ctx.typeDecls);
|
|
939
994
|
// `??` is only total when its default is: with a nullable right operand
|
|
940
995
|
// (rule-chain style `ruleA(e) ?? ruleB(e) ?? null`), the result stays
|
|
941
996
|
// optional — otherwise the enclosing chain level loses its optionality
|
|
@@ -1047,7 +1102,6 @@ function resolveExpr(e, ctx) {
|
|
|
1047
1102
|
let value = resolveExpr(f.value, valueCtx);
|
|
1048
1103
|
if (fieldDecl) {
|
|
1049
1104
|
const declTy = fieldDecl.type;
|
|
1050
|
-
value = coerceStr(value, declTy);
|
|
1051
1105
|
// Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
|
|
1052
1106
|
if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
|
|
1053
1107
|
value = { kind: "arrayLiteral", elems: [], ty: declTy };
|
|
@@ -1088,7 +1142,7 @@ function resolveExpr(e, ctx) {
|
|
|
1088
1142
|
const elems = e.elems.map((el, i) => {
|
|
1089
1143
|
const slot = slots[i];
|
|
1090
1144
|
const r = resolveExpr(el, slot ? { ...ctx, returnTy: slot } : ctx);
|
|
1091
|
-
return slot ?
|
|
1145
|
+
return slot ? coerceToTargetTy(r, slot, ctx.typeDecls) : r;
|
|
1092
1146
|
});
|
|
1093
1147
|
return { kind: "arrayLiteral", elems, ty: { kind: "tuple", elems: elems.map(x => x.ty) } };
|
|
1094
1148
|
}
|
|
@@ -1105,7 +1159,7 @@ function resolveExpr(e, ctx) {
|
|
|
1105
1159
|
const r = resolveExpr(el, elemCtx);
|
|
1106
1160
|
// Coerce a bare string-literal element to a string-union enum (e.g.
|
|
1107
1161
|
// `["off", …]: ModelThinkingLevel[]`), like return/arg positions.
|
|
1108
|
-
return expectedElem ?
|
|
1162
|
+
return expectedElem ? coerceToTargetTy(r, expectedElem, ctx.typeDecls) : r;
|
|
1109
1163
|
});
|
|
1110
1164
|
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
1111
1165
|
// No expected collection type: infer array vs tuple from the elements —
|
|
@@ -1173,21 +1227,7 @@ function resolveExpr(e, ctx) {
|
|
|
1173
1227
|
let else_ = resolveExpr(e.else, elseCtx);
|
|
1174
1228
|
then_ = coerceStr(then_, else_.ty);
|
|
1175
1229
|
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
|
-
}
|
|
1230
|
+
const ty = conditionalResultTy(then_.ty, else_.ty);
|
|
1191
1231
|
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
1192
1232
|
}
|
|
1193
1233
|
case "emptyCollection": {
|
|
@@ -1284,7 +1324,16 @@ function resolveStmt(s, ctx) {
|
|
|
1284
1324
|
// one optional level when consulting returnTy.
|
|
1285
1325
|
const initCtx = (declTy.kind === "user" || declTy.kind === "array" || declTy.kind === "optional")
|
|
1286
1326
|
? { ...ctx, returnTy: declTy } : ctx;
|
|
1287
|
-
|
|
1327
|
+
let init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
1328
|
+
// Under noUncheckedIndexedAccess, TS gives `const e = arr[i]` type T | undefined
|
|
1329
|
+
// while the index expression itself resolves to T. Leave that mismatch intact:
|
|
1330
|
+
// narrow.ts's ruleOptionalIndexBinding adds the runtime bounds guard and the
|
|
1331
|
+
// corresponding Some/None branches. Wrapping here would turn it into an
|
|
1332
|
+
// unconditional Some(arr[i]) and prevent that JS-semantics rewrite from firing.
|
|
1333
|
+
const deferOptionalIndex = declTy.kind === "optional" && init.kind === "index" &&
|
|
1334
|
+
init.obj.ty.kind === "array" && init.ty.kind !== "optional";
|
|
1335
|
+
if (!deferOptionalIndex)
|
|
1336
|
+
init = coerceToTargetTy(init, declTy, ctx.typeDecls);
|
|
1288
1337
|
let ty;
|
|
1289
1338
|
if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
|
|
1290
1339
|
// ts-morph's declared type is opaque to us (an expanded union it made
|
|
@@ -1295,7 +1344,7 @@ function resolveStmt(s, ctx) {
|
|
|
1295
1344
|
? { kind: "optional", inner: init.ty }
|
|
1296
1345
|
: init.ty;
|
|
1297
1346
|
}
|
|
1298
|
-
else if (declTy
|
|
1347
|
+
else if (isWidenedStringUnionTy(declTy, init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
|
|
1299
1348
|
// ts-morph widened a string-union to `string`; keep the initializer's
|
|
1300
1349
|
// datatype so `local === "lit"` lowers to a discriminant test.
|
|
1301
1350
|
ty = init.ty;
|
|
@@ -1321,22 +1370,11 @@ function resolveStmt(s, ctx) {
|
|
|
1321
1370
|
// to their named datatypes.
|
|
1322
1371
|
const valueCtx = (targetTy.kind === "user" || targetTy.kind === "array" || targetTy.kind === "optional")
|
|
1323
1372
|
? { ...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
|
-
}
|
|
1373
|
+
const value = coerceToTargetTy(resolveExpr(s.value, valueCtx), targetTy, ctx.typeDecls);
|
|
1330
1374
|
return [{ kind: "assign", target: s.target, value }, ctx.env];
|
|
1331
1375
|
}
|
|
1332
1376
|
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
|
-
}
|
|
1377
|
+
const value = coerceToTargetTy(resolveExpr(s.value, ctx), ctx.returnTy, ctx.typeDecls);
|
|
1340
1378
|
return [{ kind: "return", value }, ctx.env];
|
|
1341
1379
|
}
|
|
1342
1380
|
case "break":
|
|
@@ -1452,14 +1490,27 @@ function resolveStmt(s, ctx) {
|
|
|
1452
1490
|
}
|
|
1453
1491
|
}
|
|
1454
1492
|
// ── Pure / return-in-loop detection ──────────────────────────
|
|
1455
|
-
/**
|
|
1493
|
+
/** Whether a raw statement or expression tree contains a havoc anywhere. */
|
|
1494
|
+
function containsHavoc(v) {
|
|
1495
|
+
if (Array.isArray(v))
|
|
1496
|
+
return v.some(containsHavoc);
|
|
1497
|
+
if (v === null || typeof v !== "object")
|
|
1498
|
+
return false;
|
|
1499
|
+
if (v.kind === "havoc")
|
|
1500
|
+
return true;
|
|
1501
|
+
return Object.values(v).some(containsHavoc);
|
|
1502
|
+
}
|
|
1503
|
+
/** Syntactic purity: no while, no for-of, no mutable let, no havoc. */
|
|
1456
1504
|
function isSyntacticallyPure(stmts) {
|
|
1457
1505
|
for (const s of stmts) {
|
|
1506
|
+
// Havoc lowers to Dafny's `*`, which is only valid in a method.
|
|
1507
|
+
if (containsHavoc(s))
|
|
1508
|
+
return false;
|
|
1458
1509
|
switch (s.kind) {
|
|
1459
1510
|
case "while":
|
|
1460
1511
|
case "forof": return false;
|
|
1461
1512
|
case "let":
|
|
1462
|
-
if (s.mutable
|
|
1513
|
+
if (s.mutable)
|
|
1463
1514
|
return false;
|
|
1464
1515
|
break;
|
|
1465
1516
|
case "if":
|
|
@@ -1779,7 +1830,8 @@ export function resolveModule(raw) {
|
|
|
1779
1830
|
// record literals on map-typed constants (e.g. `Record<string, number>`)
|
|
1780
1831
|
// get their `ty` set to `map<...>` rather than `user("...")`.
|
|
1781
1832
|
const valueCtx = { ...emptyCtx, returnTy: ty };
|
|
1782
|
-
|
|
1833
|
+
const value = coerceToTargetTy(resolveExpr(c.value, valueCtx), ty, raw.typeDecls);
|
|
1834
|
+
return { name: c.name, ty, value };
|
|
1783
1835
|
});
|
|
1784
1836
|
const moduleConstants = new Map(constants.map(c => [c.name, c.ty]));
|
|
1785
1837
|
return {
|
package/tools/dist/transform.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* No type lookups, no string parsing, no re-inference.
|
|
6
6
|
*/
|
|
7
7
|
import { tyEqual } from "./typedir.js";
|
|
8
|
-
import { anyExprInStmts, pWild, pCtor, patternBinders, patternBinds, patternCtor } from "./ir.js";
|
|
8
|
+
import { anyExprInStmts, pWild, pCtor, pLiteral, patternBinders, patternBinds, patternCtor } from "./ir.js";
|
|
9
9
|
import { parseTsType } from "./types.js";
|
|
10
10
|
import { freshName } from "./names.js";
|
|
11
11
|
import { builtinSpec } from "./builtins.js";
|
|
@@ -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") {
|
|
@@ -1365,13 +1387,23 @@ function matchToIfChains(stmts) {
|
|
|
1365
1387
|
if (s.kind !== "match")
|
|
1366
1388
|
return [s];
|
|
1367
1389
|
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1368
|
-
|
|
1390
|
+
// Literal-value matches are not user-inductive matches and need no
|
|
1391
|
+
// discriminator rewrite. In particular, don't let a later ctor arm make us
|
|
1392
|
+
// accidentally drop an earlier literal arm from a malformed mixed match.
|
|
1393
|
+
if (arms.some(a => a.pattern.kind === "literal"))
|
|
1394
|
+
return [{ ...s, arms }];
|
|
1395
|
+
const ctorArms = arms.filter(a => a.pattern.kind === "ctor");
|
|
1369
1396
|
const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
|
|
1370
1397
|
const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
|
|
1371
1398
|
if (!decl)
|
|
1372
1399
|
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1373
1400
|
const scrutExpr = s.scrutinee;
|
|
1374
1401
|
const defaultArm = arms.find(a => a.pattern.kind === "wild");
|
|
1402
|
+
const declaredCtors = decl.kind === "discriminated-union"
|
|
1403
|
+
? decl.variants?.map(v => v.name)
|
|
1404
|
+
: decl.kind === "string-union" ? decl.values : undefined;
|
|
1405
|
+
const coveredCtors = new Set(ctorArms.map(a => patternCtor(a.pattern)).filter((c) => !!c));
|
|
1406
|
+
const exhaustiveWithoutDefault = !defaultArm && !!declaredCtors && declaredCtors.every(c => coveredCtors.has(c));
|
|
1375
1407
|
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1376
1408
|
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
1377
1409
|
const armBody = ctorArms[k].body;
|
|
@@ -1401,6 +1433,13 @@ function matchToIfChains(stmts) {
|
|
|
1401
1433
|
value: { kind: "field", obj: scrutExpr, field: f.name, fromUnion: decl.name, ctor },
|
|
1402
1434
|
});
|
|
1403
1435
|
});
|
|
1436
|
+
// An exhaustive source match has no fallthrough. Use its final arm as
|
|
1437
|
+
// the unconditional else branch; emitting `else pure ()` would force a
|
|
1438
|
+
// Unit result even when every arm returns the method's result type.
|
|
1439
|
+
if (exhaustiveWithoutDefault && k === ctorArms.length - 1) {
|
|
1440
|
+
elseBranch = [...lets, ...armBody];
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1404
1443
|
elseBranch = [{ kind: "if", cond, then: [...lets, ...armBody], else: elseBranch }];
|
|
1405
1444
|
}
|
|
1406
1445
|
return elseBranch;
|
|
@@ -1754,13 +1793,13 @@ function transformStmt(s, typeDecls) {
|
|
|
1754
1793
|
}
|
|
1755
1794
|
case "if": {
|
|
1756
1795
|
// 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
|
|
1796
|
+
const { binds, expr: cond } = liftMethodCalls(asCondition(s.cond));
|
|
1797
|
+
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
1759
1798
|
}
|
|
1760
1799
|
case "while":
|
|
1761
1800
|
return [{
|
|
1762
1801
|
kind: "while",
|
|
1763
|
-
cond:
|
|
1802
|
+
cond: transformExpr(asCondition(s.cond)),
|
|
1764
1803
|
invariants: s.invariants.map(transformExpr),
|
|
1765
1804
|
decreasing: s.decreases ? transformExpr(s.decreases) : null,
|
|
1766
1805
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
@@ -1808,10 +1847,18 @@ function mapStmtExprs(s, r) {
|
|
|
1808
1847
|
* and delegates body transformation to the caller-provided function.
|
|
1809
1848
|
* Returns null if any body transformation returns null (pure path abort). */
|
|
1810
1849
|
function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
1811
|
-
const decl =
|
|
1850
|
+
const decl = declOf(typeDecls, typeName);
|
|
1851
|
+
if (!decl || (decl.kind !== "string-union" && decl.kind !== "discriminated-union")) {
|
|
1852
|
+
throw new Error(`match type ${typeName} is not a declared union`);
|
|
1853
|
+
}
|
|
1812
1854
|
const arms = [];
|
|
1813
1855
|
for (const c of cases) {
|
|
1814
1856
|
const variant = decl?.variants?.find(v => v.name === c.name);
|
|
1857
|
+
const declared = decl.kind === "string-union"
|
|
1858
|
+
? decl.values?.includes(c.name)
|
|
1859
|
+
: !!variant;
|
|
1860
|
+
if (!declared)
|
|
1861
|
+
throw new Error(`case ${JSON.stringify(c.name)} is not a variant of ${typeName}`);
|
|
1815
1862
|
const fields = variant?.fields ?? [];
|
|
1816
1863
|
const pattern = buildMatchPattern(c.name, fields, varName);
|
|
1817
1864
|
const body = transformBody(c.body, varName, fields, c.name);
|
|
@@ -1821,6 +1868,19 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1821
1868
|
}
|
|
1822
1869
|
return arms;
|
|
1823
1870
|
}
|
|
1871
|
+
/** Build value-pattern arms for a switch on a plain string. Case labels have
|
|
1872
|
+
* already been validated and decoded by extraction; unlike datatype cases,
|
|
1873
|
+
* these are literal values and introduce no constructor-field binders. */
|
|
1874
|
+
function buildLiteralMatchArms(cases, transformBody) {
|
|
1875
|
+
const arms = [];
|
|
1876
|
+
for (const c of cases) {
|
|
1877
|
+
const body = transformBody(c.body);
|
|
1878
|
+
if (body === null)
|
|
1879
|
+
return null;
|
|
1880
|
+
arms.push({ pattern: pLiteral(c.name), body });
|
|
1881
|
+
}
|
|
1882
|
+
return arms;
|
|
1883
|
+
}
|
|
1824
1884
|
function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
1825
1885
|
const decl = declOf(typeDecls, typeName);
|
|
1826
1886
|
// Synth array-unions (discriminant "__isArray__") have single-field variants
|
|
@@ -1903,8 +1963,8 @@ function remainingVariant(typeName, cases, typeDecls) {
|
|
|
1903
1963
|
/** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
|
|
1904
1964
|
* `field`, assuming `obj` is a discriminated union with `field` as its
|
|
1905
1965
|
* discriminant. When that's NOT so — e.g. `obj` is a plain record with an
|
|
1906
|
-
* enum-typed `field` — the switch is really on the
|
|
1907
|
-
*
|
|
1966
|
+
* enum- or string-typed `field` — the switch is really on the field VALUE. This
|
|
1967
|
+
* returns the scrutinee `obj.field` (+ its resolved type) to match directly; null
|
|
1908
1968
|
* for a genuine discriminant switch or `switch(localVar)`, which callers handle
|
|
1909
1969
|
* their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
|
|
1910
1970
|
function enumFieldSwitch(s, typeDecls) {
|
|
@@ -1916,9 +1976,18 @@ function enumFieldSwitch(s, typeDecls) {
|
|
|
1916
1976
|
const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
|
|
1917
1977
|
return {
|
|
1918
1978
|
scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
|
|
1919
|
-
|
|
1979
|
+
fieldTy,
|
|
1920
1980
|
};
|
|
1921
1981
|
}
|
|
1982
|
+
/** Declared datatype matched by a switch, excluding records/aliases/opaque
|
|
1983
|
+
* types. A plain `string` intentionally has no declaration and uses literal
|
|
1984
|
+
* patterns instead. */
|
|
1985
|
+
function switchCtorDecl(typeDecls, ty) {
|
|
1986
|
+
if (!ty)
|
|
1987
|
+
return undefined;
|
|
1988
|
+
const decl = declOfTy(typeDecls, ty);
|
|
1989
|
+
return decl?.kind === "string-union" || decl?.kind === "discriminated-union" ? decl : undefined;
|
|
1990
|
+
}
|
|
1922
1991
|
/** Stamp variant ctor info onto datatype updates of the match scrutinee in
|
|
1923
1992
|
* lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
|
|
1924
1993
|
* `replaceFieldAccess`'s stamping. Emitters need the pin to use
|
|
@@ -1937,15 +2006,24 @@ function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
|
|
|
1937
2006
|
function emitSwitchStmt(s, typeDecls) {
|
|
1938
2007
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1939
2008
|
const ef = enumFieldSwitch(s, typeDecls);
|
|
2009
|
+
const matchTy = ef ? ef.fieldTy : s.expr.ty;
|
|
2010
|
+
const literalCases = matchTy?.kind === "string";
|
|
2011
|
+
const ctorDecl = switchCtorDecl(typeDecls, matchTy);
|
|
2012
|
+
if (!literalCases && !ctorDecl) {
|
|
2013
|
+
const typeName = !matchTy ? "unknown" : matchTy.kind === "user" ? matchTy.name : matchTy.kind;
|
|
2014
|
+
throw new Error(`switch scrutinee must be string-typed or a declared union, got ${typeName}`);
|
|
2015
|
+
}
|
|
1940
2016
|
const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
|
|
1941
|
-
const arms =
|
|
1942
|
-
?
|
|
1943
|
-
:
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
out =
|
|
1947
|
-
|
|
1948
|
-
|
|
2017
|
+
const arms = literalCases
|
|
2018
|
+
? buildLiteralMatchArms(cases, (body) => transformStmts(body, typeDecls))
|
|
2019
|
+
: ef
|
|
2020
|
+
? buildMatchArms(cases, undefined, ctorDecl.name, typeDecls, (body) => transformStmts(body, typeDecls))
|
|
2021
|
+
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", ctorDecl.name, typeDecls, (body, vn, fields, ctorName) => {
|
|
2022
|
+
let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
|
|
2023
|
+
if (ctorName && vn && baseName)
|
|
2024
|
+
out = stampScrutineeUpdates(out, vn, ctorName, baseName);
|
|
2025
|
+
return out;
|
|
2026
|
+
});
|
|
1949
2027
|
if (s.defaultBody.length > 0)
|
|
1950
2028
|
arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
|
|
1951
2029
|
return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
@@ -1967,14 +2045,67 @@ function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
|
1967
2045
|
}));
|
|
1968
2046
|
}
|
|
1969
2047
|
/** Replace all variant fields of obj → match binder vars in typed IR.
|
|
1970
|
-
*
|
|
2048
|
+
* Before replacing field reads, realize TypeScript's structural argument
|
|
2049
|
+
* conversion for calls such as `helper(outcome)` inside a narrowed union arm.
|
|
2050
|
+
* The source value is still the enclosing union in typed IR, while Dafny and
|
|
2051
|
+
* Lean expect the helper's nominal record. Rebuild that record solely from
|
|
2052
|
+
* the fields bound by this arm's match pattern. */
|
|
1971
2053
|
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
1972
|
-
|
|
2054
|
+
const projected = projectStructuralCallArgsInTStmts(stmts, varName, fields);
|
|
2055
|
+
return replaceFieldsInTStmts(projected, varName, fields.map(f => ({
|
|
1973
2056
|
fieldName: f.name,
|
|
1974
2057
|
newName: matchBinder(f.name, varName),
|
|
1975
2058
|
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1976
2059
|
})));
|
|
1977
2060
|
}
|
|
2061
|
+
/** Project a narrowed union scrutinee into a named structural record expected
|
|
2062
|
+
* by a same-module/extern call. Resolution stamps named calls with paramTys;
|
|
2063
|
+
* this pass fires only when every target record field has an identically typed
|
|
2064
|
+
* match-bound source field. That deliberately avoids inventing a broad cast:
|
|
2065
|
+
* it is the nominal-backend witness for the structural call TS already accepts. */
|
|
2066
|
+
function projectStructuralCallArgsInTStmts(stmts, varName, fields) {
|
|
2067
|
+
const sourceFields = fields.map(f => ({
|
|
2068
|
+
...f,
|
|
2069
|
+
resolvedTy: f.type ?? parseTsType(f.tsType),
|
|
2070
|
+
}));
|
|
2071
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
2072
|
+
if (e.kind !== "call" || !e.paramTys)
|
|
2073
|
+
return null;
|
|
2074
|
+
const paramTys = e.paramTys;
|
|
2075
|
+
let changed = false;
|
|
2076
|
+
const args = e.args.map((arg, i) => {
|
|
2077
|
+
if (arg.kind !== "var" || arg.name !== varName || i >= paramTys.length)
|
|
2078
|
+
return arg;
|
|
2079
|
+
const targetTy = paramTys[i];
|
|
2080
|
+
const targetDecl = declOfTy(_typeDecls, targetTy);
|
|
2081
|
+
if (targetTy.kind !== "user" || targetDecl?.kind !== "record" || !targetDecl.fields)
|
|
2082
|
+
return arg;
|
|
2083
|
+
const matched = [];
|
|
2084
|
+
for (const targetField of targetDecl.fields) {
|
|
2085
|
+
const sourceField = sourceFields.find(f => f.name === targetField.name);
|
|
2086
|
+
const targetFieldTy = targetField.type ?? parseTsType(targetField.tsType);
|
|
2087
|
+
if (!sourceField || !tyEqual(sourceField.resolvedTy, targetFieldTy))
|
|
2088
|
+
return arg;
|
|
2089
|
+
matched.push({ targetField, sourceField });
|
|
2090
|
+
}
|
|
2091
|
+
changed = true;
|
|
2092
|
+
return {
|
|
2093
|
+
kind: "record",
|
|
2094
|
+
spread: null,
|
|
2095
|
+
fields: matched.map(m => ({
|
|
2096
|
+
name: m.targetField.name,
|
|
2097
|
+
value: {
|
|
2098
|
+
kind: "var",
|
|
2099
|
+
name: matchBinder(m.sourceField.name, varName),
|
|
2100
|
+
ty: m.sourceField.resolvedTy,
|
|
2101
|
+
},
|
|
2102
|
+
})),
|
|
2103
|
+
ty: targetTy,
|
|
2104
|
+
};
|
|
2105
|
+
});
|
|
2106
|
+
return changed ? { ...e, args } : null;
|
|
2107
|
+
}));
|
|
2108
|
+
}
|
|
1978
2109
|
/** Replace obj.field → replacement var in typed IR expressions (before lowering).
|
|
1979
2110
|
* Mirrors replaceFieldsInTStmts but operates on a single TExpr tree. */
|
|
1980
2111
|
function replaceFieldInTExpr(expr, objName, replacements) {
|
|
@@ -2062,7 +2193,7 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
2062
2193
|
const elseExpr = transformPureBody(elseStmts, typeDecls);
|
|
2063
2194
|
if (!elseExpr)
|
|
2064
2195
|
return null;
|
|
2065
|
-
return { kind: "if", cond:
|
|
2196
|
+
return { kind: "if", cond: transformExpr(asCondition(s.cond)), then: thenExpr, else: elseExpr };
|
|
2066
2197
|
}
|
|
2067
2198
|
case "switch": return transformPureSwitch(s, typeDecls);
|
|
2068
2199
|
case "someMatch": {
|
|
@@ -2095,7 +2226,13 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2095
2226
|
const ef = enumFieldSwitch(s, typeDecls);
|
|
2096
2227
|
if (ef) {
|
|
2097
2228
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2098
|
-
const
|
|
2229
|
+
const literalCases = ef.fieldTy?.kind === "string";
|
|
2230
|
+
const ctorDecl = switchCtorDecl(typeDecls, ef.fieldTy);
|
|
2231
|
+
if (!literalCases && !ctorDecl)
|
|
2232
|
+
return null;
|
|
2233
|
+
const arms = literalCases
|
|
2234
|
+
? buildLiteralMatchArms(cases, (body) => transformPureBody(body, typeDecls))
|
|
2235
|
+
: buildMatchArms(cases, undefined, ctorDecl.name, typeDecls, (body) => transformPureBody(body, typeDecls));
|
|
2099
2236
|
if (!arms)
|
|
2100
2237
|
return null;
|
|
2101
2238
|
if (s.defaultBody.length > 0) {
|
|
@@ -2106,13 +2243,15 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2106
2243
|
}
|
|
2107
2244
|
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
2108
2245
|
}
|
|
2109
|
-
const
|
|
2110
|
-
if (!
|
|
2246
|
+
const ctorDecl = switchCtorDecl(typeDecls, s.expr.ty);
|
|
2247
|
+
if (!ctorDecl)
|
|
2111
2248
|
return null;
|
|
2249
|
+
const typeName = ctorDecl.name;
|
|
2112
2250
|
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
2113
2251
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2114
2252
|
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2115
|
-
|
|
2253
|
+
const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
|
|
2254
|
+
let result = transformPureBody(projected, typeDecls);
|
|
2116
2255
|
if (!result)
|
|
2117
2256
|
return null;
|
|
2118
2257
|
if (fields.length > 0 && vn)
|
|
@@ -2139,7 +2278,8 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2139
2278
|
// the statement-level counterpart of this substitution.
|
|
2140
2279
|
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
2141
2280
|
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2142
|
-
|
|
2281
|
+
const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
|
|
2282
|
+
let result = transformPureBody(projected, typeDecls);
|
|
2143
2283
|
if (!result)
|
|
2144
2284
|
return null;
|
|
2145
2285
|
if (fields.length > 0 && vn)
|
|
@@ -2159,7 +2299,8 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2159
2299
|
const remaining = remainingVariant(chain.typeName, chain.cases, typeDecls);
|
|
2160
2300
|
if (remaining) {
|
|
2161
2301
|
// Exactly one variant left — destructure for variant-specific field access.
|
|
2162
|
-
|
|
2302
|
+
const projected = projectStructuralCallArgsInTStmts(chain.fallthrough, chain.varName, remaining.fields);
|
|
2303
|
+
let body = transformPureBody(projected, typeDecls);
|
|
2163
2304
|
if (!body)
|
|
2164
2305
|
return null;
|
|
2165
2306
|
if (remaining.fields.length > 0)
|