lemmascript 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +125 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +228 -37
- package/tools/dist/extract.js +175 -37
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +29 -18
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +296 -677
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +268 -249
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +411 -131
- package/tools/dist/typedecls.js +59 -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
|
|
@@ -202,7 +234,10 @@ const HOF_METHODS = new Set(["map", "filter", "every", "some", "find", "findLast
|
|
|
202
234
|
* arm body would still be captured, so prime on any module-wide collision.
|
|
203
235
|
* Deterministic, so the pattern binder and its body substitutions agree. */
|
|
204
236
|
function matchBinder(fieldName, prefix) {
|
|
205
|
-
|
|
237
|
+
const safePrefix = prefix === "\\result"
|
|
238
|
+
? "result"
|
|
239
|
+
: prefix?.replace(/[^A-Za-z0-9_]/g, "_");
|
|
240
|
+
return freshName(safePrefix ? `_${safePrefix}_${fieldName}` : `_${fieldName}`);
|
|
206
241
|
}
|
|
207
242
|
/** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
|
|
208
243
|
function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
@@ -214,17 +249,16 @@ function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
|
|
|
214
249
|
function isArray(ty) { return ty.kind === "array"; }
|
|
215
250
|
function isUser(ty) { return ty.kind === "user"; }
|
|
216
251
|
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";
|
|
252
|
+
return declOfTy(_typeDecls, ty)?.kind === "record";
|
|
221
253
|
}
|
|
222
254
|
/** Truthiness test for a *lowered* value of source type `ty`, used by `||`
|
|
223
|
-
* falsiness lowering.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
255
|
+
* falsiness lowering. Which types need one is `isFalsyCapableTy` (shared with
|
|
256
|
+
* condition-facts' falsy gate); this adds the per-type test. Returns null for
|
|
257
|
+
* the always-truthy types so callers can unwrap directly instead of emitting
|
|
258
|
+
* a redundant guard. */
|
|
227
259
|
function valueTruthyCond(value, ty) {
|
|
260
|
+
if (!isFalsyCapableTy(ty))
|
|
261
|
+
return null;
|
|
228
262
|
switch (ty.kind) {
|
|
229
263
|
case "int":
|
|
230
264
|
case "nat":
|
|
@@ -327,20 +361,33 @@ function flattenLambdaBody(stmts) {
|
|
|
327
361
|
* field, index, record, forall, or exists sub-expressions.
|
|
328
362
|
*/
|
|
329
363
|
/** JS truthiness coercion for `if`/`while`/`?:` conditions.
|
|
330
|
-
* Dafny requires bool; coerce number
|
|
364
|
+
* Dafny requires bool; coerce number→`!== 0`, string→non-empty, array→`true`
|
|
331
365
|
* (every array, even `[]`, is truthy in JS).
|
|
332
|
-
*
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
366
|
+
*
|
|
367
|
+
* Rewrites the typed tree, before lowering, and distributes over `&&`/`||`:
|
|
368
|
+
* each operand of a logical connective is itself in condition position, and the
|
|
369
|
+
* operands need not share a type — `i >= 0 && carry`, with `carry` an int, is a
|
|
370
|
+
* bool conjoined with a number. A conjunction takes its type from its right
|
|
371
|
+
* operand (resolve), so coercing the whole expression by its type would emit
|
|
372
|
+
* `((i >= 0) && carry) != 0`, which is not well-typed.
|
|
373
|
+
*
|
|
374
|
+
* Optional conds never arrive here — narrow.ts rewrites them to someMatch. */
|
|
375
|
+
function asCondition(e) {
|
|
376
|
+
const bool = { kind: "bool" };
|
|
377
|
+
if (e.ty.kind === "bool")
|
|
378
|
+
return e;
|
|
379
|
+
if (e.kind === "binop" && (e.op === "&&" || e.op === "||"))
|
|
380
|
+
return { ...e, left: asCondition(e.left), right: asCondition(e.right), ty: bool };
|
|
381
|
+
if (e.ty.kind === "int" || e.ty.kind === "nat")
|
|
382
|
+
return { kind: "binop", op: "!==", left: e, right: { kind: "num", value: 0, ty: e.ty }, ty: bool };
|
|
383
|
+
if (e.ty.kind === "string")
|
|
384
|
+
return { kind: "binop", op: ">",
|
|
385
|
+
left: { kind: "field", obj: e, field: "length", ty: { kind: "nat" } },
|
|
386
|
+
right: { kind: "num", value: 0, ty: { kind: "nat" } }, ty: bool };
|
|
340
387
|
// Arrays, objects, maps, sets, tuples are always truthy in JS (even `[]`/`{}`).
|
|
341
|
-
if (["array", "user", "map", "set", "tuple"].includes(ty.kind))
|
|
342
|
-
return { kind: "bool", value: true };
|
|
343
|
-
return
|
|
388
|
+
if (["array", "user", "map", "set", "tuple"].includes(e.ty.kind))
|
|
389
|
+
return { kind: "bool", value: true, ty: bool };
|
|
390
|
+
return e;
|
|
344
391
|
}
|
|
345
392
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
346
393
|
* If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
|
|
@@ -350,7 +397,7 @@ function wrapOptionalBranch(expr, raw) {
|
|
|
350
397
|
// like the scrutinee of an outer match. Dafny treats `Option.Some` and bare
|
|
351
398
|
// `Some` equivalently — the qualification is harmless there.
|
|
352
399
|
if (raw.kind === "var" && raw.name === "undefined")
|
|
353
|
-
return { kind: "constructor", name: "none", type: "Option" };
|
|
400
|
+
return { kind: "constructor", name: "none", type: "Option", args: [] };
|
|
354
401
|
if (raw.ty.kind === "optional")
|
|
355
402
|
return expr; // already Option<T>, don't double-wrap
|
|
356
403
|
return { kind: "constructor", name: "some", type: "Option", args: [expr] };
|
|
@@ -419,12 +466,16 @@ function lowerExpr(e, binds) {
|
|
|
419
466
|
switch (e.kind) {
|
|
420
467
|
case "var": return { kind: "var", name: e.name };
|
|
421
468
|
case "num": return { kind: "num", value: e.value };
|
|
469
|
+
case "bigint": return { kind: "bigint", value: e.value };
|
|
422
470
|
case "bool": return { kind: "bool", value: e.value };
|
|
423
471
|
case "str":
|
|
424
472
|
if (e.ty.kind === "user")
|
|
425
|
-
return { kind: "constructor", name: e.value, type: e.ty.name };
|
|
473
|
+
return { kind: "constructor", name: e.value, type: e.ty.name, args: [] };
|
|
426
474
|
return { kind: "str", value: e.value };
|
|
427
475
|
case "unop":
|
|
476
|
+
// Only `num` folds. A `bigint` payload is a string, so negating it here
|
|
477
|
+
// would coerce through `Number` and round: `-9007199254740993n` stays a
|
|
478
|
+
// structural `unop("-", bigint(...))` and is negated by the emitter.
|
|
428
479
|
if (e.op === "-" && e.expr.kind === "num")
|
|
429
480
|
return { kind: "num", value: -e.expr.value };
|
|
430
481
|
// String truthiness: !str → str == ""
|
|
@@ -469,7 +520,7 @@ function lowerExpr(e, binds) {
|
|
|
469
520
|
kind: "binop",
|
|
470
521
|
op: e.op === "===" ? "=" : "≠",
|
|
471
522
|
left: transformExpr(e.left.obj),
|
|
472
|
-
right: { kind: "constructor", name: e.right.value, type: objTy },
|
|
523
|
+
right: { kind: "constructor", name: e.right.value, type: objTy, args: [] },
|
|
473
524
|
};
|
|
474
525
|
}
|
|
475
526
|
// String literal comparison — constructor if user type, string literal if string.
|
|
@@ -479,7 +530,7 @@ function lowerExpr(e, binds) {
|
|
|
479
530
|
const left = lowerExpr(e.left, binds);
|
|
480
531
|
const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
|
|
481
532
|
const right = isUser(e.left.ty)
|
|
482
|
-
? { kind: "constructor", name: e.right.value, type: leftTy }
|
|
533
|
+
? { kind: "constructor", name: e.right.value, type: leftTy, args: [] }
|
|
483
534
|
: { kind: "str", value: e.right.value };
|
|
484
535
|
return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
|
|
485
536
|
}
|
|
@@ -504,7 +555,7 @@ function lowerExpr(e, binds) {
|
|
|
504
555
|
// non-optional string-literal rule above.
|
|
505
556
|
const innerTy = optSide.ty.kind === "optional" ? optSide.ty.inner : optSide.ty;
|
|
506
557
|
const valExpr = valSide.kind === "str" && innerTy.kind === "user"
|
|
507
|
-
? { kind: "constructor", name: valSide.value, type: innerTy.name }
|
|
558
|
+
? { kind: "constructor", name: valSide.value, type: innerTy.name, args: [] }
|
|
508
559
|
: lowerExpr(valSide, binds);
|
|
509
560
|
const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
|
|
510
561
|
const noneVal = e.op === "!==" ? true : false;
|
|
@@ -543,7 +594,7 @@ function lowerExpr(e, binds) {
|
|
|
543
594
|
// || on optional → match Some/None with default. JS `||` tests falsiness of
|
|
544
595
|
// the *unwrapped* value, so when the inner type can be falsy the Some arm must
|
|
545
596
|
// re-test (`Some(0) || 1 === 1`); array/user inners are always truthy and
|
|
546
|
-
// unwrap directly.
|
|
597
|
+
// unwrap directly. Same gate as condition-facts' `canBeFalsy`.
|
|
547
598
|
if (e.op === "||" && e.left.ty.kind === "optional") {
|
|
548
599
|
const optExpr = lowerExpr(e.left, binds);
|
|
549
600
|
const defaultExpr = lowerExpr(e.right, binds);
|
|
@@ -694,21 +745,41 @@ function lowerExpr(e, binds) {
|
|
|
694
745
|
// as bare truthiness checks. Emit as the Dafny discriminator predicate for the
|
|
695
746
|
// 'true' variant: result.ok → result.true_?
|
|
696
747
|
if (e.isDiscriminant && e.obj.ty.kind === "user") {
|
|
697
|
-
const
|
|
698
|
-
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
748
|
+
const decl = unionDeclOfTy(_typeDecls, e.obj.ty);
|
|
699
749
|
if (decl?.variants?.some(v => v.name === "true")) {
|
|
700
750
|
return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
|
|
701
751
|
}
|
|
752
|
+
// Surviving string-discriminant read (`x.kind` used as a value —
|
|
753
|
+
// compared against another union's tag, passed as an argument, …):
|
|
754
|
+
// lower to the generated per-union discriminator function, which
|
|
755
|
+
// returns the source tag strings. Narrowing consumes discriminant
|
|
756
|
+
// *checks*; this catches reads that survive as values.
|
|
757
|
+
if (decl?.variants && decl.discriminant && decl.discriminant !== "__isArray__") {
|
|
758
|
+
_neededKindHelpers.set(decl.name, decl);
|
|
759
|
+
return { kind: "app", fn: kindHelperName(decl), args: [lowerExpr(e.obj, binds)] };
|
|
760
|
+
}
|
|
702
761
|
}
|
|
703
762
|
// Union destructor: `x.field` where x is a discriminated union and `field`
|
|
704
763
|
// is a data field of one of its variants. Dafny reads the destructor
|
|
705
764
|
// directly; Lean has no field projection on a multi-ctor inductive, so tag
|
|
706
765
|
// the node with the union's base name and let the Lean emitter `match`.
|
|
707
766
|
if (e.obj.ty.kind === "user") {
|
|
708
|
-
const baseName =
|
|
709
|
-
const decl = _typeDecls
|
|
710
|
-
|
|
711
|
-
|
|
767
|
+
const baseName = tyBaseName(e.obj.ty.name);
|
|
768
|
+
const decl = declOfKind(_typeDecls, baseName, "discriminated-union");
|
|
769
|
+
const owners = decl?.variants?.filter(v => v.fields.some(f => f.name === e.field)) ?? [];
|
|
770
|
+
if (owners.length > 0) {
|
|
771
|
+
// Shared field name with differing declared types: those destructors
|
|
772
|
+
// are renamed per-constructor, and the read's own resolved type
|
|
773
|
+
// identifies the owning variant (the types differ exactly when the
|
|
774
|
+
// rename happens). Pin it so emitters use the renamed destructor.
|
|
775
|
+
const ownerTy = (v) => v.fields.find(f => f.name === e.field)?.type;
|
|
776
|
+
const differ = owners.some(v => {
|
|
777
|
+
const a = ownerTy(v), b = ownerTy(owners[0]);
|
|
778
|
+
return a && b && !tyEqual(a, b);
|
|
779
|
+
});
|
|
780
|
+
const matching = differ ? owners.filter(v => { const t = ownerTy(v); return t && tyEqual(t, e.ty); }) : [];
|
|
781
|
+
const ctor = e.ofVariant ?? (matching.length === 1 ? matching[0].name : undefined);
|
|
782
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName, ctor, datatypeField: true };
|
|
712
783
|
}
|
|
713
784
|
}
|
|
714
785
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field, datatypeField: isRecordType(e.obj.ty) };
|
|
@@ -737,17 +808,22 @@ function lowerExpr(e, binds) {
|
|
|
737
808
|
e.fn.field === "isArray" && e.args.length === 1) {
|
|
738
809
|
const arg = e.args[0];
|
|
739
810
|
if (arg.ty.kind === "user") {
|
|
740
|
-
const
|
|
741
|
-
const decl = _typeDecls.find(d => d.name === baseName);
|
|
811
|
+
const decl = declOfTy(_typeDecls, arg.ty);
|
|
742
812
|
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__") {
|
|
743
813
|
return {
|
|
744
814
|
kind: "binop", op: "=",
|
|
745
815
|
left: lowerExpr(arg, binds),
|
|
746
|
-
right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name },
|
|
816
|
+
right: { kind: "constructor", name: "ArrayBranch", type: arg.ty.name, args: [] },
|
|
747
817
|
};
|
|
748
818
|
}
|
|
749
819
|
}
|
|
750
820
|
}
|
|
821
|
+
// String.fromCharCode(n) → preamble function (inverse of charCodeAt,
|
|
822
|
+
// which lowers to `(s[i] as int)`).
|
|
823
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "String" &&
|
|
824
|
+
e.fn.field === "fromCharCode" && e.args.length === 1) {
|
|
825
|
+
return { kind: "app", fn: "StringFromCharCode", args: [lowerExpr(e.args[0], binds)] };
|
|
826
|
+
}
|
|
751
827
|
// Math.abs/min/max → preamble functions
|
|
752
828
|
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
|
|
753
829
|
if (e.fn.field === "abs" && e.args.length === 1)
|
|
@@ -787,7 +863,10 @@ function lowerExpr(e, binds) {
|
|
|
787
863
|
if (e.fn.kind === "field") {
|
|
788
864
|
const recv = lowerExpr(e.fn.obj, binds);
|
|
789
865
|
let method = e.fn.field;
|
|
790
|
-
const
|
|
866
|
+
const spec = e.builtinId !== undefined ? builtinSpec(e.builtinId) : null;
|
|
867
|
+
// Lambda-taking array builtins (registry `hof`, comparator excluded —
|
|
868
|
+
// mirrors the historical HOF_METHODS set).
|
|
869
|
+
const isHOF = spec?.hof !== undefined && spec.hof.shape !== "comparator";
|
|
791
870
|
const args = e.args.map((a, i) => {
|
|
792
871
|
const lowered = lowerExpr(a, binds);
|
|
793
872
|
// Lean: a pure fn passed to a HOF by name resolves to the monadic
|
|
@@ -796,9 +875,9 @@ function lowerExpr(e, binds) {
|
|
|
796
875
|
lowered.kind === "var" && _pureDefNames.has(lowered.name)) {
|
|
797
876
|
return { kind: "var", name: `Pure.${lowered.name}` };
|
|
798
877
|
}
|
|
799
|
-
// Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1)
|
|
800
|
-
|
|
801
|
-
|
|
878
|
+
// Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1)
|
|
879
|
+
// — registry `intArgPositions`.
|
|
880
|
+
const isArrIdxArg = spec?.intArgPositions !== undefined && spec.intArgPositions.includes(i);
|
|
802
881
|
if (isArrIdxArg && !isNat(a.ty))
|
|
803
882
|
return { kind: "toNat", expr: lowered };
|
|
804
883
|
return lowered;
|
|
@@ -846,8 +925,8 @@ function lowerExpr(e, binds) {
|
|
|
846
925
|
if (e.ty.kind === "user" && !e.spread) {
|
|
847
926
|
const tyName = e.ty.name;
|
|
848
927
|
// Match base type name (strip generic args: "Result<Model, Err>" → "Result")
|
|
849
|
-
const baseName =
|
|
850
|
-
const decl = _typeDecls
|
|
928
|
+
const baseName = tyBaseName(tyName);
|
|
929
|
+
const decl = declOfKind(_typeDecls, baseName, "discriminated-union", "string-union");
|
|
851
930
|
if (decl && decl.discriminant) {
|
|
852
931
|
const discField = e.fields.find(f => f.name === decl.discriminant);
|
|
853
932
|
if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
|
|
@@ -855,8 +934,11 @@ function lowerExpr(e, binds) {
|
|
|
855
934
|
const variant = decl.variants?.find(v => v.name === variantName);
|
|
856
935
|
if (variant) {
|
|
857
936
|
const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
|
|
858
|
-
|
|
859
|
-
|
|
937
|
+
// Bare-constructor shortcut only when the variant truly has no
|
|
938
|
+
// fields — a variant with only optional fields still needs its
|
|
939
|
+
// None-filled argument list (`int_(None)`, not `int_`).
|
|
940
|
+
if (variant.fields.length === 0) {
|
|
941
|
+
return { kind: "constructor", name: variantName, type: tyName, args: [] };
|
|
860
942
|
}
|
|
861
943
|
// Constructor with args: match variant field order. Emit a bare `app`
|
|
862
944
|
// (Dafny renders `variantName(args)`, a valid unqualified constructor
|
|
@@ -877,9 +959,9 @@ function lowerExpr(e, binds) {
|
|
|
877
959
|
if (e.spread) {
|
|
878
960
|
const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
|
|
879
961
|
const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
|
|
880
|
-
const structDecl = structName ? _typeDecls
|
|
962
|
+
const structDecl = structName ? declOfKind(_typeDecls, structName, "record") : undefined;
|
|
881
963
|
// Also check discriminated-union variants for field types
|
|
882
|
-
const unionDecl = structName ? _typeDecls
|
|
964
|
+
const unionDecl = structName ? declOfKind(_typeDecls, structName, "discriminated-union") : undefined;
|
|
883
965
|
const loweredFields = e.fields.map(f => {
|
|
884
966
|
// Propagate declared field type onto value if it has unknown type
|
|
885
967
|
let fieldValue = f.value;
|
|
@@ -932,10 +1014,8 @@ function lowerExpr(e, binds) {
|
|
|
932
1014
|
// Carry the resolved record type so the emitter can pick the right
|
|
933
1015
|
// constructor when two datatypes share a field-name set (Event vs
|
|
934
1016
|
// 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;
|
|
1017
|
+
const recName = e.ty.kind === "user" ? tyBaseName(e.ty.name) : undefined;
|
|
1018
|
+
const ctor = recName && declOfKind(_typeDecls, recName, "record") ? recName : undefined;
|
|
939
1019
|
return { kind: "record", spread: null, ctor, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
940
1020
|
}
|
|
941
1021
|
case "arrayLiteral":
|
|
@@ -974,7 +1054,7 @@ function lowerExpr(e, binds) {
|
|
|
974
1054
|
// JS truthiness coercion (string/array/int → ... > 0). Matches SPEC §3.1
|
|
975
1055
|
// negation forms (`!s` → `s == ""`). Optional conds are already
|
|
976
1056
|
// rewritten to someMatch by narrow.ts.
|
|
977
|
-
const cond =
|
|
1057
|
+
const cond = lowerExpr(asCondition(e.cond), binds);
|
|
978
1058
|
let thenExpr = lowerExpr(e.then, binds);
|
|
979
1059
|
let elseExpr = lowerExpr(e.else, binds);
|
|
980
1060
|
if (e.ty.kind === "optional") {
|
|
@@ -985,10 +1065,10 @@ function lowerExpr(e, binds) {
|
|
|
985
1065
|
}
|
|
986
1066
|
case "optChain":
|
|
987
1067
|
// Narrow should have rewritten optChain to someMatch.
|
|
988
|
-
throw new Error(`optChain reached transform — narrow should have rewritten it`);
|
|
1068
|
+
throw new Error(`optChain reached transform — narrow should have rewritten it: ${JSON.stringify(e).slice(0, 400)}`);
|
|
989
1069
|
case "nullish":
|
|
990
1070
|
// Narrow should have rewritten nullish to someMatch.
|
|
991
|
-
throw new Error(`nullish reached transform — narrow should have rewritten it`);
|
|
1071
|
+
throw new Error(`nullish reached transform — narrow should have rewritten it: ${JSON.stringify(e).slice(0, 300)}`);
|
|
992
1072
|
case "havoc":
|
|
993
1073
|
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
994
1074
|
if (binds) {
|
|
@@ -1009,7 +1089,7 @@ function lowerExpr(e, binds) {
|
|
|
1009
1089
|
// Bare-var shortcut, but route \result through lowerExpr so the
|
|
1010
1090
|
// lemma-side replaceVar pass can substitute it with the function call.
|
|
1011
1091
|
scrutinee = path.fields.length === 0 && path.rootVar !== "\\result"
|
|
1012
|
-
? path.rootVar
|
|
1092
|
+
? varE(path.rootVar)
|
|
1013
1093
|
: lowerExpr(e.scrutinee, binds);
|
|
1014
1094
|
}
|
|
1015
1095
|
else {
|
|
@@ -1040,7 +1120,7 @@ function lowerExpr(e, binds) {
|
|
|
1040
1120
|
// Path scrutinees (e.g. `m.content`) get a synthesized hint derived
|
|
1041
1121
|
// from the last field/var name so the binder reads naturally.
|
|
1042
1122
|
const scrutinee = lowerExpr(e.scrutinee, binds);
|
|
1043
|
-
const decl = _typeDecls
|
|
1123
|
+
const decl = declOf(_typeDecls, e.typeName);
|
|
1044
1124
|
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
1045
1125
|
const varName = e.scrutinee.kind === "var" ? e.scrutinee.name : undefined;
|
|
1046
1126
|
const pathHint = varName ?? scrutineeHint(e.scrutinee);
|
|
@@ -1050,7 +1130,7 @@ function lowerExpr(e, binds) {
|
|
|
1050
1130
|
const fields = variant?.fields ?? [];
|
|
1051
1131
|
let body = lowerExpr(c.body, binds);
|
|
1052
1132
|
if (varName && fields.length > 0) {
|
|
1053
|
-
body = replaceFieldAccess(body, varName, fields);
|
|
1133
|
+
body = replaceFieldAccess(body, varName, fields, c.variant, tyBaseName(e.typeName));
|
|
1054
1134
|
if (isSynthArrayUnion && fields.length === 1) {
|
|
1055
1135
|
body = replaceVarInExpr(body, varName, matchBinder(fields[0].name, varName));
|
|
1056
1136
|
}
|
|
@@ -1071,7 +1151,7 @@ function lowerExpr(e, binds) {
|
|
|
1071
1151
|
body = wrapOptionalBranch(body, e.fallthrough);
|
|
1072
1152
|
arms.push({ pattern: pWild(), body });
|
|
1073
1153
|
}
|
|
1074
|
-
return { kind: "match", scrutinee: varName
|
|
1154
|
+
return { kind: "match", scrutinee: varName !== undefined ? varE(varName) : scrutinee, arms };
|
|
1075
1155
|
}
|
|
1076
1156
|
}
|
|
1077
1157
|
}
|
|
@@ -1100,7 +1180,7 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
1100
1180
|
if (obj.kind !== "var" || obj.ty.kind !== "user")
|
|
1101
1181
|
return null;
|
|
1102
1182
|
const typeName = obj.ty.name;
|
|
1103
|
-
const decl = typeDecls
|
|
1183
|
+
const decl = declOfKind(typeDecls, typeName, "discriminated-union");
|
|
1104
1184
|
if (!decl)
|
|
1105
1185
|
return null;
|
|
1106
1186
|
const variantName = e.left.right.value;
|
|
@@ -1111,18 +1191,26 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
1111
1191
|
const pattern = buildMatchPattern(variantName, fields, obj.name);
|
|
1112
1192
|
let rhs = transformExpr(e.right);
|
|
1113
1193
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
1114
|
-
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
|
|
1194
|
+
return { kind: "match", scrutinee: varE(obj.name), arms: [{ pattern, body: rhs }, { pattern: pWild(), body: { kind: "bool", value: true } }] };
|
|
1115
1195
|
}
|
|
1116
|
-
function replaceFieldAccess(e, varName, fields) {
|
|
1196
|
+
function replaceFieldAccess(e, varName, fields, ctorName, ctorOf) {
|
|
1117
1197
|
return mapExpr(e, x => {
|
|
1118
1198
|
if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
|
|
1119
1199
|
const f = fields.find(f => f.name === x.field);
|
|
1120
1200
|
if (f)
|
|
1121
1201
|
return { kind: "var", name: matchBinder(f.name, varName) };
|
|
1122
1202
|
}
|
|
1203
|
+
// Datatype update of the scrutinee (`{ ...vn, f: v }`): the arm knows the
|
|
1204
|
+
// variant, so stamp it — emitters need it for per-constructor destructor
|
|
1205
|
+
// names. Recurse manually (returning a node stops mapExpr's own descent).
|
|
1206
|
+
if (ctorName && x.kind === "record" && !x.ctor && x.spread &&
|
|
1207
|
+
x.spread.kind === "var" && x.spread.name === varName) {
|
|
1208
|
+
return { ...x, ctor: ctorName, ctorOf,
|
|
1209
|
+
fields: x.fields.map(f => ({ ...f, value: replaceFieldAccess(f.value, varName, fields, ctorName, ctorOf) })) };
|
|
1210
|
+
}
|
|
1123
1211
|
// If this let shadows the matched variable, stop replacing in the body
|
|
1124
1212
|
if (x.kind === "let" && x.name === varName)
|
|
1125
|
-
return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
|
|
1213
|
+
return { ...x, value: replaceFieldAccess(x.value, varName, fields, ctorName, ctorOf) };
|
|
1126
1214
|
return null;
|
|
1127
1215
|
});
|
|
1128
1216
|
}
|
|
@@ -1176,11 +1264,12 @@ function scrutineeHint(e) {
|
|
|
1176
1264
|
// `if (X) continue; rest` → `if (!X) { rest }` at the top of a loop body.
|
|
1177
1265
|
// Dafny's lowered while-loops have the index increment at the bottom, so a
|
|
1178
1266
|
// `continue` would skip it and loop forever; rewriting to if/else lets the
|
|
1179
|
-
// loop fall through normally.
|
|
1267
|
+
// loop fall through normally. The operand is already-lowered IR, where
|
|
1268
|
+
// negation is spelled `¬` (lowerExpr rewrites `!`).
|
|
1180
1269
|
function negateExpr(e) {
|
|
1181
|
-
if (e.kind === "unop" && e.op === "
|
|
1270
|
+
if (e.kind === "unop" && e.op === "¬")
|
|
1182
1271
|
return e.expr;
|
|
1183
|
-
return { kind: "unop", op: "
|
|
1272
|
+
return { kind: "unop", op: "¬", expr: e };
|
|
1184
1273
|
}
|
|
1185
1274
|
/** Build the two pieces of an `arr.pop()` lowering on a named array variable:
|
|
1186
1275
|
* - `optValue` is `(if |arr|>0 then Some(arr[|arr|-1]) else None)` (the popped element)
|
|
@@ -1300,14 +1389,16 @@ function matchToIfChains(stmts) {
|
|
|
1300
1389
|
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1301
1390
|
const ctorArms = arms.filter(a => a.pattern.kind !== "wild");
|
|
1302
1391
|
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;
|
|
1392
|
+
const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
|
|
1307
1393
|
if (!decl)
|
|
1308
1394
|
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1309
|
-
const scrutExpr =
|
|
1395
|
+
const scrutExpr = s.scrutinee;
|
|
1310
1396
|
const defaultArm = arms.find(a => a.pattern.kind === "wild");
|
|
1397
|
+
const declaredCtors = decl.kind === "discriminated-union"
|
|
1398
|
+
? decl.variants?.map(v => v.name)
|
|
1399
|
+
: decl.kind === "string-union" ? decl.values : undefined;
|
|
1400
|
+
const coveredCtors = new Set(ctorArms.map(a => patternCtor(a.pattern)).filter((c) => !!c));
|
|
1401
|
+
const exhaustiveWithoutDefault = !defaultArm && !!declaredCtors && declaredCtors.every(c => coveredCtors.has(c));
|
|
1311
1402
|
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1312
1403
|
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
1313
1404
|
const armBody = ctorArms[k].body;
|
|
@@ -1325,7 +1416,7 @@ function matchToIfChains(stmts) {
|
|
|
1325
1416
|
{ pattern: pCtor(ctor), body: { kind: "bool", value: true } },
|
|
1326
1417
|
{ pattern: pWild(), body: { kind: "bool", value: false } }
|
|
1327
1418
|
] }
|
|
1328
|
-
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name } };
|
|
1419
|
+
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name, args: [] } };
|
|
1329
1420
|
// Bind only the constructor-field binders the body actually uses, pinning the
|
|
1330
1421
|
// owning ctor so the destructor doesn't guess (variants share field names).
|
|
1331
1422
|
const lets = [];
|
|
@@ -1337,6 +1428,13 @@ function matchToIfChains(stmts) {
|
|
|
1337
1428
|
value: { kind: "field", obj: scrutExpr, field: f.name, fromUnion: decl.name, ctor },
|
|
1338
1429
|
});
|
|
1339
1430
|
});
|
|
1431
|
+
// An exhaustive source match has no fallthrough. Use its final arm as
|
|
1432
|
+
// the unconditional else branch; emitting `else pure ()` would force a
|
|
1433
|
+
// Unit result even when every arm returns the method's result type.
|
|
1434
|
+
if (exhaustiveWithoutDefault && k === ctorArms.length - 1) {
|
|
1435
|
+
elseBranch = [...lets, ...armBody];
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1340
1438
|
elseBranch = [{ kind: "if", cond, then: [...lets, ...armBody], else: elseBranch }];
|
|
1341
1439
|
}
|
|
1342
1440
|
return elseBranch;
|
|
@@ -1381,6 +1479,34 @@ function requireDoneWithForBreaks(stmts, fnName) {
|
|
|
1381
1479
|
}
|
|
1382
1480
|
}
|
|
1383
1481
|
}
|
|
1482
|
+
/** The forin emission places the index increment at the loop-body end, so a
|
|
1483
|
+
* surviving `continue` would skip it. Insert the increment immediately
|
|
1484
|
+
* before every same-scope continue (nested loops own their continues),
|
|
1485
|
+
* mirroring the C-style-for desugar's discipline. */
|
|
1486
|
+
function insertIncrementBeforeContinue(stmts, idxName) {
|
|
1487
|
+
const incr = { kind: "assign", target: idxName,
|
|
1488
|
+
value: { kind: "binop", op: "+", left: { kind: "var", name: idxName }, right: { kind: "num", value: 1 } } };
|
|
1489
|
+
const walk = (ss) => {
|
|
1490
|
+
const out = [];
|
|
1491
|
+
for (const s of ss) {
|
|
1492
|
+
if (s.kind === "continue") {
|
|
1493
|
+
out.push(incr, s);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (s.kind === "if") {
|
|
1497
|
+
out.push({ ...s, then: walk(s.then), else: walk(s.else) });
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
if (s.kind === "match") {
|
|
1501
|
+
out.push({ ...s, arms: s.arms.map(a => ({ ...a, body: walk(a.body) })) });
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
out.push(s);
|
|
1505
|
+
}
|
|
1506
|
+
return out;
|
|
1507
|
+
};
|
|
1508
|
+
return walk(stmts);
|
|
1509
|
+
}
|
|
1384
1510
|
function eliminateTopLevelContinue(stmts) {
|
|
1385
1511
|
const out = [];
|
|
1386
1512
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -1489,7 +1615,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
1489
1615
|
const arrSize = { kind: "field", obj: seq, field: "size" };
|
|
1490
1616
|
// Auto-add bound invariant: idx ≤ bound (always true for range loops)
|
|
1491
1617
|
const boundInv = { kind: "binop", op: "≤", left: idxVar, right: arrSize };
|
|
1492
|
-
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
1618
|
+
const bodyStmts = insertIncrementBeforeContinue(eliminateTopLevelContinue(transformStmts(s.body, typeDecls)), idxName);
|
|
1493
1619
|
result.push({
|
|
1494
1620
|
kind: "forin", idx: idxName, bound: arrSize,
|
|
1495
1621
|
invariants: [boundInv, ...s.invariants.map(transformExpr)],
|
|
@@ -1662,13 +1788,13 @@ function transformStmt(s, typeDecls) {
|
|
|
1662
1788
|
}
|
|
1663
1789
|
case "if": {
|
|
1664
1790
|
// Lift from condition only (Lean rule: don't lift from branches).
|
|
1665
|
-
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
1666
|
-
return [...binds, { kind: "if", cond
|
|
1791
|
+
const { binds, expr: cond } = liftMethodCalls(asCondition(s.cond));
|
|
1792
|
+
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
1667
1793
|
}
|
|
1668
1794
|
case "while":
|
|
1669
1795
|
return [{
|
|
1670
1796
|
kind: "while",
|
|
1671
|
-
cond:
|
|
1797
|
+
cond: transformExpr(asCondition(s.cond)),
|
|
1672
1798
|
invariants: s.invariants.map(transformExpr),
|
|
1673
1799
|
decreasing: s.decreases ? transformExpr(s.decreases) : null,
|
|
1674
1800
|
doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
|
|
@@ -1692,7 +1818,7 @@ function transformStmt(s, typeDecls) {
|
|
|
1692
1818
|
const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
|
|
1693
1819
|
const someBody = transformStmts(replaced, typeDecls);
|
|
1694
1820
|
const noneBody = transformStmts(s.noneBody, typeDecls);
|
|
1695
|
-
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
1821
|
+
const scrutinee = path.fields.length === 0 ? varE(path.rootVar) : transformExpr(s.scrutinee);
|
|
1696
1822
|
return [{
|
|
1697
1823
|
kind: "match", scrutinee,
|
|
1698
1824
|
arms: [
|
|
@@ -1716,13 +1842,13 @@ function mapStmtExprs(s, r) {
|
|
|
1716
1842
|
* and delegates body transformation to the caller-provided function.
|
|
1717
1843
|
* Returns null if any body transformation returns null (pure path abort). */
|
|
1718
1844
|
function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
1719
|
-
const decl = typeName ? typeDecls
|
|
1845
|
+
const decl = typeName ? declOf(typeDecls, typeName) : undefined;
|
|
1720
1846
|
const arms = [];
|
|
1721
1847
|
for (const c of cases) {
|
|
1722
1848
|
const variant = decl?.variants?.find(v => v.name === c.name);
|
|
1723
1849
|
const fields = variant?.fields ?? [];
|
|
1724
1850
|
const pattern = buildMatchPattern(c.name, fields, varName);
|
|
1725
|
-
const body = transformBody(c.body, varName, fields);
|
|
1851
|
+
const body = transformBody(c.body, varName, fields, c.name);
|
|
1726
1852
|
if (body === null)
|
|
1727
1853
|
return null;
|
|
1728
1854
|
arms.push({ pattern, body });
|
|
@@ -1730,7 +1856,7 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
|
|
|
1730
1856
|
return arms;
|
|
1731
1857
|
}
|
|
1732
1858
|
function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
1733
|
-
const decl = typeDecls
|
|
1859
|
+
const decl = declOf(typeDecls, typeName);
|
|
1734
1860
|
// Synth array-unions (discriminant "__isArray__") have single-field variants
|
|
1735
1861
|
// ArrayBranch(arr) / NonArrayBranch(val). The matched arm refers to the
|
|
1736
1862
|
// scrutinee by its bare name/path (`content`, `m.content`), not `.arr`, so
|
|
@@ -1784,7 +1910,7 @@ function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
|
|
|
1784
1910
|
arms.push({ pattern: pWild(), body: transformStmts(fallthrough, typeDecls) });
|
|
1785
1911
|
}
|
|
1786
1912
|
}
|
|
1787
|
-
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : prefix, arms };
|
|
1913
|
+
return { kind: "match", scrutinee: isPath ? transformExpr(scrutinee) : varE(prefix), arms };
|
|
1788
1914
|
}
|
|
1789
1915
|
/** Replace bare `var(oldName)` references → `var(newName)` with the given type.
|
|
1790
1916
|
* Used by emitMatchStmt for synth array-unions where the variant has a single
|
|
@@ -1799,7 +1925,7 @@ function replaceVarInTStmts(stmts, oldName, newName, newTy) {
|
|
|
1799
1925
|
}
|
|
1800
1926
|
/** If the chain has matched all variants but one, return that remaining variant. */
|
|
1801
1927
|
function remainingVariant(typeName, cases, typeDecls) {
|
|
1802
|
-
const decl = typeDecls
|
|
1928
|
+
const decl = declOf(typeDecls, typeName);
|
|
1803
1929
|
if (!decl?.variants)
|
|
1804
1930
|
return null;
|
|
1805
1931
|
const matched = new Set(cases.map(c => c.variant));
|
|
@@ -1818,10 +1944,7 @@ function remainingVariant(typeName, cases, typeDecls) {
|
|
|
1818
1944
|
function enumFieldSwitch(s, typeDecls) {
|
|
1819
1945
|
if (!s.discriminant)
|
|
1820
1946
|
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;
|
|
1947
|
+
const objDecl = declOfTy(typeDecls, s.expr.ty);
|
|
1825
1948
|
if (objDecl?.kind === "discriminated-union" && objDecl.discriminant === s.discriminant)
|
|
1826
1949
|
return null;
|
|
1827
1950
|
const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
|
|
@@ -1830,15 +1953,36 @@ function enumFieldSwitch(s, typeDecls) {
|
|
|
1830
1953
|
enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
|
|
1831
1954
|
};
|
|
1832
1955
|
}
|
|
1956
|
+
/** Stamp variant ctor info onto datatype updates of the match scrutinee in
|
|
1957
|
+
* lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
|
|
1958
|
+
* `replaceFieldAccess`'s stamping. Emitters need the pin to use
|
|
1959
|
+
* per-constructor destructor names for collision-renamed fields. */
|
|
1960
|
+
function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
|
|
1961
|
+
const stamp = (x) => {
|
|
1962
|
+
if (x.kind === "record" && !x.ctor && x.spread &&
|
|
1963
|
+
x.spread.kind === "var" && x.spread.name === varName) {
|
|
1964
|
+
return { ...x, ctor: ctorName, ctorOf,
|
|
1965
|
+
fields: x.fields.map(f => ({ ...f, value: mapExpr(f.value, stamp) })) };
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
};
|
|
1969
|
+
return body.map(st => mapStmt(st, stamp));
|
|
1970
|
+
}
|
|
1833
1971
|
function emitSwitchStmt(s, typeDecls) {
|
|
1834
1972
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1835
1973
|
const ef = enumFieldSwitch(s, typeDecls);
|
|
1974
|
+
const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
|
|
1836
1975
|
const arms = ef
|
|
1837
1976
|
? 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) =>
|
|
1977
|
+
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields, ctorName) => {
|
|
1978
|
+
let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
|
|
1979
|
+
if (ctorName && vn && baseName)
|
|
1980
|
+
out = stampScrutineeUpdates(out, vn, ctorName, baseName);
|
|
1981
|
+
return out;
|
|
1982
|
+
});
|
|
1839
1983
|
if (s.defaultBody.length > 0)
|
|
1840
1984
|
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 };
|
|
1985
|
+
return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
1842
1986
|
}
|
|
1843
1987
|
/** Replace obj.field → replacement var in typed IR.
|
|
1844
1988
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
@@ -1857,14 +2001,67 @@ function replaceFieldsInTStmts(stmts, objName, replacements) {
|
|
|
1857
2001
|
}));
|
|
1858
2002
|
}
|
|
1859
2003
|
/** Replace all variant fields of obj → match binder vars in typed IR.
|
|
1860
|
-
*
|
|
2004
|
+
* Before replacing field reads, realize TypeScript's structural argument
|
|
2005
|
+
* conversion for calls such as `helper(outcome)` inside a narrowed union arm.
|
|
2006
|
+
* The source value is still the enclosing union in typed IR, while Dafny and
|
|
2007
|
+
* Lean expect the helper's nominal record. Rebuild that record solely from
|
|
2008
|
+
* the fields bound by this arm's match pattern. */
|
|
1861
2009
|
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
1862
|
-
|
|
2010
|
+
const projected = projectStructuralCallArgsInTStmts(stmts, varName, fields);
|
|
2011
|
+
return replaceFieldsInTStmts(projected, varName, fields.map(f => ({
|
|
1863
2012
|
fieldName: f.name,
|
|
1864
2013
|
newName: matchBinder(f.name, varName),
|
|
1865
2014
|
fallbackTy: f.type ?? parseTsType(f.tsType),
|
|
1866
2015
|
})));
|
|
1867
2016
|
}
|
|
2017
|
+
/** Project a narrowed union scrutinee into a named structural record expected
|
|
2018
|
+
* by a same-module/extern call. Resolution stamps named calls with paramTys;
|
|
2019
|
+
* this pass fires only when every target record field has an identically typed
|
|
2020
|
+
* match-bound source field. That deliberately avoids inventing a broad cast:
|
|
2021
|
+
* it is the nominal-backend witness for the structural call TS already accepts. */
|
|
2022
|
+
function projectStructuralCallArgsInTStmts(stmts, varName, fields) {
|
|
2023
|
+
const sourceFields = fields.map(f => ({
|
|
2024
|
+
...f,
|
|
2025
|
+
resolvedTy: f.type ?? parseTsType(f.tsType),
|
|
2026
|
+
}));
|
|
2027
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
2028
|
+
if (e.kind !== "call" || !e.paramTys)
|
|
2029
|
+
return null;
|
|
2030
|
+
const paramTys = e.paramTys;
|
|
2031
|
+
let changed = false;
|
|
2032
|
+
const args = e.args.map((arg, i) => {
|
|
2033
|
+
if (arg.kind !== "var" || arg.name !== varName || i >= paramTys.length)
|
|
2034
|
+
return arg;
|
|
2035
|
+
const targetTy = paramTys[i];
|
|
2036
|
+
const targetDecl = declOfTy(_typeDecls, targetTy);
|
|
2037
|
+
if (targetTy.kind !== "user" || targetDecl?.kind !== "record" || !targetDecl.fields)
|
|
2038
|
+
return arg;
|
|
2039
|
+
const matched = [];
|
|
2040
|
+
for (const targetField of targetDecl.fields) {
|
|
2041
|
+
const sourceField = sourceFields.find(f => f.name === targetField.name);
|
|
2042
|
+
const targetFieldTy = targetField.type ?? parseTsType(targetField.tsType);
|
|
2043
|
+
if (!sourceField || !tyEqual(sourceField.resolvedTy, targetFieldTy))
|
|
2044
|
+
return arg;
|
|
2045
|
+
matched.push({ targetField, sourceField });
|
|
2046
|
+
}
|
|
2047
|
+
changed = true;
|
|
2048
|
+
return {
|
|
2049
|
+
kind: "record",
|
|
2050
|
+
spread: null,
|
|
2051
|
+
fields: matched.map(m => ({
|
|
2052
|
+
name: m.targetField.name,
|
|
2053
|
+
value: {
|
|
2054
|
+
kind: "var",
|
|
2055
|
+
name: matchBinder(m.sourceField.name, varName),
|
|
2056
|
+
ty: m.sourceField.resolvedTy,
|
|
2057
|
+
},
|
|
2058
|
+
})),
|
|
2059
|
+
ty: targetTy,
|
|
2060
|
+
};
|
|
2061
|
+
});
|
|
2062
|
+
return changed ? { ...e, args } : null;
|
|
2063
|
+
}));
|
|
2064
|
+
}
|
|
1868
2065
|
/** Replace obj.field → replacement var in typed IR expressions (before lowering).
|
|
1869
2066
|
* Mirrors replaceFieldsInTStmts but operates on a single TExpr tree. */
|
|
1870
2067
|
function replaceFieldInTExpr(expr, objName, replacements) {
|
|
@@ -1952,7 +2149,7 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1952
2149
|
const elseExpr = transformPureBody(elseStmts, typeDecls);
|
|
1953
2150
|
if (!elseExpr)
|
|
1954
2151
|
return null;
|
|
1955
|
-
return { kind: "if", cond:
|
|
2152
|
+
return { kind: "if", cond: transformExpr(asCondition(s.cond)), then: thenExpr, else: elseExpr };
|
|
1956
2153
|
}
|
|
1957
2154
|
case "switch": return transformPureSwitch(s, typeDecls);
|
|
1958
2155
|
case "someMatch": {
|
|
@@ -1965,7 +2162,7 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1965
2162
|
const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
|
|
1966
2163
|
if (!noneExpr)
|
|
1967
2164
|
return null;
|
|
1968
|
-
const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
|
|
2165
|
+
const scrutinee = path.fields.length === 0 ? varE(path.rootVar) : transformExpr(s.scrutinee);
|
|
1969
2166
|
return {
|
|
1970
2167
|
kind: "match", scrutinee,
|
|
1971
2168
|
arms: [
|
|
@@ -1997,16 +2194,17 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
1997
2194
|
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
1998
2195
|
}
|
|
1999
2196
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
|
|
2000
|
-
if (!typeDecls
|
|
2197
|
+
if (!declOf(typeDecls, typeName))
|
|
2001
2198
|
return null;
|
|
2002
2199
|
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
2003
2200
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
2004
|
-
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
|
|
2005
|
-
|
|
2201
|
+
const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2202
|
+
const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
|
|
2203
|
+
let result = transformPureBody(projected, typeDecls);
|
|
2006
2204
|
if (!result)
|
|
2007
2205
|
return null;
|
|
2008
2206
|
if (fields.length > 0 && vn)
|
|
2009
|
-
result = replaceFieldAccess(result, vn, fields);
|
|
2207
|
+
result = replaceFieldAccess(result, vn, fields, ctorName, tyBaseName(typeName));
|
|
2010
2208
|
return result;
|
|
2011
2209
|
});
|
|
2012
2210
|
if (!arms)
|
|
@@ -2019,21 +2217,22 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
2019
2217
|
}
|
|
2020
2218
|
if (s.expr.kind !== "var")
|
|
2021
2219
|
return null;
|
|
2022
|
-
return { kind: "match", scrutinee: s.expr.name, arms };
|
|
2220
|
+
return { kind: "match", scrutinee: varE(s.expr.name), arms };
|
|
2023
2221
|
}
|
|
2024
2222
|
function transformPureMatch(chain, typeDecls) {
|
|
2025
2223
|
const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
|
|
2026
|
-
const decl = typeDecls
|
|
2224
|
+
const decl = declOf(typeDecls, chain.typeName);
|
|
2027
2225
|
// Synth array-unions have single-field variants and user code refers to the
|
|
2028
2226
|
// scrutinee by its bare name, not field-accessed. See emitMatchStmt for
|
|
2029
2227
|
// the statement-level counterpart of this substitution.
|
|
2030
2228
|
const isSynthArrayUnion = decl?.discriminant === "__isArray__";
|
|
2031
|
-
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
|
|
2032
|
-
|
|
2229
|
+
const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields, ctorName) => {
|
|
2230
|
+
const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
|
|
2231
|
+
let result = transformPureBody(projected, typeDecls);
|
|
2033
2232
|
if (!result)
|
|
2034
2233
|
return null;
|
|
2035
2234
|
if (fields.length > 0 && vn)
|
|
2036
|
-
result = replaceFieldAccess(result, vn, fields);
|
|
2235
|
+
result = replaceFieldAccess(result, vn, fields, ctorName, tyBaseName(chain.typeName));
|
|
2037
2236
|
if (isSynthArrayUnion && fields.length === 1 && vn) {
|
|
2038
2237
|
result = replaceVarInExpr(result, vn, matchBinder(fields[0].name, vn));
|
|
2039
2238
|
}
|
|
@@ -2049,11 +2248,12 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2049
2248
|
const remaining = remainingVariant(chain.typeName, chain.cases, typeDecls);
|
|
2050
2249
|
if (remaining) {
|
|
2051
2250
|
// Exactly one variant left — destructure for variant-specific field access.
|
|
2052
|
-
|
|
2251
|
+
const projected = projectStructuralCallArgsInTStmts(chain.fallthrough, chain.varName, remaining.fields);
|
|
2252
|
+
let body = transformPureBody(projected, typeDecls);
|
|
2053
2253
|
if (!body)
|
|
2054
2254
|
return null;
|
|
2055
2255
|
if (remaining.fields.length > 0)
|
|
2056
|
-
body = replaceFieldAccess(body, chain.varName, remaining.fields);
|
|
2256
|
+
body = replaceFieldAccess(body, chain.varName, remaining.fields, remaining.name, tyBaseName(chain.typeName));
|
|
2057
2257
|
if (isSynthArrayUnion && remaining.fields.length === 1) {
|
|
2058
2258
|
body = replaceVarInExpr(body, chain.varName, matchBinder(remaining.fields[0].name, chain.varName));
|
|
2059
2259
|
}
|
|
@@ -2066,7 +2266,7 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
2066
2266
|
arms.push({ pattern: pWild(), body });
|
|
2067
2267
|
}
|
|
2068
2268
|
}
|
|
2069
|
-
return { kind: "match", scrutinee: chain.varName, arms };
|
|
2269
|
+
return { kind: "match", scrutinee: varE(chain.varName), arms };
|
|
2070
2270
|
}
|
|
2071
2271
|
// ── Generate type declarations ───────────────────────────────
|
|
2072
2272
|
function transformTypeDecl(d) {
|
|
@@ -2203,6 +2403,7 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2203
2403
|
_forofCounters.clear();
|
|
2204
2404
|
_liftCounter = 0;
|
|
2205
2405
|
_typeDecls = mod.typeDecls;
|
|
2406
|
+
_neededKindHelpers = new Map();
|
|
2206
2407
|
_pureDefNames = new Set(mod.functions.filter(f => f.isPure).map(f => f.name));
|
|
2207
2408
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
2208
2409
|
// Module-level constants
|
|
@@ -2272,27 +2473,6 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2272
2473
|
ensures: ext.ensures.map(e => replaceVar(transformExpr(e), "\\result", fnCall)),
|
|
2273
2474
|
};
|
|
2274
2475
|
});
|
|
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
2476
|
// Def file: Velvet methods
|
|
2297
2477
|
// Pure functions get a thin wrapper that calls Pure.fnName
|
|
2298
2478
|
// def-by-method functions also skip their method wrappers
|
|
@@ -2369,6 +2549,106 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2369
2549
|
methods: classMethods,
|
|
2370
2550
|
};
|
|
2371
2551
|
});
|
|
2552
|
+
// Types file — assembled after all body transforms, so needs discovered
|
|
2553
|
+
// there (discriminator kind-helpers) are included.
|
|
2554
|
+
const kindHelpers = [..._neededKindHelpers.values()].map(kindHelperDecl);
|
|
2555
|
+
// ── Imported / undeclared user types: opaque by default ─────────────
|
|
2556
|
+
// A module may reference types it imports (ir.ts uses typedir's `Ty`).
|
|
2557
|
+
// Standalone compilation has no declaration for them; synthesize an
|
|
2558
|
+
// opaque type — the value passes through, uninspectable, which is the
|
|
2559
|
+
// only sound use of an undeclared type (same doctrine as _synthOpaque).
|
|
2560
|
+
// Any attempted inspection still fails loudly: an opaque type has no
|
|
2561
|
+
// constructors and no operations. Signature-level coverage (type-decl
|
|
2562
|
+
// fields, params, returns, class fields, externs, consts); a body-level
|
|
2563
|
+
// reference to an undeclared type still errors in the backend.
|
|
2564
|
+
const referenced = new Set();
|
|
2565
|
+
const collectTy = (ty) => {
|
|
2566
|
+
switch (ty.kind) {
|
|
2567
|
+
case "user":
|
|
2568
|
+
referenced.add(tyBaseName(ty.name));
|
|
2569
|
+
return;
|
|
2570
|
+
case "array":
|
|
2571
|
+
case "set":
|
|
2572
|
+
collectTy(ty.elem);
|
|
2573
|
+
return;
|
|
2574
|
+
case "optional":
|
|
2575
|
+
collectTy(ty.inner);
|
|
2576
|
+
return;
|
|
2577
|
+
case "map":
|
|
2578
|
+
collectTy(ty.key);
|
|
2579
|
+
collectTy(ty.value);
|
|
2580
|
+
return;
|
|
2581
|
+
case "tuple":
|
|
2582
|
+
ty.elems.forEach(collectTy);
|
|
2583
|
+
return;
|
|
2584
|
+
case "fn":
|
|
2585
|
+
ty.params.forEach(collectTy);
|
|
2586
|
+
collectTy(ty.result);
|
|
2587
|
+
return;
|
|
2588
|
+
default: return;
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
const knownTypeNames = new Set(typeDecls.map(d => d.name));
|
|
2592
|
+
const allTypeParams = new Set();
|
|
2593
|
+
// Exclude type params from the *source* decls — the transformed IR drops
|
|
2594
|
+
// them for aliases (`type Step<S, A> = …`), and a generic alias's params
|
|
2595
|
+
// must not be mistaken for imported types. Params may carry a `//@ type`
|
|
2596
|
+
// decoration ("S(==)"); references collect as the bare name, so strip it.
|
|
2597
|
+
const addTp = (tp) => { allTypeParams.add(tp.replace(/\(.*$/, "").trim()); };
|
|
2598
|
+
for (const d of mod.typeDecls)
|
|
2599
|
+
d.typeParams?.forEach(addTp);
|
|
2600
|
+
for (const d of typeDecls) {
|
|
2601
|
+
if (d.kind === "inductive")
|
|
2602
|
+
d.constructors.forEach(c => c.fields.forEach(f => collectTy(f.type)));
|
|
2603
|
+
else if (d.kind === "structure")
|
|
2604
|
+
d.fields.forEach(f => collectTy(f.type));
|
|
2605
|
+
else if (d.kind === "type-alias")
|
|
2606
|
+
collectTy(d.target);
|
|
2607
|
+
}
|
|
2608
|
+
for (const fn of mod.functions) {
|
|
2609
|
+
fn.typeParams.forEach(addTp);
|
|
2610
|
+
fn.params.forEach(p => collectTy(p.ty));
|
|
2611
|
+
collectTy(fn.returnTy);
|
|
2612
|
+
}
|
|
2613
|
+
for (const cls of mod.classes ?? []) {
|
|
2614
|
+
cls.fields.forEach(f => collectTy(f.ty));
|
|
2615
|
+
for (const m of cls.methods) {
|
|
2616
|
+
m.typeParams.forEach(addTp);
|
|
2617
|
+
m.params.forEach(p => collectTy(p.ty));
|
|
2618
|
+
collectTy(m.returnTy);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
for (const ext of mod.externs ?? []) {
|
|
2622
|
+
ext.typeParams.forEach(addTp);
|
|
2623
|
+
ext.params.forEach(p => collectTy(p.ty));
|
|
2624
|
+
collectTy(ext.returnTy);
|
|
2625
|
+
}
|
|
2626
|
+
for (const c of mod.constants ?? [])
|
|
2627
|
+
collectTy(c.ty);
|
|
2628
|
+
const opaqueImports = [...referenced]
|
|
2629
|
+
.filter(n => !knownTypeNames.has(n) && !allTypeParams.has(n))
|
|
2630
|
+
.map(n => ({ kind: "opaque-type", name: n }));
|
|
2631
|
+
const typesImports = ["LemmaScript"];
|
|
2632
|
+
let typesFile = null;
|
|
2633
|
+
const pureNamespace = pureDefs.length > 0
|
|
2634
|
+
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
2635
|
+
: [];
|
|
2636
|
+
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0 || opaqueImports.length > 0) {
|
|
2637
|
+
// Declaration order differs by backend. Dafny allows forward references, so
|
|
2638
|
+
// externs go first to be in scope everywhere. Lean requires definition-before-use:
|
|
2639
|
+
// an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
|
|
2640
|
+
// so types must precede externs, which in turn precede the pure mirrors that may call them.
|
|
2641
|
+
// Kind-helpers sit right after the datatypes they discriminate.
|
|
2642
|
+
const decls = _opts.backend === "lean"
|
|
2643
|
+
? [...opaqueImports, ...typeDecls, ...kindHelpers, ...externDecls, ...pureNamespace]
|
|
2644
|
+
: [...externDecls, ...opaqueImports, ...typeDecls, ...kindHelpers, ...pureNamespace];
|
|
2645
|
+
typesFile = {
|
|
2646
|
+
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
2647
|
+
imports: typesImports,
|
|
2648
|
+
options: [],
|
|
2649
|
+
decls,
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2372
2652
|
const defImport = specImport ?? (typesFile ? `«${moduleBase}.types»` : null);
|
|
2373
2653
|
const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
|
|
2374
2654
|
const defFile = {
|