lemmascript 0.5.17 → 0.5.19
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 +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +124 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +162 -35
- package/tools/dist/extract.js +108 -21
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +27 -16
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +297 -678
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +187 -205
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +298 -108
- package/tools/dist/typedecls.js +59 -0
- package/tools/dist/typedir.js +6 -0
package/tools/dist/transform.js
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* Consumes resolved types and classifications.
|
|
5
5
|
* No type lookups, no string parsing, no re-inference.
|
|
6
6
|
*/
|
|
7
|
+
import { tyEqual } from "./typedir.js";
|
|
7
8
|
import { anyExprInStmts, pWild, pCtor, patternBinders, patternBinds, patternCtor } from "./ir.js";
|
|
8
9
|
import { parseTsType } from "./types.js";
|
|
9
10
|
import { freshName } from "./names.js";
|
|
11
|
+
import { builtinSpec } from "./builtins.js";
|
|
12
|
+
import { isFalsyCapableTy } from "./condition-facts.js";
|
|
13
|
+
import { declOf, declOfKind, declOfTy, unionDeclOfTy, declWithVariant, tyBaseName } from "./typedecls.js";
|
|
10
14
|
// ── Generic IR walkers ──────────────────────────────────────
|
|
11
15
|
/**
|
|
12
16
|
* Map over all sub-expressions in an Expr. `f` is called on each node;
|
|
@@ -21,6 +25,7 @@ function mapExpr(e, f) {
|
|
|
21
25
|
switch (e.kind) {
|
|
22
26
|
case "var":
|
|
23
27
|
case "num":
|
|
28
|
+
case "bigint":
|
|
24
29
|
case "bool":
|
|
25
30
|
case "str":
|
|
26
31
|
case "emptyMap":
|
|
@@ -43,7 +48,7 @@ function mapExpr(e, f) {
|
|
|
43
48
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
44
49
|
case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
45
50
|
case "match": {
|
|
46
|
-
const scr =
|
|
51
|
+
const scr = r(e.scrutinee);
|
|
47
52
|
return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
|
|
48
53
|
}
|
|
49
54
|
case "forall": return { ...e, body: r(e.body) };
|
|
@@ -66,7 +71,7 @@ function mapStmt(s, f) {
|
|
|
66
71
|
case "continue": return s;
|
|
67
72
|
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
|
|
68
73
|
case "match": {
|
|
69
|
-
const scr =
|
|
74
|
+
const scr = r(s.scrutinee);
|
|
70
75
|
return { ...s, scrutinee: scr, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
|
|
71
76
|
}
|
|
72
77
|
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
|
|
@@ -81,7 +86,10 @@ function mapStmt(s, f) {
|
|
|
81
86
|
* rest of the block), `match` arm patterns, `forall`/`exists`, and `for-in`
|
|
82
87
|
* indices. Capture-avoiding: a nested scope that reintroduces `from` keeps its
|
|
83
88
|
* own binding untouched. `mapExpr` doesn't descend into lambda bodies, so this
|
|
84
|
-
* walks them by hand
|
|
89
|
+
* walks them by hand — statement binders are tracked only at that body's top
|
|
90
|
+
* level, which is enough for the sole caller (dafny-emit's
|
|
91
|
+
* `comprehensionBinder`). TODO: generalize if more callers need this. */
|
|
92
|
+
const varE = (name) => ({ kind: "var", name });
|
|
85
93
|
export function renameFreeVar(e, from, to) {
|
|
86
94
|
const f = (x) => {
|
|
87
95
|
if (x.kind === "var")
|
|
@@ -93,9 +101,7 @@ export function renameFreeVar(e, from, to) {
|
|
|
93
101
|
if ((x.kind === "forall" || x.kind === "exists") && x.var === from)
|
|
94
102
|
return x;
|
|
95
103
|
if (x.kind === "match") {
|
|
96
|
-
|
|
97
|
-
? (x.scrutinee === from ? to : x.scrutinee) : mapExpr(x.scrutinee, f);
|
|
98
|
-
return { ...x, scrutinee: scr, arms: x.arms.map(a => patternBinds(a.pattern, from) ? a : { ...a, body: mapExpr(a.body, f) }) };
|
|
104
|
+
return { ...x, scrutinee: mapExpr(x.scrutinee, f), arms: x.arms.map(a => patternBinds(a.pattern, from) ? a : { ...a, body: mapExpr(a.body, f) }) };
|
|
99
105
|
}
|
|
100
106
|
if (x.kind === "lambda") {
|
|
101
107
|
if (x.params.some(p => p.name === from))
|
|
@@ -128,6 +134,7 @@ function mapTExpr(e, f) {
|
|
|
128
134
|
switch (e.kind) {
|
|
129
135
|
case "var":
|
|
130
136
|
case "num":
|
|
137
|
+
case "bigint":
|
|
131
138
|
case "str":
|
|
132
139
|
case "bool":
|
|
133
140
|
case "havoc": return e;
|
|
@@ -193,8 +200,33 @@ let _typeDecls = [];
|
|
|
193
200
|
* in a higher-order position resolves to the monadic method, so it must be
|
|
194
201
|
* redirected to the pure mirror. Set once per module transform. */
|
|
195
202
|
let _pureDefNames = new Set();
|
|
196
|
-
/**
|
|
197
|
-
|
|
203
|
+
/** Discriminated unions whose tag is read as a *value* (`x.kind` compared
|
|
204
|
+
* to another union's tag, passed as an argument, …). Each gets a generated
|
|
205
|
+
* `<Union>_<disc>` discriminator function beside its datatype, returning
|
|
206
|
+
* the source tag strings. Narrowing consumes discriminant *checks*, so this
|
|
207
|
+
* fires only for surviving reads. Populated during body transforms; drained
|
|
208
|
+
* into the types file. */
|
|
209
|
+
let _neededKindHelpers = new Map();
|
|
210
|
+
function kindHelperName(decl) {
|
|
211
|
+
return freshName(`${decl.name}_${decl.discriminant}`);
|
|
212
|
+
}
|
|
213
|
+
function kindHelperDecl(decl) {
|
|
214
|
+
return {
|
|
215
|
+
kind: "def",
|
|
216
|
+
name: kindHelperName(decl),
|
|
217
|
+
typeParams: [],
|
|
218
|
+
params: [{ name: "t", type: { kind: "user", name: decl.name } }],
|
|
219
|
+
returnType: { kind: "string" },
|
|
220
|
+
requires: [], ensures: [], decreases: null,
|
|
221
|
+
body: {
|
|
222
|
+
kind: "match", scrutinee: varE("t"),
|
|
223
|
+
arms: decl.variants.map(v => ({
|
|
224
|
+
pattern: { kind: "ctor", ctor: v.name, binders: v.fields.map((_, i) => `_${i}`) },
|
|
225
|
+
body: { kind: "str", value: v.name },
|
|
226
|
+
})),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
198
230
|
/** Prefix match-bound field names to avoid capturing user variables.
|
|
199
231
|
* When prefix is given (the scrutinee name), include it to avoid
|
|
200
232
|
* collisions in nested matches on different variables. `freshName` closes
|
|
@@ -214,17 +246,16 @@ function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
|
|
|
214
246
|
function isArray(ty) { return ty.kind === "array"; }
|
|
215
247
|
function isUser(ty) { return ty.kind === "user"; }
|
|
216
248
|
function isRecordType(ty) {
|
|
217
|
-
|
|
218
|
-
return false;
|
|
219
|
-
const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
|
|
220
|
-
return _typeDecls.find(d => d.name === base)?.kind === "record";
|
|
249
|
+
return declOfTy(_typeDecls, ty)?.kind === "record";
|
|
221
250
|
}
|
|
222
251
|
/** Truthiness test for a *lowered* value of source type `ty`, used by `||`
|
|
223
|
-
* falsiness lowering.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
252
|
+
* falsiness lowering. Which types need one is `isFalsyCapableTy` (shared with
|
|
253
|
+
* condition-facts' falsy gate); this adds the per-type test. Returns null for
|
|
254
|
+
* the always-truthy types so callers can unwrap directly instead of emitting
|
|
255
|
+
* a redundant guard. */
|
|
227
256
|
function valueTruthyCond(value, ty) {
|
|
257
|
+
if (!isFalsyCapableTy(ty))
|
|
258
|
+
return null;
|
|
228
259
|
switch (ty.kind) {
|
|
229
260
|
case "int":
|
|
230
261
|
case "nat":
|
|
@@ -350,7 +381,7 @@ function wrapOptionalBranch(expr, raw) {
|
|
|
350
381
|
// like the scrutinee of an outer match. Dafny treats `Option.Some` and bare
|
|
351
382
|
// `Some` equivalently — the qualification is harmless there.
|
|
352
383
|
if (raw.kind === "var" && raw.name === "undefined")
|
|
353
|
-
return { kind: "constructor", name: "none", type: "Option" };
|
|
384
|
+
return { kind: "constructor", name: "none", type: "Option", args: [] };
|
|
354
385
|
if (raw.ty.kind === "optional")
|
|
355
386
|
return expr; // already Option<T>, don't double-wrap
|
|
356
387
|
return { kind: "constructor", name: "some", type: "Option", args: [expr] };
|
|
@@ -419,12 +450,16 @@ function lowerExpr(e, binds) {
|
|
|
419
450
|
switch (e.kind) {
|
|
420
451
|
case "var": return { kind: "var", name: e.name };
|
|
421
452
|
case "num": return { kind: "num", value: e.value };
|
|
453
|
+
case "bigint": return { kind: "bigint", value: e.value };
|
|
422
454
|
case "bool": return { kind: "bool", value: e.value };
|
|
423
455
|
case "str":
|
|
424
456
|
if (e.ty.kind === "user")
|
|
425
|
-
return { kind: "constructor", name: e.value, type: e.ty.name };
|
|
457
|
+
return { kind: "constructor", name: e.value, type: e.ty.name, args: [] };
|
|
426
458
|
return { kind: "str", value: e.value };
|
|
427
459
|
case "unop":
|
|
460
|
+
// Only `num` folds. A `bigint` payload is a string, so negating it here
|
|
461
|
+
// would coerce through `Number` and round: `-9007199254740993n` stays a
|
|
462
|
+
// structural `unop("-", bigint(...))` and is negated by the emitter.
|
|
428
463
|
if (e.op === "-" && e.expr.kind === "num")
|
|
429
464
|
return { kind: "num", value: -e.expr.value };
|
|
430
465
|
// String truthiness: !str → str == ""
|
|
@@ -469,7 +504,7 @@ function lowerExpr(e, binds) {
|
|
|
469
504
|
kind: "binop",
|
|
470
505
|
op: e.op === "===" ? "=" : "≠",
|
|
471
506
|
left: transformExpr(e.left.obj),
|
|
472
|
-
right: { kind: "constructor", name: e.right.value, type: objTy },
|
|
507
|
+
right: { kind: "constructor", name: e.right.value, type: objTy, args: [] },
|
|
473
508
|
};
|
|
474
509
|
}
|
|
475
510
|
// String literal comparison — constructor if user type, string literal if string.
|
|
@@ -479,7 +514,7 @@ function lowerExpr(e, binds) {
|
|
|
479
514
|
const left = lowerExpr(e.left, binds);
|
|
480
515
|
const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
|
|
481
516
|
const right = isUser(e.left.ty)
|
|
482
|
-
? { kind: "constructor", name: e.right.value, type: leftTy }
|
|
517
|
+
? { kind: "constructor", name: e.right.value, type: leftTy, args: [] }
|
|
483
518
|
: { kind: "str", value: e.right.value };
|
|
484
519
|
return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
|
|
485
520
|
}
|
|
@@ -504,7 +539,7 @@ function lowerExpr(e, binds) {
|
|
|
504
539
|
// non-optional string-literal rule above.
|
|
505
540
|
const innerTy = optSide.ty.kind === "optional" ? optSide.ty.inner : optSide.ty;
|
|
506
541
|
const valExpr = valSide.kind === "str" && innerTy.kind === "user"
|
|
507
|
-
? { kind: "constructor", name: valSide.value, type: innerTy.name }
|
|
542
|
+
? { kind: "constructor", name: valSide.value, type: innerTy.name, args: [] }
|
|
508
543
|
: lowerExpr(valSide, binds);
|
|
509
544
|
const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
|
|
510
545
|
const noneVal = e.op === "!==" ? true : false;
|
|
@@ -543,7 +578,7 @@ function lowerExpr(e, binds) {
|
|
|
543
578
|
// || on optional → match Some/None with default. JS `||` tests falsiness of
|
|
544
579
|
// the *unwrapped* value, so when the inner type can be falsy the Some arm must
|
|
545
580
|
// re-test (`Some(0) || 1 === 1`); array/user inners are always truthy and
|
|
546
|
-
// unwrap directly.
|
|
581
|
+
// unwrap directly. Same gate as condition-facts' `canBeFalsy`.
|
|
547
582
|
if (e.op === "||" && e.left.ty.kind === "optional") {
|
|
548
583
|
const optExpr = lowerExpr(e.left, binds);
|
|
549
584
|
const defaultExpr = lowerExpr(e.right, binds);
|
|
@@ -694,21 +729,41 @@ function lowerExpr(e, binds) {
|
|
|
694
729
|
// as bare truthiness checks. Emit as the Dafny discriminator predicate for the
|
|
695
730
|
// 'true' variant: result.ok → result.true_?
|
|
696
731
|
if (e.isDiscriminant && e.obj.ty.kind === "user") {
|
|
697
|
-
const
|
|
698
|
-
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
732
|
+
const decl = unionDeclOfTy(_typeDecls, e.obj.ty);
|
|
699
733
|
if (decl?.variants?.some(v => v.name === "true")) {
|
|
700
734
|
return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
|
|
701
735
|
}
|
|
736
|
+
// Surviving string-discriminant read (`x.kind` used as a value —
|
|
737
|
+
// compared against another union's tag, passed as an argument, …):
|
|
738
|
+
// lower to the generated per-union discriminator function, which
|
|
739
|
+
// returns the source tag strings. Narrowing consumes discriminant
|
|
740
|
+
// *checks*; this catches reads that survive as values.
|
|
741
|
+
if (decl?.variants && decl.discriminant && decl.discriminant !== "__isArray__") {
|
|
742
|
+
_neededKindHelpers.set(decl.name, decl);
|
|
743
|
+
return { kind: "app", fn: kindHelperName(decl), args: [lowerExpr(e.obj, binds)] };
|
|
744
|
+
}
|
|
702
745
|
}
|
|
703
746
|
// Union destructor: `x.field` where x is a discriminated union and `field`
|
|
704
747
|
// is a data field of one of its variants. Dafny reads the destructor
|
|
705
748
|
// directly; Lean has no field projection on a multi-ctor inductive, so tag
|
|
706
749
|
// the node with the union's base name and let the Lean emitter `match`.
|
|
707
750
|
if (e.obj.ty.kind === "user") {
|
|
708
|
-
const baseName =
|
|
709
|
-
const decl = _typeDecls
|
|
710
|
-
|
|
711
|
-
|
|
751
|
+
const baseName = tyBaseName(e.obj.ty.name);
|
|
752
|
+
const decl = declOfKind(_typeDecls, baseName, "discriminated-union");
|
|
753
|
+
const owners = decl?.variants?.filter(v => v.fields.some(f => f.name === e.field)) ?? [];
|
|
754
|
+
if (owners.length > 0) {
|
|
755
|
+
// Shared field name with differing declared types: those destructors
|
|
756
|
+
// are renamed per-constructor, and the read's own resolved type
|
|
757
|
+
// identifies the owning variant (the types differ exactly when the
|
|
758
|
+
// rename happens). Pin it so emitters use the renamed destructor.
|
|
759
|
+
const ownerTy = (v) => v.fields.find(f => f.name === e.field)?.type;
|
|
760
|
+
const differ = owners.some(v => {
|
|
761
|
+
const a = ownerTy(v), b = ownerTy(owners[0]);
|
|
762
|
+
return a && b && !tyEqual(a, b);
|
|
763
|
+
});
|
|
764
|
+
const matching = differ ? owners.filter(v => { const t = ownerTy(v); return t && tyEqual(t, e.ty); }) : [];
|
|
765
|
+
const ctor = e.ofVariant ?? (matching.length === 1 ? matching[0].name : undefined);
|
|
766
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName, ctor, datatypeField: true };
|
|
712
767
|
}
|
|
713
768
|
}
|
|
714
769
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field, datatypeField: isRecordType(e.obj.ty) };
|
|
@@ -737,13 +792,12 @@ function lowerExpr(e, binds) {
|
|
|
737
792
|
e.fn.field === "isArray" && e.args.length === 1) {
|
|
738
793
|
const arg = e.args[0];
|
|
739
794
|
if (arg.ty.kind === "user") {
|
|
740
|
-
const
|
|
741
|
-
const decl = _typeDecls.find(d => d.name === baseName);
|
|
795
|
+
const decl = declOfTy(_typeDecls, arg.ty);
|
|
742
796
|
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__") {
|
|
743
797
|
return {
|
|
744
798
|
kind: "binop", op: "=",
|
|
745
799
|
left: lowerExpr(arg, binds),
|
|
746
|
-
right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name },
|
|
800
|
+
right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name, args: [] },
|
|
747
801
|
};
|
|
748
802
|
}
|
|
749
803
|
}
|
|
@@ -787,7 +841,10 @@ function lowerExpr(e, binds) {
|
|
|
787
841
|
if (e.fn.kind === "field") {
|
|
788
842
|
const recv = lowerExpr(e.fn.obj, binds);
|
|
789
843
|
let method = e.fn.field;
|
|
790
|
-
const
|
|
844
|
+
const spec = e.builtinId !== undefined ? builtinSpec(e.builtinId) : null;
|
|
845
|
+
// Lambda-taking array builtins (registry `hof`, comparator excluded —
|
|
846
|
+
// mirrors the historical HOF_METHODS set).
|
|
847
|
+
const isHOF = spec?.hof !== undefined && spec.hof.shape !== "comparator";
|
|
791
848
|
const args = e.args.map((a, i) => {
|
|
792
849
|
const lowered = lowerExpr(a, binds);
|
|
793
850
|
// Lean: a pure fn passed to a HOF by name resolves to the monadic
|
|
@@ -796,9 +853,9 @@ function lowerExpr(e, binds) {
|
|
|
796
853
|
lowered.kind === "var" && _pureDefNames.has(lowered.name)) {
|
|
797
854
|
return { kind: "var", name: `Pure.${lowered.name}` };
|
|
798
855
|
}
|
|
799
|
-
// Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1)
|
|
800
|
-
|
|
801
|
-
|
|
856
|
+
// Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1)
|
|
857
|
+
// — registry `intArgPositions`.
|
|
858
|
+
const isArrIdxArg = spec?.intArgPositions !== undefined && spec.intArgPositions.includes(i);
|
|
802
859
|
if (isArrIdxArg && !isNat(a.ty))
|
|
803
860
|
return { kind: "toNat", expr: lowered };
|
|
804
861
|
return lowered;
|
|
@@ -846,8 +903,8 @@ function lowerExpr(e, binds) {
|
|
|
846
903
|
if (e.ty.kind === "user" && !e.spread) {
|
|
847
904
|
const tyName = e.ty.name;
|
|
848
905
|
// Match base type name (strip generic args: "Result<Model, Err>" → "Result")
|
|
849
|
-
const baseName =
|
|
850
|
-
const decl = _typeDecls
|
|
906
|
+
const baseName = tyBaseName(tyName);
|
|
907
|
+
const decl = declOfKind(_typeDecls, baseName, "discriminated-union", "string-union");
|
|
851
908
|
if (decl && decl.discriminant) {
|
|
852
909
|
const discField = e.fields.find(f => f.name === decl.discriminant);
|
|
853
910
|
if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
|
|
@@ -855,8 +912,11 @@ function lowerExpr(e, binds) {
|
|
|
855
912
|
const variant = decl.variants?.find(v => v.name === variantName);
|
|
856
913
|
if (variant) {
|
|
857
914
|
const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
|
|
858
|
-
|
|
859
|
-
|
|
915
|
+
// Bare-constructor shortcut only when the variant truly has no
|
|
916
|
+
// fields — a variant with only optional fields still needs its
|
|
917
|
+
// None-filled argument list (`int_(None)`, not `int_`).
|
|
918
|
+
if (variant.fields.length === 0) {
|
|
919
|
+
return { kind: "constructor", name: variantName, type: tyName, args: [] };
|
|
860
920
|
}
|
|
861
921
|
// Constructor with args: match variant field order. Emit a bare `app`
|
|
862
922
|
// (Dafny renders `variantName(args)`, a valid unqualified constructor
|
|
@@ -877,9 +937,9 @@ function lowerExpr(e, binds) {
|
|
|
877
937
|
if (e.spread) {
|
|
878
938
|
const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
|
|
879
939
|
const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
|
|
880
|
-
const structDecl = structName ? _typeDecls
|
|
940
|
+
const structDecl = structName ? declOfKind(_typeDecls, structName, "record") : undefined;
|
|
881
941
|
// Also check discriminated-union variants for field types
|
|
882
|
-
const unionDecl = structName ? _typeDecls
|
|
942
|
+
const unionDecl = structName ? declOfKind(_typeDecls, structName, "discriminated-union") : undefined;
|
|
883
943
|
const loweredFields = e.fields.map(f => {
|
|
884
944
|
// Propagate declared field type onto value if it has unknown type
|
|
885
945
|
let fieldValue = f.value;
|
|
@@ -932,10 +992,8 @@ function lowerExpr(e, binds) {
|
|
|
932
992
|
// Carry the resolved record type so the emitter can pick the right
|
|
933
993
|
// constructor when two datatypes share a field-name set (Event vs
|
|
934
994
|
// SparseEvent) — structural matching alone would take the first-declared.
|
|
935
|
-
const recName = e.ty.kind === "user"
|
|
936
|
-
|
|
937
|
-
: undefined;
|
|
938
|
-
const ctor = recName && _typeDecls.find(d => d.name === recName && d.kind === "record") ? recName : undefined;
|
|
995
|
+
const recName = e.ty.kind === "user" ? tyBaseName(e.ty.name) : undefined;
|
|
996
|
+
const ctor = recName && declOfKind(_typeDecls, recName, "record") ? recName : undefined;
|
|
939
997
|
return { kind: "record", spread: null, ctor, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
940
998
|
}
|
|
941
999
|
case "arrayLiteral":
|
|
@@ -985,10 +1043,10 @@ function lowerExpr(e, binds) {
|
|
|
985
1043
|
}
|
|
986
1044
|
case "optChain":
|
|
987
1045
|
// Narrow should have rewritten optChain to someMatch.
|
|
988
|
-
throw new Error(`optChain reached transform — narrow should have rewritten it`);
|
|
1046
|
+
throw new Error(`optChain reached transform — narrow should have rewritten it: ${JSON.stringify(e).slice(0, 400)}`);
|
|
989
1047
|
case "nullish":
|
|
990
1048
|
// Narrow should have rewritten nullish to someMatch.
|
|
991
|
-
throw new Error(`nullish reached transform — narrow should have rewritten it`);
|
|
1049
|
+
throw new Error(`nullish reached transform — narrow should have rewritten it: ${JSON.stringify(e).slice(0, 300)}`);
|
|
992
1050
|
case "havoc":
|
|
993
1051
|
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
994
1052
|
if (binds) {
|
|
@@ -1009,7 +1067,7 @@ function lowerExpr(e, binds) {
|
|
|
1009
1067
|
// Bare-var shortcut, but route \result through lowerExpr so the
|
|
1010
1068
|
// lemma-side replaceVar pass can substitute it with the function call.
|
|
1011
1069
|
scrutinee = path.fields.length === 0 && path.rootVar !== "\\result"
|
|
1012
|
-
? path.rootVar
|
|
1070
|
+
? varE(path.rootVar)
|
|
1013
1071
|
: lowerExpr(e.scrutinee, binds);
|
|
1014
1072
|
}
|
|
1015
1073
|
else {
|
|
@@ -1040,7 +1098,7 @@ function lowerExpr(e, binds) {
|
|
|
1040
1098
|
// Path scrutinees (e.g. `m.content`) get a synthesized hint derived
|
|
1041
1099
|
// from the last field/var name so the binder reads naturally.
|
|
1042
1100
|
const scrutinee = lowerExpr(e.scrutinee, binds);
|
|
1043
|
-
const decl = _typeDecls
|
|
1101
|
+
const decl = declOf(_typeDecls, e.typeName);
|
|
1044
1102
|
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
1045
1103
|
const varName = e.scrutinee.kind === "var" ? e.scrutinee.name : undefined;
|
|
1046
1104
|
const pathHint = varName ?? scrutineeHint(e.scrutinee);
|
|
@@ -1050,7 +1108,7 @@ function lowerExpr(e, binds) {
|
|
|
1050
1108
|
const fields = variant?.fields ?? [];
|
|
1051
1109
|
let body = lowerExpr(c.body, binds);
|
|
1052
1110
|
if (varName && fields.length > 0) {
|
|
1053
|
-
body = replaceFieldAccess(body, varName, fields);
|
|
1111
|
+
body = replaceFieldAccess(body, varName, fields, c.variant, tyBaseName(e.typeName));
|
|
1054
1112
|
if (isSynthArrayUnion && fields.length === 1) {
|
|
1055
1113
|
body = replaceVarInExpr(body, varName, matchBinder(fields[0].name, varName));
|
|
1056
1114
|
}
|
|
@@ -1071,7 +1129,7 @@ function lowerExpr(e, binds) {
|
|
|
1071
1129
|
body = wrapOptionalBranch(body, e.fallthrough);
|
|
1072
1130
|
arms.push({ pattern: pWild(), body });
|
|
1073
1131
|
}
|
|
1074
|
-
return { kind: "match", scrutinee: varName
|
|
1132
|
+
return { kind: "match", scrutinee: varName !== undefined ? varE(varName) : scrutinee, arms };
|
|
1075
1133
|
}
|
|
1076
1134
|
}
|
|
1077
1135
|
}
|
|
@@ -1100,7 +1158,7 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
1100
1158
|
if (obj.kind !== "var" || obj.ty.kind !== "user")
|
|
1101
1159
|
return null;
|
|
1102
1160
|
const typeName = obj.ty.name;
|
|
1103
|
-
const decl = typeDecls
|
|
1161
|
+
const decl = declOfKind(typeDecls, typeName, "discriminated-union");
|
|
1104
1162
|
if (!decl)
|
|
1105
1163
|
return null;
|
|
1106
1164
|
const variantName = e.left.right.value;
|
|
@@ -1111,18 +1169,26 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
1111
1169
|
const pattern = buildMatchPattern(variantName, fields, obj.name);
|
|
1112
1170
|
let rhs = transformExpr(e.right);
|
|
1113
1171
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
1114
|
-
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
|
|
1172
|
+
return { kind: "match", scrutinee: varE(obj.name), arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
|
|
1115
1173
|
}
|
|
1116
|
-
function replaceFieldAccess(e, varName, fields) {
|
|
1174
|
+
function replaceFieldAccess(e, varName, fields, ctorName, ctorOf) {
|
|
1117
1175
|
return mapExpr(e, x => {
|
|
1118
1176
|
if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
|
|
1119
1177
|
const f = fields.find(f => f.name === x.field);
|
|
1120
1178
|
if (f)
|
|
1121
1179
|
return { kind: "var", name: matchBinder(f.name, varName) };
|
|
1122
1180
|
}
|
|
1181
|
+
// Datatype update of the scrutinee (`{ ...vn, f: v }`): the arm knows the
|
|
1182
|
+
// variant, so stamp it — emitters need it for per-constructor destructor
|
|
1183
|
+
// names. Recurse manually (returning a node stops mapExpr's own descent).
|
|
1184
|
+
if (ctorName && x.kind === "record" && !x.ctor && x.spread &&
|
|
1185
|
+
x.spread.kind === "var" && x.spread.name === varName) {
|
|
1186
|
+
return { ...x, ctor: ctorName, ctorOf,
|
|
1187
|
+
fields: x.fields.map(f => ({ ...f, value: replaceFieldAccess(f.value, varName, fields, ctorName, ctorOf) })) };
|
|
1188
|
+
}
|
|
1123
1189
|
// If this let shadows the matched variable, stop replacing in the body
|
|
1124
1190
|
if (x.kind === "let" && x.name === varName)
|
|
1125
|
-
return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
|
|
1191
|
+
return { ...x, value: replaceFieldAccess(x.value, varName, fields, ctorName, ctorOf) };
|
|
1126
1192
|
return null;
|
|
1127
1193
|
});
|
|
1128
1194
|
}
|
|
@@ -1176,11 +1242,12 @@ function scrutineeHint(e) {
|
|
|
1176
1242
|
// `if (X) continue; rest` → `if (!X) { rest }` at the top of a loop body.
|
|
1177
1243
|
// Dafny's lowered while-loops have the index increment at the bottom, so a
|
|
1178
1244
|
// `continue` would skip it and loop forever; rewriting to if/else lets the
|
|
1179
|
-
// loop fall through normally.
|
|
1245
|
+
// loop fall through normally. The operand is already-lowered IR, where
|
|
1246
|
+
// negation is spelled `¬` (lowerExpr rewrites `!`).
|
|
1180
1247
|
function negateExpr(e) {
|
|
1181
|
-
if (e.kind === "unop" && e.op === "
|
|
1248
|
+
if (e.kind === "unop" && e.op === "¬")
|
|
1182
1249
|
return e.expr;
|
|
1183
|
-
return { kind: "unop", op: "
|
|
1250
|
+
return { kind: "unop", op: "¬", expr: e };
|
|
1184
1251
|
}
|
|
1185
1252
|
/** Build the two pieces of an `arr.pop()` lowering on a named array variable:
|
|
1186
1253
|
* - `optValue` is `(if |arr|>0 then Some(arr[|arr|-1]) else None)` (the popped element)
|
|
@@ -1300,13 +1367,10 @@ function matchToIfChains(stmts) {
|
|
|
1300
1367
|
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1301
1368
|
const ctorArms = arms.filter(a => a.pattern.kind !== "wild");
|
|
1302
1369
|
const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
|
|
1303
|
-
const decl = firstCtor
|
|
1304
|
-
? _typeDecls.find(d => (d.kind === "discriminated-union" || d.kind === "string-union") &&
|
|
1305
|
-
((d.variants?.some(v => v.name === firstCtor)) || (d.values?.includes(firstCtor))))
|
|
1306
|
-
: undefined;
|
|
1370
|
+
const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
|
|
1307
1371
|
if (!decl)
|
|
1308
1372
|
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1309
|
-
const scrutExpr =
|
|
1373
|
+
const scrutExpr = s.scrutinee;
|
|
1310
1374
|
const defaultArm = arms.find(a => a.pattern.kind === "wild");
|
|
1311
1375
|
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1312
1376
|
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
@@ -1325,7 +1389,7 @@ function matchToIfChains(stmts) {
|
|
|
1325
1389
|
{ pattern: pCtor(ctor), body: { kind: "bool", value: true } },
|
|
1326
1390
|
{ pattern: pWild(), body: { kind: "bool", value: false } }
|
|
1327
1391
|
] }
|
|
1328
|
-
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name } };
|
|
1392
|
+
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name, args: [] } };
|
|
1329
1393
|
// Bind only the constructor-field binders the body actually uses, pinning the
|
|
1330
1394
|
// owning ctor so the destructor doesn't guess (variants share field names).
|
|
1331
1395
|
const lets = [];
|
|
@@ -1381,6 +1445,34 @@ function requireDoneWithForBreaks(stmts, fnName) {
|
|
|
1381
1445
|
}
|
|
1382
1446
|
}
|
|
1383
1447
|
}
|
|
1448
|
+
/** The forin emission places the index increment at the loop-body end, so a
|
|
1449
|
+
* surviving `continue` would skip it. Insert the increment immediately
|
|
1450
|
+
* before every same-scope continue (nested loops own their continues),
|
|
1451
|
+
* mirroring the C-style-for desugar's discipline. */
|
|
1452
|
+
function insertIncrementBeforeContinue(stmts, idxName) {
|
|
1453
|
+
const incr = { kind: "assign", target: idxName,
|
|
1454
|
+
value: { kind: "binop", op: "+", left: { kind: "var", name: idxName }, right: { kind: "num", value: 1 } } };
|
|
1455
|
+
const walk = (ss) => {
|
|
1456
|
+
const out = [];
|
|
1457
|
+
for (const s of ss) {
|
|
1458
|
+
if (s.kind === "continue") {
|
|
1459
|
+
out.push(incr, s);
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (s.kind === "if") {
|
|
1463
|
+
out.push({ ...s, then: walk(s.then), else: walk(s.else) });
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (s.kind === "match") {
|
|
1467
|
+
out.push({ ...s, arms: s.arms.map(a => ({ ...a, body: walk(a.body) })) });
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
out.push(s);
|
|
1471
|
+
}
|
|
1472
|
+
return out;
|
|
1473
|
+
};
|
|
1474
|
+
return walk(stmts);
|
|
1475
|
+
}
|
|
1384
1476
|
function eliminateTopLevelContinue(stmts) {
|
|
1385
1477
|
const out = [];
|
|
1386
1478
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -1489,7 +1581,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
1489
1581
|
const arrSize = { kind: "field", obj: seq, field: "size" };
|
|
1490
1582
|
// Auto-add bound invariant: idx ≤ bound (always true for range loops)
|
|
1491
1583
|
const boundInv = { kind: "binop", op: "≤", left: idxVar, right: arrSize };
|
|
1492
|
-
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
1584
|
+
const bodyStmts = insertIncrementBeforeContinue(eliminateTopLevelContinue(transformStmts(s.body, typeDecls)), idxName);
|
|
1493
1585
|
result.push({
|
|
1494
1586
|
kind: "forin", idx: idxName, bound: arrSize,
|
|
1495
1587
|
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
@@ -1692,7 +1784,7 @@ function transformStmt(s, typeDecls) {
|
|
|
1692
1784
|
const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
|
|
1693
1785
|
const someBody = transformStmts(replaced, typeDecls);
|
|
1694
1786
|
const noneBody = transformStmts(s.noneBody, typeDecls);
|
|
1695
|
-
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
1787
|
+
const scrutinee = path.fields.length === 0 ? varE(path.rootVar) : transformExpr(s.scrutinee);
|
|
1696
1788
|
return [{
|
|
1697
1789
|
kind: "match", scrutinee,
|
|
1698
1790
|
arms: [
|
|
@@ -1716,13 +1808,13 @@ function mapStmtExprs(s, r) {
|
|
|
1716
1808
|
* and delegates body transformation to the caller-provided function.
|
|
1717
1809
|
* Returns null if any body transformation returns null (pure path abort). */
|
|
1718
1810
|
function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
1719
|
-
const decl = typeName ? typeDecls
|
|
1811
|
+
const decl = typeName ? declOf(typeDecls, typeName) : undefined;
|
|
1720
1812
|
const arms = [];
|
|
1721
1813
|
for (const c of cases) {
|
|
1722
1814
|
const variant = decl?.variants?.find(v => v.name === c.name);
|
|
1723
1815
|
const fields = variant?.fields ?? [];
|
|
1724
1816
|
const pattern = buildMatchPattern(c.name, fields, varName);
|
|
1725
|
-
const body = transformBody(c.body, varName, fields);
|
|
1817
|
+
const body = transformBody(c.body, varName, fields, c.name);
|
|
1726
1818
|
if (body === null)
|
|
1727
1819
|
return null;
|
|
1728
1820
|
arms.push({ pattern, body });
|
|
@@ -1730,7 +1822,7 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1730
1822
|
return arms;
|
|
1731
1823
|
}
|
|
1732
1824
|
function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
1733
|
-
const decl = typeDecls
|
|
1825
|
+
const decl = declOf(typeDecls, typeName);
|
|
1734
1826
|
// Synth array-unions (discriminant "__isArray__") have single-field variants
|
|
1735
1827
|
// ArrayBranch(arr) / NonArrayBranch(val). The matched arm refers to the
|
|
1736
1828
|
// scrutinee by its bare name/path (`content`, `m.content`), not `.arr`, so
|
|
@@ -1784,7 +1876,7 @@ function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
|
1784
1876
|
arms.push({ pattern: pWild(), body: transformStmts(fallthrough, typeDecls) });
|
|
1785
1877
|
}
|
|
1786
1878
|
}
|
|
1787
|
-
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : prefix, arms };
|
|
1879
|
+
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : varE(prefix), arms };
|
|
1788
1880
|
}
|
|
1789
1881
|
/** Replace bare `var(oldName)` references → `var(newName)` with the given type.
|
|
1790
1882
|
* Used by emitMatchStmt for synth array-unions where the variant has a single
|
|
@@ -1799,7 +1891,7 @@ function replaceVarInTStmts(stmts, oldName, newName, newTy) {
|
|
|
1799
1891
|
}
|
|
1800
1892
|
/** If the chain has matched all variants but one, return that remaining variant. */
|
|
1801
1893
|
function remainingVariant(typeName, cases, typeDecls) {
|
|
1802
|
-
const decl = typeDecls
|
|
1894
|
+
const decl = declOf(typeDecls, typeName);
|
|
1803
1895
|
if (!decl?.variants)
|
|
1804
1896
|
return null;
|
|
1805
1897
|
const matched = new Set(cases.map(c => c.variant));
|
|
@@ -1818,10 +1910,7 @@ function remainingVariant(typeName, cases, typeDecls) {
|
|
|
1818
1910
|
function enumFieldSwitch(s, typeDecls) {
|
|
1819
1911
|
if (!s.discriminant)
|
|
1820
1912
|
return null;
|
|
1821
|
-
const
|
|
1822
|
-
? (s.expr.ty.name.includes("<") ? s.expr.ty.name.slice(0, s.expr.ty.name.indexOf("<")) : s.expr.ty.name)
|
|
1823
|
-
: undefined;
|
|
1824
|
-
const objDecl = objBase ? typeDecls.find(d => d.name === objBase) : undefined;
|
|
1913
|
+
const objDecl = declOfTy(typeDecls, s.expr.ty);
|
|
1825
1914
|
if (objDecl?.kind === "discriminated-union" && objDecl.discriminant === s.discriminant)
|
|
1826
1915
|
return null;
|
|
1827
1916
|
const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
|
|
@@ -1830,15 +1919,36 @@ function enumFieldSwitch(s, typeDecls) {
|
|
|
1830
1919
|
enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
|
|
1831
1920
|
};
|
|
1832
1921
|
}
|
|
1922
|
+
/** Stamp variant ctor info onto datatype updates of the match scrutinee in
|
|
1923
|
+
* lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
|
|
1924
|
+
* `replaceFieldAccess`'s stamping. Emitters need the pin to use
|
|
1925
|
+
* per-constructor destructor names for collision-renamed fields. */
|
|
1926
|
+
function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
|
|
1927
|
+
const stamp = (x) => {
|
|
1928
|
+
if (x.kind === "record" && !x.ctor && x.spread &&
|
|
1929
|
+
x.spread.kind === "var" && x.spread.name === varName) {
|
|
1930
|
+
return { ...x, ctor: ctorName, ctorOf,
|
|
1931
|
+
fields: x.fields.map(f => ({ ...f, value: mapExpr(f.value, stamp) })) };
|
|
1932
|
+
}
|
|
1933
|
+
return null;
|
|
1934
|
+
};
|
|
1935
|
+
return body.map(st => mapStmt(st, stamp));
|
|
1936
|
+
}
|
|
1833
1937
|
function emitSwitchStmt(s, typeDecls) {
|
|
1834
1938
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1835
1939
|
const ef = enumFieldSwitch(s, typeDecls);
|
|
1940
|
+
const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
|
|
1836
1941
|
const arms = ef
|
|
1837
1942
|
? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
|
|
1838
|
-
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields) =>
|
|
1943
|
+
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields, ctorName) => {
|
|
1944
|
+
let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
|
|
1945
|
+
if (ctorName && vn && baseName)
|
|
1946
|
+
out = stampScrutineeUpdates(out, vn, ctorName, baseName);
|
|
1947
|
+
return out;
|
|
1948
|
+
});
|
|
1839
1949
|
if (s.defaultBody.length > 0)
|
|
1840
1950
|
arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
|
|
1841
|
-
return { kind: "match", scrutinee: ef ? ef.scrutinee : (s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
1951
|
+
return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
1842
1952
|
}
|
|
1843
1953
|
/** Replace obj.field → replacement var in typed IR.
|
|
1844
1954
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
@@ -1965,7 +2075,7 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1965
2075
|
const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
|
|
1966
2076
|
if (!noneExpr)
|
|
1967
2077
|
return null;
|
|
1968
|
-
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
2078
|
+
const scrutinee = path.fields.length === 0 ? varE(path.rootVar) : transformExpr(s.scrutinee);
|
|
1969
2079
|
return {
|
|
1970
2080
|
kind: "match", scrutinee,
|
|
1971
2081
|
arms: [
|
|
@@ -1997,16 +2107,16 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1997
2107
|
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
1998
2108
|
}
|
|
1999
2109
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
|
|
2000
|
-
if (!typeDecls
|
|
2110
|
+
if (!declOf(typeDecls, typeName))
|
|
2001
2111
|
return null;
|
|
2002
2112
|
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
2003
2113
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2004
|
-
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
|
|
2114
|
+
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2005
2115
|
let result = transformPureBody(body, typeDecls);
|
|
2006
2116
|
if (!result)
|
|
2007
2117
|
return null;
|
|
2008
2118
|
if (fields.length > 0 && vn)
|
|
2009
|
-
result = replaceFieldAccess(result, vn, fields);
|
|
2119
|
+
result = replaceFieldAccess(result, vn, fields, ctorName, tyBaseName(typeName));
|
|
2010
2120
|
return result;
|
|
2011
2121
|
});
|
|
2012
2122
|
if (!arms)
|
|
@@ -2019,21 +2129,21 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2019
2129
|
}
|
|
2020
2130
|
if (s.expr.kind !== "var")
|
|
2021
2131
|
return null;
|
|
2022
|
-
return { kind: "match", scrutinee: s.expr.name, arms };
|
|
2132
|
+
return { kind: "match", scrutinee: varE(s.expr.name), arms };
|
|
2023
2133
|
}
|
|
2024
2134
|
function transformPureMatch(chain, typeDecls) {
|
|
2025
2135
|
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
2026
|
-
const decl = typeDecls
|
|
2136
|
+
const decl = declOf(typeDecls, chain.typeName);
|
|
2027
2137
|
// Synth array-unions have single-field variants and user code refers to the
|
|
2028
2138
|
// scrutinee by its bare name, not field-accessed. See emitMatchStmt for
|
|
2029
2139
|
// the statement-level counterpart of this substitution.
|
|
2030
2140
|
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
2031
|
-
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
|
|
2141
|
+
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2032
2142
|
let result = transformPureBody(body, typeDecls);
|
|
2033
2143
|
if (!result)
|
|
2034
2144
|
return null;
|
|
2035
2145
|
if (fields.length > 0 && vn)
|
|
2036
|
-
result = replaceFieldAccess(result, vn, fields);
|
|
2146
|
+
result = replaceFieldAccess(result, vn, fields, ctorName, tyBaseName(chain.typeName));
|
|
2037
2147
|
if (isSynthArrayUnion && fields.length === 1 && vn) {
|
|
2038
2148
|
result = replaceVarInExpr(result, vn, matchBinder(fields[0].name, vn));
|
|
2039
2149
|
}
|
|
@@ -2053,7 +2163,7 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2053
2163
|
if (!body)
|
|
2054
2164
|
return null;
|
|
2055
2165
|
if (remaining.fields.length > 0)
|
|
2056
|
-
body = replaceFieldAccess(body, chain.varName, remaining.fields);
|
|
2166
|
+
body = replaceFieldAccess(body, chain.varName, remaining.fields, remaining.name, tyBaseName(chain.typeName));
|
|
2057
2167
|
if (isSynthArrayUnion && remaining.fields.length === 1) {
|
|
2058
2168
|
body = replaceVarInExpr(body, chain.varName, matchBinder(remaining.fields[0].name, chain.varName));
|
|
2059
2169
|
}
|
|
@@ -2066,7 +2176,7 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2066
2176
|
arms.push({ pattern: pWild(), body });
|
|
2067
2177
|
}
|
|
2068
2178
|
}
|
|
2069
|
-
return { kind: "match", scrutinee: chain.varName, arms };
|
|
2179
|
+
return { kind: "match", scrutinee: varE(chain.varName), arms };
|
|
2070
2180
|
}
|
|
2071
2181
|
// ── Generate type declarations ───────────────────────────────
|
|
2072
2182
|
function transformTypeDecl(d) {
|
|
@@ -2203,6 +2313,7 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2203
2313
|
_forofCounters.clear();
|
|
2204
2314
|
_liftCounter = 0;
|
|
2205
2315
|
_typeDecls = mod.typeDecls;
|
|
2316
|
+
_neededKindHelpers = new Map();
|
|
2206
2317
|
_pureDefNames = new Set(mod.functions.filter(f => f.isPure).map(f => f.name));
|
|
2207
2318
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
2208
2319
|
// Module-level constants
|
|
@@ -2272,27 +2383,6 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2272
2383
|
ensures: ext.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall)),
|
|
2273
2384
|
};
|
|
2274
2385
|
});
|
|
2275
|
-
// Types file
|
|
2276
|
-
const typesImports = ["LemmaScript"];
|
|
2277
|
-
let typesFile = null;
|
|
2278
|
-
const pureNamespace = pureDefs.length > 0
|
|
2279
|
-
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
2280
|
-
: [];
|
|
2281
|
-
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0) {
|
|
2282
|
-
// Declaration order differs by backend. Dafny allows forward references, so
|
|
2283
|
-
// externs go first to be in scope everywhere. Lean requires definition-before-use:
|
|
2284
|
-
// an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
|
|
2285
|
-
// so types must precede externs, which in turn precede the pure mirrors that may call them.
|
|
2286
|
-
const decls = _opts.backend === "lean"
|
|
2287
|
-
? [...typeDecls, ...externDecls, ...pureNamespace]
|
|
2288
|
-
: [...externDecls, ...typeDecls, ...pureNamespace];
|
|
2289
|
-
typesFile = {
|
|
2290
|
-
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
2291
|
-
imports: typesImports,
|
|
2292
|
-
options: [],
|
|
2293
|
-
decls,
|
|
2294
|
-
};
|
|
2295
|
-
}
|
|
2296
2386
|
// Def file: Velvet methods
|
|
2297
2387
|
// Pure functions get a thin wrapper that calls Pure.fnName
|
|
2298
2388
|
// def-by-method functions also skip their method wrappers
|
|
@@ -2369,6 +2459,106 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2369
2459
|
methods: classMethods,
|
|
2370
2460
|
};
|
|
2371
2461
|
});
|
|
2462
|
+
// Types file — assembled after all body transforms, so needs discovered
|
|
2463
|
+
// there (discriminator kind-helpers) are included.
|
|
2464
|
+
const kindHelpers = [..._neededKindHelpers.values()].map(kindHelperDecl);
|
|
2465
|
+
// ── Imported / undeclared user types: opaque by default ─────────────
|
|
2466
|
+
// A module may reference types it imports (ir.ts uses typedir's `Ty`).
|
|
2467
|
+
// Standalone compilation has no declaration for them; synthesize an
|
|
2468
|
+
// opaque type — the value passes through, uninspectable, which is the
|
|
2469
|
+
// only sound use of an undeclared type (same doctrine as _synthOpaque).
|
|
2470
|
+
// Any attempted inspection still fails loudly: an opaque type has no
|
|
2471
|
+
// constructors and no operations. Signature-level coverage (type-decl
|
|
2472
|
+
// fields, params, returns, class fields, externs, consts); a body-level
|
|
2473
|
+
// reference to an undeclared type still errors in the backend.
|
|
2474
|
+
const referenced = new Set();
|
|
2475
|
+
const collectTy = (ty) => {
|
|
2476
|
+
switch (ty.kind) {
|
|
2477
|
+
case "user":
|
|
2478
|
+
referenced.add(tyBaseName(ty.name));
|
|
2479
|
+
return;
|
|
2480
|
+
case "array":
|
|
2481
|
+
case "set":
|
|
2482
|
+
collectTy(ty.elem);
|
|
2483
|
+
return;
|
|
2484
|
+
case "optional":
|
|
2485
|
+
collectTy(ty.inner);
|
|
2486
|
+
return;
|
|
2487
|
+
case "map":
|
|
2488
|
+
collectTy(ty.key);
|
|
2489
|
+
collectTy(ty.value);
|
|
2490
|
+
return;
|
|
2491
|
+
case "tuple":
|
|
2492
|
+
ty.elems.forEach(collectTy);
|
|
2493
|
+
return;
|
|
2494
|
+
case "fn":
|
|
2495
|
+
ty.params.forEach(collectTy);
|
|
2496
|
+
collectTy(ty.result);
|
|
2497
|
+
return;
|
|
2498
|
+
default: return;
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
const knownTypeNames = new Set(typeDecls.map(d => d.name));
|
|
2502
|
+
const allTypeParams = new Set();
|
|
2503
|
+
// Exclude type params from the *source* decls — the transformed IR drops
|
|
2504
|
+
// them for aliases (`type Step<S, A> = …`), and a generic alias's params
|
|
2505
|
+
// must not be mistaken for imported types. Params may carry a `//@ type`
|
|
2506
|
+
// decoration ("S(==)"); references collect as the bare name, so strip it.
|
|
2507
|
+
const addTp = (tp) => { allTypeParams.add(tp.replace(/\(.*$/, "").trim()); };
|
|
2508
|
+
for (const d of mod.typeDecls)
|
|
2509
|
+
d.typeParams?.forEach(addTp);
|
|
2510
|
+
for (const d of typeDecls) {
|
|
2511
|
+
if (d.kind === "inductive")
|
|
2512
|
+
d.constructors.forEach(c => c.fields.forEach(f => collectTy(f.type)));
|
|
2513
|
+
else if (d.kind === "structure")
|
|
2514
|
+
d.fields.forEach(f => collectTy(f.type));
|
|
2515
|
+
else if (d.kind === "type-alias")
|
|
2516
|
+
collectTy(d.target);
|
|
2517
|
+
}
|
|
2518
|
+
for (const fn of mod.functions) {
|
|
2519
|
+
fn.typeParams.forEach(addTp);
|
|
2520
|
+
fn.params.forEach(p => collectTy(p.ty));
|
|
2521
|
+
collectTy(fn.returnTy);
|
|
2522
|
+
}
|
|
2523
|
+
for (const cls of mod.classes ?? []) {
|
|
2524
|
+
cls.fields.forEach(f => collectTy(f.ty));
|
|
2525
|
+
for (const m of cls.methods) {
|
|
2526
|
+
m.typeParams.forEach(addTp);
|
|
2527
|
+
m.params.forEach(p => collectTy(p.ty));
|
|
2528
|
+
collectTy(m.returnTy);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
for (const ext of mod.externs ?? []) {
|
|
2532
|
+
ext.typeParams.forEach(addTp);
|
|
2533
|
+
ext.params.forEach(p => collectTy(p.ty));
|
|
2534
|
+
collectTy(ext.returnTy);
|
|
2535
|
+
}
|
|
2536
|
+
for (const c of mod.constants ?? [])
|
|
2537
|
+
collectTy(c.ty);
|
|
2538
|
+
const opaqueImports = [...referenced]
|
|
2539
|
+
.filter(n => !knownTypeNames.has(n) && !allTypeParams.has(n))
|
|
2540
|
+
.map(n => ({ kind: "opaque-type", name: n }));
|
|
2541
|
+
const typesImports = ["LemmaScript"];
|
|
2542
|
+
let typesFile = null;
|
|
2543
|
+
const pureNamespace = pureDefs.length > 0
|
|
2544
|
+
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
2545
|
+
: [];
|
|
2546
|
+
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0 || opaqueImports.length > 0) {
|
|
2547
|
+
// Declaration order differs by backend. Dafny allows forward references, so
|
|
2548
|
+
// externs go first to be in scope everywhere. Lean requires definition-before-use:
|
|
2549
|
+
// an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
|
|
2550
|
+
// so types must precede externs, which in turn precede the pure mirrors that may call them.
|
|
2551
|
+
// Kind-helpers sit right after the datatypes they discriminate.
|
|
2552
|
+
const decls = _opts.backend === "lean"
|
|
2553
|
+
? [...opaqueImports, ...typeDecls, ...kindHelpers, ...externDecls, ...pureNamespace]
|
|
2554
|
+
: [...externDecls, ...opaqueImports, ...typeDecls, ...kindHelpers, ...pureNamespace];
|
|
2555
|
+
typesFile = {
|
|
2556
|
+
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
2557
|
+
imports: typesImports,
|
|
2558
|
+
options: [],
|
|
2559
|
+
decls,
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2372
2562
|
const defImport = specImport ?? (typesFile ? `«${moduleBase}.types»` : null);
|
|
2373
2563
|
const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
|
|
2374
2564
|
const defFile = {
|