lemmascript 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/package.json +3 -2
- package/tools/dist/autohavoc.js +536 -0
- package/tools/dist/dafny-emit.js +87 -13
- package/tools/dist/extract.js +175 -16
- package/tools/dist/lean-emit.js +69 -9
- package/tools/dist/lsc.js +5 -1
- package/tools/dist/narrow.js +32 -4
- package/tools/dist/peephole.js +3 -0
- package/tools/dist/resolve.js +103 -10
- package/tools/dist/transform.js +99 -10
- package/tools/dist/typedir.js +4 -1
- package/tools/dist/types.js +12 -2
package/tools/dist/lean-emit.js
CHANGED
|
@@ -7,7 +7,22 @@ function tyToLean(ty) {
|
|
|
7
7
|
switch (ty.kind) {
|
|
8
8
|
case "nat": return "Nat";
|
|
9
9
|
case "int": return "Int";
|
|
10
|
-
case "real":
|
|
10
|
+
case "real":
|
|
11
|
+
// Real arithmetic isn't supported by the Lean backend yet: ℝ is
|
|
12
|
+
// noncomputable and needs Mathlib's real-number development, so we fail
|
|
13
|
+
// fast here rather than emit Lean that can't compile.
|
|
14
|
+
//
|
|
15
|
+
// Workarounds, in order of preference:
|
|
16
|
+
// 1. If integer division was intended, write `Math.floor(a / b)` — it
|
|
17
|
+
// lowers to flooring integer division on Lean (no real involved).
|
|
18
|
+
// 2. For `bigint` operands, `/` is already integer division — declaring
|
|
19
|
+
// the value `bigint` instead of `number` keeps it off the real path.
|
|
20
|
+
// 3. If the file genuinely needs reals, restrict it to Dafny with a
|
|
21
|
+
// `//@ backend dafny` directive.
|
|
22
|
+
// Full Lean real support is feasible but was set aside: it needs
|
|
23
|
+
// `import Mathlib.Data.Real.Basic`, `noncomputable def`s for real-valued
|
|
24
|
+
// functions, and the Int→ℝ coercion (see the stashed WIP for a sketch).
|
|
25
|
+
throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
|
|
11
26
|
case "bool": return "Bool";
|
|
12
27
|
case "string": return "String";
|
|
13
28
|
case "void": return "Unit";
|
|
@@ -90,6 +105,16 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
90
105
|
return `Array.push ${obj} ${args[0]}`;
|
|
91
106
|
if (method === "concat")
|
|
92
107
|
return `Array.push ${obj} ${args[0]}`;
|
|
108
|
+
// arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
|
|
109
|
+
// type in Lean, so the receiver itself); one arg drops the prefix, two args
|
|
110
|
+
// give the half-open range. Matches JS for non-negative bounds (negative
|
|
111
|
+
// indices are unsupported — same caveat as the Dafny backend's direct slice).
|
|
112
|
+
if (method === "slice" && args.length === 0)
|
|
113
|
+
return obj;
|
|
114
|
+
if (method === "slice" && args.length === 1)
|
|
115
|
+
return `${obj}.extract ${args[0]} ${obj}.size`;
|
|
116
|
+
if (method === "slice" && args.length === 2)
|
|
117
|
+
return `${obj}.extract ${args[0]} ${args[1]}`;
|
|
93
118
|
}
|
|
94
119
|
// String methods
|
|
95
120
|
if (tyKind === "string") {
|
|
@@ -122,12 +147,17 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
122
147
|
throw new Error(`Unsupported Lean method call: .${method}() on ${tyKind}`);
|
|
123
148
|
}
|
|
124
149
|
// ── Expression emission ─────────────────────────────────────
|
|
125
|
-
// Lean
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
150
|
+
// Some Lean term forms extend their body as far as possible: `∀`/`∃` bodies,
|
|
151
|
+
// and `if`/`let` tails. As an operator operand they would swallow the operator
|
|
152
|
+
// — `(if c then 1 else 0) + r` written bare parses as `if c then 1 else (0 + r)`.
|
|
153
|
+
// Wrap these forms in parens so the operand is closed before the operator.
|
|
154
|
+
// (`match` self-parenthesizes in `emitExpr`, and other forms close via
|
|
155
|
+
// precedence, so neither needs wrapping here.)
|
|
156
|
+
function wrapOperand(sub, parentPrec) {
|
|
129
157
|
const inner = emitExpr(sub, parentPrec);
|
|
130
|
-
return (sub.kind === "forall" || sub.kind === "exists"
|
|
158
|
+
return (sub.kind === "forall" || sub.kind === "exists" ||
|
|
159
|
+
sub.kind === "if" || sub.kind === "let")
|
|
160
|
+
? `(${inner})` : inner;
|
|
131
161
|
}
|
|
132
162
|
function emitExpr(e, parentPrec) {
|
|
133
163
|
switch (e.kind) {
|
|
@@ -183,19 +213,30 @@ function emitExpr(e, parentPrec) {
|
|
|
183
213
|
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
184
214
|
}
|
|
185
215
|
const op = e.op === "arrayConcat" ? "++" : e.op;
|
|
186
|
-
const s = `${
|
|
216
|
+
const s = `${wrapOperand(e.left, prec(e.op))} ${op} ${wrapOperand(e.right, prec(e.op))}`;
|
|
187
217
|
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
188
218
|
}
|
|
189
219
|
case "implies": {
|
|
190
|
-
const parts = [...e.premises.map(p =>
|
|
220
|
+
const parts = [...e.premises.map(p => wrapOperand(p)), emitExpr(e.conclusion)];
|
|
191
221
|
const s = parts.join(" → ");
|
|
192
222
|
return parentPrec !== undefined ? `(${s})` : s;
|
|
193
223
|
}
|
|
194
224
|
case "app": {
|
|
195
225
|
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
226
|
+
// Datatype constructor (tagged by transform): Lean needs the qualified name
|
|
227
|
+
// `BaseType.variant`; a bare `variant` is an unknown identifier. (Dafny keeps
|
|
228
|
+
// the bare form, so its output is unaffected.)
|
|
229
|
+
if (e.ctorOf)
|
|
230
|
+
return args.length ? `${e.ctorOf}.${e.fn} ${args.join(" ")}` : `${e.ctorOf}.${e.fn}`;
|
|
196
231
|
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
197
232
|
if (e.fn === "SetToSeq" && args.length === 1)
|
|
198
233
|
return `${args[0]}.toArray`;
|
|
234
|
+
// perm(a, b) → `List.Perm` on the underlying lists. Dafny lowers it to
|
|
235
|
+
// `multiset(a) == multiset(b)`; the Lean image is `a.toList ~ b.toList`,
|
|
236
|
+
// which mathlib's `List.Perm` provides (reflexivity, symmetry,
|
|
237
|
+
// `perm_append_comm`, and `Perm.count_eq` for the count-invariance payoff).
|
|
238
|
+
if (e.fn === "Perm" && args.length === 2)
|
|
239
|
+
return `(${args[0]}.toList).Perm (${args[1]}.toList)`;
|
|
199
240
|
return `${e.fn} ${args.join(" ")}`;
|
|
200
241
|
}
|
|
201
242
|
case "field": {
|
|
@@ -210,6 +251,10 @@ function emitExpr(e, parentPrec) {
|
|
|
210
251
|
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
211
252
|
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
212
253
|
}
|
|
254
|
+
case "toReal":
|
|
255
|
+
// A real value reached the Lean backend via coercion (e.g. number `/`).
|
|
256
|
+
// Same unsupported-real story as the `real` type case in tyToLean.
|
|
257
|
+
throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
|
|
213
258
|
case "index":
|
|
214
259
|
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
215
260
|
case "record": {
|
|
@@ -393,9 +438,24 @@ function emitDecl(d) {
|
|
|
393
438
|
case "type-alias": {
|
|
394
439
|
return `abbrev ${d.name} := ${tyToLean(d.target)}`;
|
|
395
440
|
}
|
|
441
|
+
case "opaque-type": {
|
|
442
|
+
// Abstract type — no definition. Never constructed or destructured.
|
|
443
|
+
return `opaque ${d.name} : Type`;
|
|
444
|
+
}
|
|
396
445
|
case "def": {
|
|
397
446
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
398
|
-
|
|
447
|
+
let out = `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
|
|
448
|
+
// A `//@ decreases` on a pure function marks it recursive and names its
|
|
449
|
+
// termination measure — emit it as Lean's `termination_by`. This is
|
|
450
|
+
// required when the recursion is on `arr.slice(...)` (→ `Array.extract`,
|
|
451
|
+
// which Lean cannot see as a structural subterm); for a bare-Nat counter
|
|
452
|
+
// Lean could infer structural recursion on its own, but honoring the
|
|
453
|
+
// clause uniformly is simpler and harmless. Lean's default `decreasing_by`
|
|
454
|
+
// discharges the goal in both cases (it knows `Array.size_extract`), so no
|
|
455
|
+
// explicit tactic is needed.
|
|
456
|
+
if (d.decreases)
|
|
457
|
+
out += `\ntermination_by ${emitExpr(d.decreases)}`;
|
|
458
|
+
return out;
|
|
399
459
|
}
|
|
400
460
|
case "def-by-method":
|
|
401
461
|
throw new Error("function by method is not supported for Lean backend");
|
package/tools/dist/lsc.js
CHANGED
|
@@ -10,6 +10,7 @@ import path from "path";
|
|
|
10
10
|
import { extractModule } from "./extract.js";
|
|
11
11
|
import { resolveModule } from "./resolve.js";
|
|
12
12
|
import { narrowModule } from "./narrow.js";
|
|
13
|
+
import { autoHavocModule } from "./autohavoc.js";
|
|
13
14
|
import { transformModuleLean, transformModuleDafny } from "./transform.js";
|
|
14
15
|
import { peepholeModule } from "./peephole.js";
|
|
15
16
|
import { emitLeanFile } from "./lean-emit.js";
|
|
@@ -95,7 +96,10 @@ function main() {
|
|
|
95
96
|
// Resolve: Raw IR → Typed IR
|
|
96
97
|
const resolved = resolveModule(raw);
|
|
97
98
|
// Narrow: Typed IR → Typed IR (rewrites optional-narrowing patterns to someMatch)
|
|
98
|
-
|
|
99
|
+
// auto-havoc (//@ autohavoc): replace unmodellable expressions with arbitrary
|
|
100
|
+
// values so verification rests only on the declared contracts (a sound
|
|
101
|
+
// over-approximation). No-op unless a function opts in.
|
|
102
|
+
const typed = autoHavocModule(narrowModule(resolved));
|
|
99
103
|
const dir = path.dirname(absPath);
|
|
100
104
|
const base = path.basename(filePath, ".ts");
|
|
101
105
|
// ── Dafny backend ─────────────────────────────────────────
|
package/tools/dist/narrow.js
CHANGED
|
@@ -349,14 +349,18 @@ function ruleConditionalArrayIsArray(e) {
|
|
|
349
349
|
if (e.kind !== "conditional")
|
|
350
350
|
return null;
|
|
351
351
|
const pos = parseArrayIsArrayCall(e.cond);
|
|
352
|
-
|
|
352
|
+
// `typeof x === "string"` is a positive check like `Array.isArray`, but selects
|
|
353
|
+
// the NonArrayBranch — its then-branch is the matched-variant body.
|
|
354
|
+
const tof = pos ? null : parseTypeofStringCheck(e.cond);
|
|
355
|
+
const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
|
|
353
356
|
? parseArrayIsArrayCall(e.cond.expr)
|
|
354
357
|
: null;
|
|
355
|
-
const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
358
|
+
const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
356
359
|
if (!matched)
|
|
357
360
|
return null;
|
|
358
|
-
const
|
|
359
|
-
const
|
|
361
|
+
const positive = pos ?? tof;
|
|
362
|
+
const thenBody = positive ? e.then : e.else;
|
|
363
|
+
const elseBody = positive ? e.else : e.then;
|
|
360
364
|
return {
|
|
361
365
|
kind: "tagMatch",
|
|
362
366
|
scrutinee: matched.scrutinee,
|
|
@@ -602,6 +606,30 @@ function parseArrayIsArrayCall(call) {
|
|
|
602
606
|
return null;
|
|
603
607
|
return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
|
|
604
608
|
}
|
|
609
|
+
/** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
|
|
610
|
+
* union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
|
|
611
|
+
* The runtime `=== "string"` test matches that branch only when `U` is string —
|
|
612
|
+
* for any other non-array payload (`number | T[]`, …) it never holds, so we must
|
|
613
|
+
* NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
|
|
614
|
+
function parseTypeofStringCheck(e) {
|
|
615
|
+
if (e.kind !== "binop" || e.op !== "===")
|
|
616
|
+
return null;
|
|
617
|
+
const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
|
|
618
|
+
: e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
|
|
619
|
+
const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
|
|
620
|
+
if (!tof || lit !== "string")
|
|
621
|
+
return null;
|
|
622
|
+
if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
|
|
623
|
+
return null;
|
|
624
|
+
const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
|
|
625
|
+
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
626
|
+
if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
|
|
627
|
+
return null;
|
|
628
|
+
const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
|
|
629
|
+
if (valTy?.kind !== "string")
|
|
630
|
+
return null; // guard: the non-array branch must actually be `string`
|
|
631
|
+
return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
|
|
632
|
+
}
|
|
605
633
|
/** A "narrowable path" is a var or a chain of field accesses rooted at a var
|
|
606
634
|
* — i.e., pure and structurally addressable, so transforms can substitute
|
|
607
635
|
* occurrences inside a matched arm without worrying about re-evaluation. */
|
package/tools/dist/peephole.js
CHANGED
|
@@ -20,6 +20,7 @@ function mapExpr(e, f) {
|
|
|
20
20
|
case "app": return { ...e, args: e.args.map(r) };
|
|
21
21
|
case "field": return { ...e, obj: r(e.obj) };
|
|
22
22
|
case "toNat": return { ...e, expr: r(e.expr) };
|
|
23
|
+
case "toReal": return { ...e, expr: r(e.expr) };
|
|
23
24
|
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
24
25
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null,
|
|
25
26
|
fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
@@ -364,6 +365,7 @@ function rewriteChildrenExpr(e) {
|
|
|
364
365
|
case "app": return { ...e, args: e.args.map(r) };
|
|
365
366
|
case "field": return { ...e, obj: r(e.obj) };
|
|
366
367
|
case "toNat": return { ...e, expr: r(e.expr) };
|
|
368
|
+
case "toReal": return { ...e, expr: r(e.expr) };
|
|
367
369
|
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
368
370
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null,
|
|
369
371
|
fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
@@ -445,6 +447,7 @@ function peepholeDecl(d) {
|
|
|
445
447
|
case "inductive":
|
|
446
448
|
case "structure":
|
|
447
449
|
case "type-alias":
|
|
450
|
+
case "opaque-type":
|
|
448
451
|
case "extern":
|
|
449
452
|
return d;
|
|
450
453
|
}
|
package/tools/dist/resolve.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses linked environments (Scheme-style) for lexical scoping.
|
|
5
5
|
* No mutation — each let extends the chain, lookup walks it.
|
|
6
6
|
*/
|
|
7
|
+
import { isBigInt } from "./typedir.js";
|
|
7
8
|
import { parseTsType } from "./types.js";
|
|
8
9
|
import { parseExpr } from "./specparser.js";
|
|
9
10
|
function lookup(env, name) {
|
|
@@ -447,6 +448,12 @@ function tyToTsStr(ty) {
|
|
|
447
448
|
return "number";
|
|
448
449
|
if (ty.kind === "bool")
|
|
449
450
|
return "boolean";
|
|
451
|
+
// Optional element (e.g. a `.filter` over a `T | undefined`-typed map result):
|
|
452
|
+
// type the callback param so its `x !== undefined` check narrows correctly.
|
|
453
|
+
if (ty.kind === "optional") {
|
|
454
|
+
const inner = tyToTsStr(ty.inner);
|
|
455
|
+
return inner ? `${inner} | undefined` : undefined;
|
|
456
|
+
}
|
|
450
457
|
return undefined;
|
|
451
458
|
}
|
|
452
459
|
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
@@ -486,6 +493,21 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
486
493
|
}
|
|
487
494
|
return rawArgs;
|
|
488
495
|
}
|
|
496
|
+
/** A defined-check filter predicate: `(x) => x !== undefined` (expression or
|
|
497
|
+
* single-return body). Detected on the raw IR before narrowing rewrites it. */
|
|
498
|
+
function isDefinedCheckRawLambda(raw) {
|
|
499
|
+
if (raw.kind !== "lambda" || raw.params.length !== 1)
|
|
500
|
+
return false;
|
|
501
|
+
const p = raw.params[0].name;
|
|
502
|
+
const body = Array.isArray(raw.body)
|
|
503
|
+
? (raw.body.length === 1 && raw.body[0].kind === "return" ? raw.body[0].value : null)
|
|
504
|
+
: raw.body;
|
|
505
|
+
if (!body || body.kind !== "binop" || body.op !== "!==")
|
|
506
|
+
return false;
|
|
507
|
+
const isParam = (x) => x.kind === "var" && x.name === p;
|
|
508
|
+
const isUndef = (x) => x.kind === "var" && x.name === "undefined";
|
|
509
|
+
return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
|
|
510
|
+
}
|
|
489
511
|
/** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
|
|
490
512
|
function coerceCallArgs(args, fn, ctx) {
|
|
491
513
|
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
@@ -517,6 +539,16 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
517
539
|
if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
|
|
518
540
|
return { kind: "bool" };
|
|
519
541
|
}
|
|
542
|
+
// Math.* numeric builtins: abs/min/max preserve the operand's numeric type
|
|
543
|
+
// (real if any operand is real); floor/ceil/round/trunc return an integer.
|
|
544
|
+
if (fn.obj.kind === "var" && fn.obj.name === "Math") {
|
|
545
|
+
if (fn.field === "abs" && args.length === 1)
|
|
546
|
+
return args[0].ty;
|
|
547
|
+
if ((fn.field === "min" || fn.field === "max") && args.length >= 1)
|
|
548
|
+
return args.some(a => a.ty.kind === "real") ? { kind: "real" } : args[0].ty;
|
|
549
|
+
if (["floor", "ceil", "round", "trunc"].includes(fn.field))
|
|
550
|
+
return { kind: "int" };
|
|
551
|
+
}
|
|
520
552
|
const objTy = fn.obj.ty;
|
|
521
553
|
if (objTy.kind === "map") {
|
|
522
554
|
if (fn.field === "get")
|
|
@@ -558,8 +590,11 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
558
590
|
if (fn.field === "join" && objTy.elem.kind === "string")
|
|
559
591
|
return { kind: "string" };
|
|
560
592
|
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
561
|
-
const
|
|
562
|
-
|
|
593
|
+
const lam = args[0];
|
|
594
|
+
// Prefer the lambda's declared return type (handles multi-statement bodies
|
|
595
|
+
// where body[0] is an `if`, not a `return`); fall back to the body's return.
|
|
596
|
+
const retTy = lam.ty.kind === "fn" ? lam.ty.result
|
|
597
|
+
: lam.body.length > 0 && lam.body[0].kind === "return" ? lam.body[0].value.ty : { kind: "unknown" };
|
|
563
598
|
return { kind: "array", elem: retTy };
|
|
564
599
|
}
|
|
565
600
|
}
|
|
@@ -613,6 +648,8 @@ function resolveExpr(e, ctx) {
|
|
|
613
648
|
case "num":
|
|
614
649
|
if (!Number.isInteger(e.value))
|
|
615
650
|
return { kind: "num", value: e.value, ty: { kind: "real" } };
|
|
651
|
+
if (e.big)
|
|
652
|
+
return { kind: "num", value: e.value, ty: { kind: "int", big: true } };
|
|
616
653
|
return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
|
|
617
654
|
case "str":
|
|
618
655
|
return { kind: "str", value: e.value, ty: { kind: "string" } };
|
|
@@ -658,16 +695,38 @@ function resolveExpr(e, ctx) {
|
|
|
658
695
|
}
|
|
659
696
|
else if (e.op === "||")
|
|
660
697
|
ty = right.ty;
|
|
661
|
-
else if (
|
|
698
|
+
else if (e.op === "/") {
|
|
699
|
+
// `number / number` is real (floating-point) division: 3 / 2 === 1.5,
|
|
700
|
+
// never 1 — an integer quotient requires an explicit Math.floor (which
|
|
701
|
+
// lowers to JSFloorDiv). But `bigint / bigint` is genuinely integer
|
|
702
|
+
// division in JS (3n / 2n === 1n), so keep it integer.
|
|
703
|
+
ty = (isBigInt(left.ty) || isBigInt(right.ty)) ? { kind: "int", big: true } : { kind: "real" };
|
|
704
|
+
}
|
|
705
|
+
else if (["+", "-", "*", "%"].includes(e.op)) {
|
|
662
706
|
ty = (left.ty.kind === "real" || right.ty.kind === "real") ? { kind: "real" } : left.ty;
|
|
663
707
|
}
|
|
664
708
|
return { kind: "binop", op: e.op, left, right, ty };
|
|
665
709
|
}
|
|
666
710
|
case "unop": {
|
|
667
711
|
const expr = resolveExpr(e.expr, ctx);
|
|
668
|
-
|
|
712
|
+
const ty = e.op === "!" ? { kind: "bool" } : e.op === "typeof" ? { kind: "string" } : expr.ty;
|
|
713
|
+
return { kind: "unop", op: e.op, expr, ty };
|
|
669
714
|
}
|
|
670
715
|
case "call": {
|
|
716
|
+
// perm(a, b): spec-only permutation predicate — true iff `a` and `b` are
|
|
717
|
+
// reorderings of each other (equal as multisets). Lowers to the `Perm`
|
|
718
|
+
// preamble (Dafny `multiset(a) == multiset(b)`; Lean `a.toList ~ b.toList`).
|
|
719
|
+
// It has no runtime counterpart, so it is rejected outside `//@` specs.
|
|
720
|
+
if (e.fn.kind === "var" && e.fn.name === "perm" && e.args.length === 2) {
|
|
721
|
+
if (!ctx.inSpec)
|
|
722
|
+
throw new Error("perm(a, b) may only be used in //@ specifications");
|
|
723
|
+
const a = resolveExpr(e.args[0], ctx);
|
|
724
|
+
const b = resolveExpr(e.args[1], ctx);
|
|
725
|
+
if (a.ty.kind !== "array" || b.ty.kind !== "array")
|
|
726
|
+
throw new Error(`perm(a, b) requires two array arguments (got ${a.ty.kind} and ${b.ty.kind})`);
|
|
727
|
+
const fn = { kind: "var", name: "Perm", ty: { kind: "unknown" } };
|
|
728
|
+
return { kind: "call", fn, args: [a, b], ty: { kind: "bool" }, callKind: "pure" };
|
|
729
|
+
}
|
|
671
730
|
// Extern dispatch: `NS.method(args)` where NS.method is declared via
|
|
672
731
|
// `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
|
|
673
732
|
// rest of the pipeline sees an ordinary pure function. The extern's
|
|
@@ -711,6 +770,16 @@ function resolveExpr(e, ctx) {
|
|
|
711
770
|
if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
|
|
712
771
|
ty = ctx.fnReturns.get(fn.name);
|
|
713
772
|
}
|
|
773
|
+
// filterMap: `seqOfOption.filter(x => x !== undefined)` (a defined-check,
|
|
774
|
+
// typically with an `x is T` type guard) drops the Nones AND unwraps to
|
|
775
|
+
// seq<T>. Rewrite to a synthetic `filterSome` call lowered to the proven
|
|
776
|
+
// SeqFilterSome preamble (a plain `Map(.value, Filter(.Some?))` wouldn't
|
|
777
|
+
// verify — `.value` is partial).
|
|
778
|
+
if (e.fn.kind === "field" && e.fn.field === "filter" && e.args.length === 1
|
|
779
|
+
&& isDefinedCheckRawLambda(e.args[0])
|
|
780
|
+
&& fn.kind === "field" && fn.obj.ty.kind === "array" && fn.obj.ty.elem.kind === "optional") {
|
|
781
|
+
return { kind: "call", fn: { ...fn, field: "filterSome" }, args: [], ty: { kind: "array", elem: fn.obj.ty.elem.inner }, callKind: "method" };
|
|
782
|
+
}
|
|
714
783
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
715
784
|
}
|
|
716
785
|
case "index": {
|
|
@@ -881,7 +950,13 @@ function resolveExpr(e, ctx) {
|
|
|
881
950
|
return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
882
951
|
}
|
|
883
952
|
case "arrayLiteral": {
|
|
884
|
-
|
|
953
|
+
// Thread the expected element type into each element, so a record/union
|
|
954
|
+
// literal in an array resolves to its named datatype rather than an
|
|
955
|
+
// anonymous tuple (mirrors return-position and call-argument records, which
|
|
956
|
+
// get their type via ctx.returnTy). Only narrow when the context type is an
|
|
957
|
+
// array; otherwise leave ctx untouched.
|
|
958
|
+
const elemCtx = ctx.returnTy.kind === "array" ? { ...ctx, returnTy: ctx.returnTy.elem } : ctx;
|
|
959
|
+
const elems = e.elems.map(el => resolveExpr(el, elemCtx));
|
|
885
960
|
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
886
961
|
return { kind: "arrayLiteral", elems, ty: { kind: "array", elem: elemTy } };
|
|
887
962
|
}
|
|
@@ -895,12 +970,21 @@ function resolveExpr(e, ctx) {
|
|
|
895
970
|
let lambdaEnv = ctx.env;
|
|
896
971
|
for (const p of params)
|
|
897
972
|
lambdaEnv = extend(lambdaEnv, p.name, p.ty);
|
|
898
|
-
|
|
973
|
+
// Set returnTy to the lambda's own return annotation (not the enclosing
|
|
974
|
+
// function's), so return-position record literals in the body resolve to
|
|
975
|
+
// their named type rather than an anonymous tuple.
|
|
976
|
+
const lambdaReturnTy = e.returnTsType ? parseTsType(e.returnTsType) : { kind: "unknown" };
|
|
977
|
+
const lambdaCtx = { ...withEnv(ctx, lambdaEnv), inLambda: true, returnTy: lambdaReturnTy };
|
|
899
978
|
// Body: expression (wrap in return stmt) or statement block
|
|
900
979
|
const body = Array.isArray(e.body)
|
|
901
980
|
? resolveBlock(e.body, lambdaCtx)
|
|
902
981
|
: [{ kind: "return", value: resolveExpr(e.body, lambdaCtx) }];
|
|
903
|
-
|
|
982
|
+
// Carry the lambda's type as a fn type when its return is known, so chained
|
|
983
|
+
// array methods (`.map(...).filter(...)`) can infer downstream element types.
|
|
984
|
+
const lamTy = e.returnTsType
|
|
985
|
+
? { kind: "fn", params: params.map(p => p.ty), result: lambdaReturnTy }
|
|
986
|
+
: { kind: "unknown" };
|
|
987
|
+
return { kind: "lambda", params, body, ty: lamTy };
|
|
904
988
|
}
|
|
905
989
|
case "conditional": {
|
|
906
990
|
const cond = resolveExpr(e.cond, ctx);
|
|
@@ -1028,9 +1112,11 @@ function resolveStmt(s, ctx) {
|
|
|
1028
1112
|
// to its underlying type, so array methods / index-assignment on the
|
|
1029
1113
|
// local dispatch correctly (params get the same treatment, see makeParams).
|
|
1030
1114
|
const declTy = expandAlias(resolveTsType(s.tsType, ctx.overrides, s.name), ctx.typeDecls);
|
|
1031
|
-
// Propagate declared type as returnTy so nested record expressions
|
|
1032
|
-
//
|
|
1033
|
-
|
|
1115
|
+
// Propagate declared type as returnTy so nested record expressions resolve
|
|
1116
|
+
// union variants correctly (e.g., EffectState → mode: EffectMode → { kind:
|
|
1117
|
+
// 'Idle' }). Arrays too, so `const xs: Foo[] = [{...}]` threads the element
|
|
1118
|
+
// type into the array literal (see the arrayLiteral case).
|
|
1119
|
+
const initCtx = (declTy.kind === "user" || declTy.kind === "array") ? { ...ctx, returnTy: declTy } : ctx;
|
|
1034
1120
|
const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
1035
1121
|
let ty;
|
|
1036
1122
|
if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
|
|
@@ -1042,6 +1128,12 @@ function resolveStmt(s, ctx) {
|
|
|
1042
1128
|
? { kind: "optional", inner: init.ty }
|
|
1043
1129
|
: init.ty;
|
|
1044
1130
|
}
|
|
1131
|
+
else if ((declTy.kind === "int" || declTy.kind === "nat") && init.ty.kind === "real" && !ctx.overrides.has(s.name)) {
|
|
1132
|
+
// TS infers `number` (→ int/nat) for an expression LS computes as `real`
|
|
1133
|
+
// (e.g. `a / b`, now real division). `number` can't tell them apart, so
|
|
1134
|
+
// trust the real-valued initializer — unless the user pinned the type.
|
|
1135
|
+
ty = init.ty;
|
|
1136
|
+
}
|
|
1045
1137
|
else {
|
|
1046
1138
|
// Map indexing: TS says T, but access can fail → use Optional<T> from init
|
|
1047
1139
|
ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
|
|
@@ -1381,6 +1473,7 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns
|
|
|
1381
1473
|
decreases: fn.decreases ? resolveSpec(fn.decreases, requiresCtx) : null,
|
|
1382
1474
|
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
1383
1475
|
forcePure: fn.pure,
|
|
1476
|
+
autohavoc: fn.autohavoc,
|
|
1384
1477
|
body: resolveBlock(fn.body, bodyCtx),
|
|
1385
1478
|
};
|
|
1386
1479
|
}
|
package/tools/dist/transform.js
CHANGED
|
@@ -32,6 +32,7 @@ function mapExpr(e, f) {
|
|
|
32
32
|
case "app": return { ...e, args: e.args.map(r) };
|
|
33
33
|
case "field": return { ...e, obj: r(e.obj) };
|
|
34
34
|
case "toNat": return { ...e, expr: r(e.expr) };
|
|
35
|
+
case "toReal": return { ...e, expr: r(e.expr) };
|
|
35
36
|
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
36
37
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
37
38
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
@@ -154,6 +155,7 @@ function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
|
154
155
|
}
|
|
155
156
|
const _forofCounters = new Map();
|
|
156
157
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
158
|
+
function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
|
|
157
159
|
function isArray(ty) { return ty.kind === "array"; }
|
|
158
160
|
function isUser(ty) { return ty.kind === "user"; }
|
|
159
161
|
/** Check if transformed lambda body contains monadic binds. */
|
|
@@ -186,6 +188,8 @@ const OP_MAP = {
|
|
|
186
188
|
const BOOL_OP_MAP = {
|
|
187
189
|
...OP_MAP, "===": "==", "!==": "!=",
|
|
188
190
|
};
|
|
191
|
+
/** Arithmetic + comparison ops eligible for int→real operand coercion. */
|
|
192
|
+
const NUMERIC_OPS = new Set(["+", "-", "*", "/", "===", "!==", ">=", "<=", ">", "<"]);
|
|
189
193
|
function transformExpr(e) { return lowerExpr(e, null); }
|
|
190
194
|
/** Reduce an if/let/return-shaped statement body to a single expression, for
|
|
191
195
|
* expression-only lambda bodies. Returns null for shapes that can't be a pure
|
|
@@ -219,6 +223,19 @@ function flattenLambdaBody(stmts) {
|
|
|
219
223
|
const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
|
|
220
224
|
return thenExpr === null || elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenExpr, else: elseExpr };
|
|
221
225
|
}
|
|
226
|
+
// A `switch` lowered to a match-statement: reduce each arm's body to an
|
|
227
|
+
// expression (an arm that doesn't return falls through into `rest`), giving a
|
|
228
|
+
// match-expression — same reduction the `if` case does, one level wider.
|
|
229
|
+
if (first.kind === "match") {
|
|
230
|
+
const arms = [];
|
|
231
|
+
for (const arm of first.arms) {
|
|
232
|
+
const armExpr = flattenLambdaBody([...arm.body, ...rest]);
|
|
233
|
+
if (armExpr === null)
|
|
234
|
+
return null;
|
|
235
|
+
arms.push({ pattern: arm.pattern, body: armExpr });
|
|
236
|
+
}
|
|
237
|
+
return { kind: "match", scrutinee: first.scrutinee, arms };
|
|
238
|
+
}
|
|
222
239
|
return null;
|
|
223
240
|
}
|
|
224
241
|
/**
|
|
@@ -419,6 +436,20 @@ function lowerExpr(e, binds) {
|
|
|
419
436
|
right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
|
|
420
437
|
}
|
|
421
438
|
}
|
|
439
|
+
// Numeric int→real coercion. After resolve, `/` is always real, and any
|
|
440
|
+
// arithmetic/comparison mixing real and integral operands is real-valued.
|
|
441
|
+
// Lift each integral operand to `real` so the backend sees homogeneous
|
|
442
|
+
// real operations (Dafny `as real`, Lean Int→Float).
|
|
443
|
+
if (NUMERIC_OPS.has(e.op)) {
|
|
444
|
+
const realCtx = e.ty.kind === "real" || e.left.ty.kind === "real" || e.right.ty.kind === "real";
|
|
445
|
+
if (realCtx) {
|
|
446
|
+
const lift = (operand) => {
|
|
447
|
+
const lowered = lowerExpr(operand, binds);
|
|
448
|
+
return isIntegral(operand.ty) ? { kind: "toReal", expr: lowered } : lowered;
|
|
449
|
+
};
|
|
450
|
+
return { kind: "binop", op: OP_MAP[e.op] ?? e.op, left: lift(e.left), right: lift(e.right) };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
422
453
|
return {
|
|
423
454
|
kind: "binop",
|
|
424
455
|
op: OP_MAP[e.op] ?? e.op,
|
|
@@ -494,13 +525,23 @@ function lowerExpr(e, binds) {
|
|
|
494
525
|
return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
|
|
495
526
|
return lowerExpr(arg, binds);
|
|
496
527
|
}
|
|
497
|
-
// Math.floor(x):
|
|
528
|
+
// Math.floor(x):
|
|
529
|
+
// - a / b on integral operands → integer floor division, kept in
|
|
530
|
+
// integer arithmetic (JSFloorDiv on Dafny; native Int/Nat `/` floors
|
|
531
|
+
// on Lean). Checked first: after resolve, `a / b` is typed `real`, so
|
|
532
|
+
// the real branch below would otherwise drag it into real arithmetic.
|
|
533
|
+
// - real arg → FloorReal (Dafny's .Floor)
|
|
534
|
+
// - int arg → identity
|
|
498
535
|
if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
499
536
|
const arg = e.args[0];
|
|
537
|
+
if (arg.kind === "binop" && arg.op === "/" && isIntegral(arg.left.ty) && isIntegral(arg.right.ty)) {
|
|
538
|
+
const l = lowerExpr(arg.left, binds), r = lowerExpr(arg.right, binds);
|
|
539
|
+
return _opts.backend === "dafny"
|
|
540
|
+
? { kind: "app", fn: "JSFloorDiv", args: [l, r] }
|
|
541
|
+
: { kind: "binop", op: "/", left: l, right: r };
|
|
542
|
+
}
|
|
500
543
|
if (arg.ty.kind === "real")
|
|
501
544
|
return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
|
|
502
|
-
if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
|
|
503
|
-
return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
|
|
504
545
|
return lowerExpr(arg, binds);
|
|
505
546
|
}
|
|
506
547
|
// Method call: receiver.method(args) → methodCall node
|
|
@@ -564,12 +605,17 @@ function lowerExpr(e, binds) {
|
|
|
564
605
|
if (nonDiscFields.length === 0) {
|
|
565
606
|
return { kind: "constructor", name: variantName, type: tyName };
|
|
566
607
|
}
|
|
567
|
-
// Constructor with args: match variant field order
|
|
608
|
+
// Constructor with args: match variant field order. Emit a bare `app`
|
|
609
|
+
// (Dafny renders `variantName(args)`, a valid unqualified constructor
|
|
610
|
+
// call — unchanged output) tagged with `ctorOf` so the Lean emitter,
|
|
611
|
+
// which CANNOT take a bare constructor name, qualifies it as
|
|
612
|
+
// `BaseType.variantName args`. Use the BASE type name (no generic args):
|
|
613
|
+
// `Result.true_` is valid in Lean; `Result<Model,Err>.true_` is not.
|
|
568
614
|
const args = variant.fields.map(vf => {
|
|
569
615
|
const ef = nonDiscFields.find(f => f.name === vf.name);
|
|
570
616
|
return ef ? lowerExpr(ef.value, binds) : { kind: "var", name: "None" };
|
|
571
617
|
});
|
|
572
|
-
return { kind: "app", fn: variantName, args };
|
|
618
|
+
return { kind: "app", fn: variantName, args, ctorOf: baseName };
|
|
573
619
|
}
|
|
574
620
|
}
|
|
575
621
|
}
|
|
@@ -642,7 +688,10 @@ function lowerExpr(e, binds) {
|
|
|
642
688
|
return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
|
|
643
689
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
644
690
|
case "lambda": {
|
|
645
|
-
|
|
691
|
+
// Pass the module typeDecls (not []), so type lookups inside the lambda
|
|
692
|
+
// body — e.g. a `switch`'s variant fields — resolve. A bare `[]` left a
|
|
693
|
+
// discriminated-union switch in a lambda with binderless patterns.
|
|
694
|
+
const body = transformStmts(e.body, _typeDecls);
|
|
646
695
|
// Flatten an if/let/return-shaped multi-statement body into a single
|
|
647
696
|
// `return <expr>` so both backends' single-return-lambda fast path emits
|
|
648
697
|
// it (Dafny lambdas are expression-only; Lean prefers the expression form
|
|
@@ -1321,14 +1370,37 @@ function remainingVariant(typeName, cases, typeDecls) {
|
|
|
1321
1370
|
return null;
|
|
1322
1371
|
return remaining[0];
|
|
1323
1372
|
}
|
|
1373
|
+
/** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
|
|
1374
|
+
* `field`, assuming `obj` is a discriminated union with `field` as its
|
|
1375
|
+
* discriminant. When that's NOT so — e.g. `obj` is a plain record with an
|
|
1376
|
+
* enum-typed `field` — the switch is really on the enum VALUE. This returns the
|
|
1377
|
+
* enum scrutinee `obj.field` (+ the field's enum type) to match directly; null
|
|
1378
|
+
* for a genuine discriminant switch or `switch(localVar)`, which callers handle
|
|
1379
|
+
* their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
|
|
1380
|
+
function enumFieldSwitch(s, typeDecls) {
|
|
1381
|
+
if (!s.discriminant)
|
|
1382
|
+
return null;
|
|
1383
|
+
const objBase = s.expr.ty.kind === "user"
|
|
1384
|
+
? (s.expr.ty.name.includes("<") ? s.expr.ty.name.slice(0, s.expr.ty.name.indexOf("<")) : s.expr.ty.name)
|
|
1385
|
+
: undefined;
|
|
1386
|
+
const objDecl = objBase ? typeDecls.find(d => d.name === objBase) : undefined;
|
|
1387
|
+
if (objDecl?.kind === "discriminated-union" && objDecl.discriminant === s.discriminant)
|
|
1388
|
+
return null;
|
|
1389
|
+
const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
|
|
1390
|
+
return {
|
|
1391
|
+
scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
|
|
1392
|
+
enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1324
1395
|
function emitSwitchStmt(s, typeDecls) {
|
|
1325
|
-
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
1326
|
-
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
|
|
1327
1396
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1328
|
-
const
|
|
1397
|
+
const ef = enumFieldSwitch(s, typeDecls);
|
|
1398
|
+
const arms = ef
|
|
1399
|
+
? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
|
|
1400
|
+
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
1329
1401
|
if (s.defaultBody.length > 0)
|
|
1330
1402
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
1331
|
-
return { kind: "match", scrutinee:
|
|
1403
|
+
return { kind: "match", scrutinee: ef ? ef.scrutinee : (s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
1332
1404
|
}
|
|
1333
1405
|
/** Replace obj.field → replacement var in typed IR.
|
|
1334
1406
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
@@ -1472,6 +1544,20 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1472
1544
|
return null;
|
|
1473
1545
|
}
|
|
1474
1546
|
function transformPureSwitch(s, typeDecls) {
|
|
1547
|
+
const ef = enumFieldSwitch(s, typeDecls);
|
|
1548
|
+
if (ef) {
|
|
1549
|
+
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1550
|
+
const arms = buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformPureBody(body, typeDecls));
|
|
1551
|
+
if (!arms)
|
|
1552
|
+
return null;
|
|
1553
|
+
if (s.defaultBody.length > 0) {
|
|
1554
|
+
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
1555
|
+
if (!body)
|
|
1556
|
+
return null;
|
|
1557
|
+
arms.push({ pattern: "_", body });
|
|
1558
|
+
}
|
|
1559
|
+
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
1560
|
+
}
|
|
1475
1561
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
|
|
1476
1562
|
if (!typeDecls.find(d => d.name === typeName))
|
|
1477
1563
|
return null;
|
|
@@ -1570,6 +1656,9 @@ function transformTypeDecl(d) {
|
|
|
1570
1656
|
target: d.aliasOfTy,
|
|
1571
1657
|
};
|
|
1572
1658
|
}
|
|
1659
|
+
else if (d.kind === "opaque") {
|
|
1660
|
+
return { kind: "opaque-type", name: d.name };
|
|
1661
|
+
}
|
|
1573
1662
|
else {
|
|
1574
1663
|
return {
|
|
1575
1664
|
kind: "structure", name: d.name,
|