lemmascript 0.5.20 → 0.5.22
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/dafny-emit.js +4 -1
- package/tools/dist/extract.js +96 -15
- package/tools/dist/ir.js +1 -0
- package/tools/dist/lean-emit.js +30 -3
- package/tools/dist/resolve.js +12 -0
- package/tools/dist/transform.js +71 -19
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -1555,15 +1555,18 @@ function qualifyCtor(name, type) {
|
|
|
1555
1555
|
return `${type}.${mapped}`;
|
|
1556
1556
|
return mapped;
|
|
1557
1557
|
}
|
|
1558
|
-
/** Translate a
|
|
1558
|
+
/** Translate a backend-neutral match pattern to Dafny syntax.
|
|
1559
1559
|
* ".ctorName field1 field2" → "ctorName(field1, field2)"
|
|
1560
1560
|
* ".ctorName" → "ctorName"
|
|
1561
|
+
* literal value → quoted Dafny string
|
|
1561
1562
|
* "_" → "_"
|
|
1562
1563
|
*/
|
|
1563
1564
|
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
1564
1565
|
function translatePattern(p) {
|
|
1565
1566
|
if (p.kind === "wild")
|
|
1566
1567
|
return "_";
|
|
1568
|
+
if (p.kind === "literal")
|
|
1569
|
+
return emitExpr({ kind: "str", value: p.value });
|
|
1567
1570
|
const ctorName = (Object.hasOwn(CTOR_MAP, p.ctor) ? CTOR_MAP[p.ctor] : undefined) ?? dafnyCtorName(p.ctor);
|
|
1568
1571
|
if (p.binders.length === 0)
|
|
1569
1572
|
return ctorName;
|
package/tools/dist/extract.js
CHANGED
|
@@ -515,19 +515,78 @@ function extractExpr(node) {
|
|
|
515
515
|
}
|
|
516
516
|
// Arrow function: (x) => expr or (x) => { stmts }
|
|
517
517
|
if (Node.isArrowFunction(node)) {
|
|
518
|
-
|
|
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) => {
|
|
519
531
|
const typeNode = p.getTypeNode();
|
|
520
|
-
|
|
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 };
|
|
521
576
|
});
|
|
522
577
|
// Return type from the checker — inferred when unannotated — so resolve can
|
|
523
578
|
// type return-position record literals and give the lambda a real fn type.
|
|
524
579
|
const returnTsType = typeToString(node.getReturnType());
|
|
525
580
|
const body = node.getBody();
|
|
526
581
|
if (Node.isExpression(body)) {
|
|
527
|
-
|
|
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 };
|
|
528
587
|
}
|
|
529
588
|
if (Node.isBlock(body)) {
|
|
530
|
-
return { kind: "lambda", params, body: extractStmts(body.getStatements()), returnTsType };
|
|
589
|
+
return { kind: "lambda", params, body: [...destructureBindings, ...extractStmts(body.getStatements())], returnTsType };
|
|
531
590
|
}
|
|
532
591
|
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
533
592
|
}
|
|
@@ -1675,10 +1734,16 @@ function extractStmts(stmts) {
|
|
|
1675
1734
|
// B: ...`) — the stripped breaks are the switch exits.
|
|
1676
1735
|
const clauseInfos = s.getClauses().map(clause => {
|
|
1677
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
|
+
}
|
|
1678
1745
|
return {
|
|
1679
|
-
label
|
|
1680
|
-
? clause.getExpression().getText().replace(/^["']|["']$/g, "")
|
|
1681
|
-
: null,
|
|
1746
|
+
label,
|
|
1682
1747
|
stmts,
|
|
1683
1748
|
exits: isExit(stmts[stmts.length - 1]),
|
|
1684
1749
|
};
|
|
@@ -1963,6 +2028,17 @@ export function extractModule(sourceFile) {
|
|
|
1963
2028
|
// here), deduped by qualified name.
|
|
1964
2029
|
_externs.clear();
|
|
1965
2030
|
_externSigTypes.length = 0;
|
|
2031
|
+
// Match a `//@ <kw>` directive only as the first non-whitespace on a line,
|
|
2032
|
+
// so prose mentioning an annotation does not activate it.
|
|
2033
|
+
function hasLineDirective(text, kw) {
|
|
2034
|
+
return new RegExp(String.raw `^[ \t]*//@ ${kw}\b`, "m").test(text);
|
|
2035
|
+
}
|
|
2036
|
+
// Declaration-level directives must be attached as leading comments. Do
|
|
2037
|
+
// not scan a function's whole body: a statement-level `//@ skip` inside it
|
|
2038
|
+
// must not omit the enclosing function.
|
|
2039
|
+
function hasLeadingDirective(node, kw) {
|
|
2040
|
+
return node.getLeadingCommentRanges().some(r => hasLineDirective(r.getText(), kw));
|
|
2041
|
+
}
|
|
1966
2042
|
// Share the module's ts-morph Project with parseTsType (scratch source file
|
|
1967
2043
|
// for type-string parsing). Done before declare-type parsing so any
|
|
1968
2044
|
// parseTsType call downstream uses the same Project.
|
|
@@ -2059,6 +2135,8 @@ export function extractModule(sourceFile) {
|
|
|
2059
2135
|
const constants = [];
|
|
2060
2136
|
for (const stmt of sourceFile.getStatements()) {
|
|
2061
2137
|
if (Node.isVariableStatement(stmt)) {
|
|
2138
|
+
if (hasLeadingDirective(stmt, "skip"))
|
|
2139
|
+
continue;
|
|
2062
2140
|
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
2063
2141
|
if (stmt.getDeclarationList().getFlags() & 2 /* const */) {
|
|
2064
2142
|
const init = decl.getInitializer();
|
|
@@ -2141,11 +2219,9 @@ export function extractModule(sourceFile) {
|
|
|
2141
2219
|
// regex — but its callers should still be verifiable against an
|
|
2142
2220
|
// uninterpreted predicate. Parallel to auto-extern for cross-file calls,
|
|
2143
2221
|
// and emitted the same way (`function {:axiom} foo(...)` in Dafny).
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
function hasLineDirective(text, kw) {
|
|
2148
|
-
return new RegExp(String.raw `^[ \t]*//@ ${kw}\b`, "m").test(text);
|
|
2222
|
+
function hasSkip(f) {
|
|
2223
|
+
return hasLeadingDirective(f.parentStmt ?? f.node, "skip")
|
|
2224
|
+
|| (!!f.parentStmt && hasLeadingDirective(f.node, "skip"));
|
|
2149
2225
|
}
|
|
2150
2226
|
function hasExtern(f) {
|
|
2151
2227
|
if (hasLineDirective(f.node.getFullText(), "extern"))
|
|
@@ -2178,7 +2254,7 @@ export function extractModule(sourceFile) {
|
|
|
2178
2254
|
return null;
|
|
2179
2255
|
}
|
|
2180
2256
|
for (const f of allFns) {
|
|
2181
|
-
if (!hasExtern(f))
|
|
2257
|
+
if (hasSkip(f) || !hasExtern(f))
|
|
2182
2258
|
continue;
|
|
2183
2259
|
const qualified = externName(f) ?? f.name;
|
|
2184
2260
|
const flat = qualified.replace(/\./g, "_");
|
|
@@ -2215,8 +2291,9 @@ export function extractModule(sourceFile) {
|
|
|
2215
2291
|
}
|
|
2216
2292
|
return false;
|
|
2217
2293
|
}
|
|
2218
|
-
const
|
|
2219
|
-
const
|
|
2294
|
+
const nonExternFns = allFns.filter(f => !hasSkip(f) && !hasExtern(f));
|
|
2295
|
+
const hasVerifiedClassMethod = sourceFile.getClasses().some(cls => !hasLeadingDirective(cls, "skip") && cls.getMethods().some(method => !hasLeadingDirective(method, "skip") && hasLineDirective(method.getFullText(), "verify")));
|
|
2296
|
+
const hasVerifyDirective = nonExternFns.some(hasVerify) || hasVerifiedClassMethod;
|
|
2220
2297
|
const fnsToExtract = hasVerifyDirective ? nonExternFns.filter(hasVerify) : nonExternFns;
|
|
2221
2298
|
// `//@ autohavoc` — enable the auto-havoc abstraction (see autohavoc.ts).
|
|
2222
2299
|
// File-level: a directive at column 0 (top of file) enables it for every
|
|
@@ -2730,8 +2807,12 @@ export function extractModule(sourceFile) {
|
|
|
2730
2807
|
// Extract classes with //@ verify methods
|
|
2731
2808
|
const classes = [];
|
|
2732
2809
|
for (const cls of sourceFile.getClasses()) {
|
|
2810
|
+
if (hasLeadingDirective(cls, "skip"))
|
|
2811
|
+
continue;
|
|
2733
2812
|
const methods = [];
|
|
2734
2813
|
for (const method of cls.getMethods()) {
|
|
2814
|
+
if (hasLeadingDirective(method, "skip"))
|
|
2815
|
+
continue;
|
|
2735
2816
|
if (!method.getFullText().includes('//@ verify'))
|
|
2736
2817
|
continue;
|
|
2737
2818
|
methods.push(extractFunction(method));
|
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": {
|
package/tools/dist/resolve.js
CHANGED
|
@@ -370,6 +370,18 @@ function isWidenedStringUnionTy(declTy, initTy, typeDecls) {
|
|
|
370
370
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
371
371
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
372
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
|
+
}
|
|
373
385
|
// Look for membership/lookup builtins (map.has(k), map.get(k),
|
|
374
386
|
// array.includes(k) — registry `argIsKey`) where k is our variable
|
|
375
387
|
if (body.kind === "call" && body.fn.kind === "field" &&
|
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";
|
|
@@ -1387,7 +1387,12 @@ function matchToIfChains(stmts) {
|
|
|
1387
1387
|
if (s.kind !== "match")
|
|
1388
1388
|
return [s];
|
|
1389
1389
|
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1390
|
-
|
|
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");
|
|
1391
1396
|
const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
|
|
1392
1397
|
const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
|
|
1393
1398
|
if (!decl)
|
|
@@ -1842,10 +1847,18 @@ function mapStmtExprs(s, r) {
|
|
|
1842
1847
|
* and delegates body transformation to the caller-provided function.
|
|
1843
1848
|
* Returns null if any body transformation returns null (pure path abort). */
|
|
1844
1849
|
function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
1845
|
-
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
|
+
}
|
|
1846
1854
|
const arms = [];
|
|
1847
1855
|
for (const c of cases) {
|
|
1848
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}`);
|
|
1849
1862
|
const fields = variant?.fields ?? [];
|
|
1850
1863
|
const pattern = buildMatchPattern(c.name, fields, varName);
|
|
1851
1864
|
const body = transformBody(c.body, varName, fields, c.name);
|
|
@@ -1855,6 +1868,19 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1855
1868
|
}
|
|
1856
1869
|
return arms;
|
|
1857
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
|
+
}
|
|
1858
1884
|
function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
1859
1885
|
const decl = declOf(typeDecls, typeName);
|
|
1860
1886
|
// Synth array-unions (discriminant "__isArray__") have single-field variants
|
|
@@ -1937,8 +1963,8 @@ function remainingVariant(typeName, cases, typeDecls) {
|
|
|
1937
1963
|
/** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
|
|
1938
1964
|
* `field`, assuming `obj` is a discriminated union with `field` as its
|
|
1939
1965
|
* discriminant. When that's NOT so — e.g. `obj` is a plain record with an
|
|
1940
|
-
* enum-typed `field` — the switch is really on the
|
|
1941
|
-
*
|
|
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
|
|
1942
1968
|
* for a genuine discriminant switch or `switch(localVar)`, which callers handle
|
|
1943
1969
|
* their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
|
|
1944
1970
|
function enumFieldSwitch(s, typeDecls) {
|
|
@@ -1950,9 +1976,18 @@ function enumFieldSwitch(s, typeDecls) {
|
|
|
1950
1976
|
const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
|
|
1951
1977
|
return {
|
|
1952
1978
|
scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
|
|
1953
|
-
|
|
1979
|
+
fieldTy,
|
|
1954
1980
|
};
|
|
1955
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
|
+
}
|
|
1956
1991
|
/** Stamp variant ctor info onto datatype updates of the match scrutinee in
|
|
1957
1992
|
* lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
|
|
1958
1993
|
* `replaceFieldAccess`'s stamping. Emitters need the pin to use
|
|
@@ -1971,15 +2006,24 @@ function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
|
|
|
1971
2006
|
function emitSwitchStmt(s, typeDecls) {
|
|
1972
2007
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1973
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
|
+
}
|
|
1974
2016
|
const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
|
|
1975
|
-
const arms =
|
|
1976
|
-
?
|
|
1977
|
-
:
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
out =
|
|
1981
|
-
|
|
1982
|
-
|
|
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
|
+
});
|
|
1983
2027
|
if (s.defaultBody.length > 0)
|
|
1984
2028
|
arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
|
|
1985
2029
|
return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
@@ -2182,7 +2226,13 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2182
2226
|
const ef = enumFieldSwitch(s, typeDecls);
|
|
2183
2227
|
if (ef) {
|
|
2184
2228
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2185
|
-
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));
|
|
2186
2236
|
if (!arms)
|
|
2187
2237
|
return null;
|
|
2188
2238
|
if (s.defaultBody.length > 0) {
|
|
@@ -2193,9 +2243,10 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2193
2243
|
}
|
|
2194
2244
|
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
2195
2245
|
}
|
|
2196
|
-
const
|
|
2197
|
-
if (!
|
|
2246
|
+
const ctorDecl = switchCtorDecl(typeDecls, s.expr.ty);
|
|
2247
|
+
if (!ctorDecl)
|
|
2198
2248
|
return null;
|
|
2249
|
+
const typeName = ctorDecl.name;
|
|
2199
2250
|
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
2200
2251
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2201
2252
|
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
@@ -2474,7 +2525,8 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2474
2525
|
};
|
|
2475
2526
|
});
|
|
2476
2527
|
// Def file: Velvet methods
|
|
2477
|
-
// Pure functions get a thin wrapper
|
|
2528
|
+
// Pure functions get a thin wrapper. Lean keeps pure definitions in the
|
|
2529
|
+
// `Pure` namespace; Dafny flattens that namespace, so its call is unqualified.
|
|
2478
2530
|
// def-by-method functions also skip their method wrappers
|
|
2479
2531
|
const pureDefNames = new Set([...pureDefs.map(d => d.name), ...defByMethods.map(d => d.name)]);
|
|
2480
2532
|
const methods = mod.functions.map(fn => {
|
|
@@ -2488,7 +2540,7 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2488
2540
|
}
|
|
2489
2541
|
_forofCounters.clear();
|
|
2490
2542
|
let body = pureDefNames.has(fn.name)
|
|
2491
|
-
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}
|
|
2543
|
+
? [{ kind: "return", value: { kind: "app", fn: _opts.backend === "lean" ? `Pure.${fn.name}` : fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
2492
2544
|
: promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
|
|
2493
2545
|
// Lean-only method-body rewrites (Velvet can't WP-synthesize monadic matches
|
|
2494
2546
|
// and forbids `return` in loops):
|