lemmascript 0.5.8 → 0.5.9
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/dafny-emit.js +3 -0
- package/tools/dist/ir.js +56 -1
- package/tools/dist/lean-emit.js +242 -22
- package/tools/dist/lsc.js +14 -6
- package/tools/dist/narrow.js +33 -1
- package/tools/dist/peephole.js +2 -0
- package/tools/dist/transform.js +224 -7
- package/tools/dist/emit.js +0 -253
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ See the external case studies:
|
|
|
29
29
|
- **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (semantic: under a unique-id precondition, the result is in-place — `|result| ≤ |edges|`, no insertion — *and* when a matching edge existed with non-empty new endpoints, the output contains an edge with those endpoints. Uses `//@ assume` to characterize destructuring, `find`, and the constructed edge), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Also adds a new verified feature — a **DAG connection gate**: `canReach` decides reachability soundly *and* completely, and `wouldCreateCycle` gates both new connections and edge reconnections so the graph is proven to stay acyclic, with `isAcyclic` (sound + complete) establishing the base case and a topological-rank witness giving a safe evaluation order (+29 obligations), shown live in a React Flow demo that refuses cycle-closing edges — extending the case study from verifying existing code to adding a verified feature. Dafny only.
|
|
30
30
|
- **[rallly-lemmascript](https://github.com/midspiral/rallly-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [rallly](https://github.com/lukevella/rallly)'s meeting-poll Next.js app. 2 functions: `validateRedirectUrl` (in-place — open-redirect predicate; non-`undefined` outputs start with `/` but not `//`) and `scorePoll` (extracted ranking core — length preservation, score bounds, top-choice characterization, score-formula equality, within-poll monotonicity, tiebreaker injectivity). The injectivity proof surfaced a real spec-level constraint on the existing `(yes + ifNeedBe) * 1000 + yes` encoding: it overflows when an option has ≥ 1000 yes votes. 10 Dafny VCs, 0 errors. Drove four toolchain additions: `s.startsWith()`, `T | null` nullability, `\result` narrowing under `==>`, `Math.max(...arr)` spread. Dafny only.
|
|
31
31
|
- **[opencode-lemmascript](https://github.com/midspiral/opencode-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [opencode](https://github.com/anomalyco/opencode)'s permission system and unified-diff patch parser. Highlights: (1) `Patch.parsePatch` carries conservation loop invariants over local ghost state — a parser bug here would silently corrupt user files when an AI applies a patch, and (2) the permission-engine work mechanically closes opencode bug #26514 (subagents bypassing Plan Mode's file-edit restrictions). 9 functions verified in-place, 0 errors. Dafny only.
|
|
32
|
-
- **[pi-lemmascript](https://github.com/midspiral/pi-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield, **in-place** verification of the context-compaction cut-point selector in **pi** (the [earendil-works](https://pi.dev) agent harness). When the context window fills, pi discards history before a chosen cut; a provider API rejects a retained prefix containing an orphaned `toolResult` (a tool result whose tool call was cut away). Both selector functions proven: the cut never lets the kept suffix *start with* — nor *split a tool-use/tool-result run* into — an orphaned tool result, even across the backward metadata snap. The no-orphan result forced the session tree's tool-pairing ordering into an explicit `requires`. 4 VCs, 0 errors. Drove five toolchain additions, headlined by an **opaque fall-through type**: a union LemmaScript can't discriminate (here an array-element union of unreachable imports) becomes a single opaque `type` — the field stays present so distinct values stay distinct, and with no constructor or tag predicate it can only be passed through, never unsoundly observed. Dafny
|
|
32
|
+
- **[pi-lemmascript](https://github.com/midspiral/pi-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield, **in-place** verification of the context-compaction cut-point selector in **pi** (the [earendil-works](https://pi.dev) agent harness). When the context window fills, pi discards history before a chosen cut; a provider API rejects a retained prefix containing an orphaned `toolResult` (a tool result whose tool call was cut away). Both selector functions proven: the cut never lets the kept suffix *start with* — nor *split a tool-use/tool-result run* into — an orphaned tool result, even across the backward metadata snap. The no-orphan result forced the session tree's tool-pairing ordering into an explicit `requires`. 4 VCs, 0 errors. Drove five toolchain additions, headlined by an **opaque fall-through type**: a union LemmaScript can't discriminate (here an array-element union of unreachable imports) becomes a single opaque `type` — the field stays present so distinct values stay distinct, and with no constructor or tag predicate it can only be passed through, never unsoundly observed. **Dafny + Lean**: the no-orphan theorem, the changelog semver core, and both tool-output truncators also carry Lean 4 (Velvet/Loom) proofs from the same annotated source, zero `sorry`; the Lean port drove the backend's brownfield batch (cross-file externs, union destructor lowering, Bool-vs-Prop contexts, `return`-in-loop elimination).
|
|
33
33
|
- **[balanced-match-lemmascript](https://github.com/midspiral/balanced-match-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield, **in-place** verification of [balanced-match](https://github.com/juliangruber/balanced-match), the ~70-line balanced-bracket finder pulled in by `npm`, `webpack`, and most of the JS tooling stack (1B+ downloads/month). The stack-based `range` core is verified by **refinement**: a pure recursive spec `range_spec` mirrors the loop one branch per recursive case, so the single equivalence `range == range_spec` transfers every property automatically — including an unconditional Dyck body-balance theorem for the interior of every returned pair. 2233 VCs, 0 errors under `--isolate-assertions` (registered on the `dafny-slow` track). Dafny only.
|
|
34
34
|
- **[guardians-lemmascript](https://github.com/midspiral/guardians-lemmascript)** — greenfield verification of the core safety argument behind [Guardians](https://github.com/metareflection/guardians) (Erik Meijer, "Guardians of the Agents", CACM Jan 2026), a generate-verify-execute checker for AI-agent workflows. Instead of verifying an app, it proves the agent *guardrail itself sound* — that a static taint/automaton check over the real recursive workflow AST can never admit an unsafe plan. Highlights: taint over **nested conditionals** as a sound branch-union over-approximation; per-source **provenance** with a join (a multi-input tool is tainted if *any* input was); **unbounded loops** discharged by a one-step pre-fixpoint (`sat = t0 ‖ bodyTaint(t0)`) that bounds taint over any iteration count without iterating to a fixpoint; and a **unified capstone** (`verifyWfSound`) — one clean verdict rules out, on *every* path, both a tainted-data-to-sink leak and a security-automaton error. 54 Dafny obligations, 0 errors. The verified cores are reached from a Guardians-style `Workflow`/`Policy` through a thin *unverified* adapter, differentially tested against the real Python Guardians (used as the oracle, not a porting target). Dafny only.
|
|
35
35
|
- **[quorum-lemmascript](https://github.com/midspiral/quorum-lemmascript)** — greenfield verified when2meet/Doodle-style group scheduler (React + Cloudflare Durable Objects), with one `domain.ts` running unchanged in the browser, the in-app query, and the server. The standout is that **the proof licenses the architecture**: `countFree` is a homomorphism from participant-list concatenation to integer addition (so the heatmap is order-independent) plus same-participant last-writer-wins convergence — which is exactly what makes the lock-free, no-login, *optimistic* multi-device backend safe, with the Durable Object and the browser applying the **same** verified `applyOp` (server-authoritatively, client-optimistically) with no rollback or operational transform. Also: heatmap is exactly the per-slot count and `isBest` exactly its argmax; monotonicity; invariant-preserving mutations + op-log `replay`; a sparse export codec round-trip; an in-app `whoIsFree(e, s)` whose length provably equals the cell's count; and a separate `grid.ts` proving the `(day, time) → slot` map in-range + injective — which makes specific-dates-vs-days-of-the-week pure shell labeling at zero proof cost; and full element-level permutation invariance (`heatmapPermInvariant` — the heatmap depends only on the *multiset* of participant rows), which drove the `perm(...)` spec predicate into LemmaScript itself. 100 Dafny VCs (90 + 10), 0 errors. The *aggregate* is proven; the React UI, WebSocket/DO I/O, and timezone labeling are the stated trust boundary. Dafny only.
|
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -533,6 +533,9 @@ function emitExpr(e) {
|
|
|
533
533
|
case "exists": return emitQuantifier(e, "exists");
|
|
534
534
|
case "let": return `(var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)})`;
|
|
535
535
|
case "havoc": return "*";
|
|
536
|
+
// No Dafny producer: the only `default` source is the Lean return-in-loop
|
|
537
|
+
// rewrite, which never runs on the Dafny path (native in-loop returns).
|
|
538
|
+
case "default": throw new Error("default-value expression not produced on the Dafny path");
|
|
536
539
|
}
|
|
537
540
|
}
|
|
538
541
|
/** Emit a pure expression with indentation for if/match/let. */
|
package/tools/dist/ir.js
CHANGED
|
@@ -4,4 +4,59 @@
|
|
|
4
4
|
* The transform phase produces these types.
|
|
5
5
|
* The emit phase pretty-prints them to backend syntax (Lean or Dafny).
|
|
6
6
|
*/
|
|
7
|
-
export {
|
|
7
|
+
export function anyExpr(e, pred) {
|
|
8
|
+
if (pred(e))
|
|
9
|
+
return true;
|
|
10
|
+
switch (e.kind) {
|
|
11
|
+
case "var":
|
|
12
|
+
case "num":
|
|
13
|
+
case "bool":
|
|
14
|
+
case "str":
|
|
15
|
+
case "emptyMap":
|
|
16
|
+
case "emptySet":
|
|
17
|
+
case "havoc":
|
|
18
|
+
case "default": return false;
|
|
19
|
+
case "constructor": return (e.args ?? []).some(a => anyExpr(a, pred));
|
|
20
|
+
case "binop": return anyExpr(e.left, pred) || anyExpr(e.right, pred);
|
|
21
|
+
case "unop":
|
|
22
|
+
case "toNat":
|
|
23
|
+
case "toReal": return anyExpr(e.expr, pred);
|
|
24
|
+
case "app": return e.args.some(a => anyExpr(a, pred));
|
|
25
|
+
case "field": return anyExpr(e.obj, pred);
|
|
26
|
+
case "index": return anyExpr(e.arr, pred) || anyExpr(e.idx, pred);
|
|
27
|
+
case "implies": return e.premises.some(p => anyExpr(p, pred)) || anyExpr(e.conclusion, pred);
|
|
28
|
+
case "record": return (e.spread ? anyExpr(e.spread, pred) : false) || e.fields.some(f => anyExpr(f.value, pred));
|
|
29
|
+
case "arrayLiteral": return e.elems.some(x => anyExpr(x, pred));
|
|
30
|
+
case "mapLiteral": return e.entries.some(en => anyExpr(en.key, pred) || anyExpr(en.value, pred));
|
|
31
|
+
case "methodCall": return anyExpr(e.obj, pred) || e.args.some(a => anyExpr(a, pred));
|
|
32
|
+
case "lambda": return e.body.some(s => anyExprInStmt(s, pred));
|
|
33
|
+
case "if": return anyExpr(e.cond, pred) || anyExpr(e.then, pred) || anyExpr(e.else, pred);
|
|
34
|
+
case "match": return (typeof e.scrutinee !== "string" && anyExpr(e.scrutinee, pred)) || e.arms.some(a => anyExpr(a.body, pred));
|
|
35
|
+
case "forall":
|
|
36
|
+
case "exists": return anyExpr(e.body, pred);
|
|
37
|
+
case "let": return anyExpr(e.value, pred) || anyExpr(e.body, pred);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function anyExprInStmt(s, pred) {
|
|
41
|
+
switch (s.kind) {
|
|
42
|
+
case "let":
|
|
43
|
+
case "assign":
|
|
44
|
+
case "bind":
|
|
45
|
+
case "let-bind":
|
|
46
|
+
case "return":
|
|
47
|
+
case "ghostLet":
|
|
48
|
+
case "ghostAssign": return anyExpr(s.value, pred);
|
|
49
|
+
case "assert": return anyExpr(s.expr, pred);
|
|
50
|
+
case "break":
|
|
51
|
+
case "continue": return false;
|
|
52
|
+
case "if": return anyExpr(s.cond, pred) || anyExprInStmts(s.then, pred) || anyExprInStmts(s.else, pred);
|
|
53
|
+
case "match": return (typeof s.scrutinee !== "string" && anyExpr(s.scrutinee, pred)) || s.arms.some(a => anyExprInStmts(a.body, pred));
|
|
54
|
+
case "while": return anyExpr(s.cond, pred) || s.invariants.some(i => anyExpr(i, pred))
|
|
55
|
+
|| (s.decreasing ? anyExpr(s.decreasing, pred) : false) || (s.doneWith ? anyExpr(s.doneWith, pred) : false)
|
|
56
|
+
|| anyExprInStmts(s.body, pred);
|
|
57
|
+
case "forin": return anyExpr(s.bound, pred) || s.invariants.some(i => anyExpr(i, pred)) || anyExprInStmts(s.body, pred);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function anyExprInStmts(stmts, pred) {
|
|
61
|
+
return stmts.some(s => anyExprInStmt(s, pred));
|
|
62
|
+
}
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Lean emitter — IR → Lean text.
|
|
3
3
|
* No logic, no type decisions — just serialization.
|
|
4
4
|
*/
|
|
5
|
+
import { anyExpr } from "./ir.js";
|
|
5
6
|
// ── Ty → Lean type string ──────────────────────────────────
|
|
6
7
|
function tyToLean(ty) {
|
|
7
8
|
switch (ty.kind) {
|
|
@@ -55,7 +56,14 @@ function tyToLean(ty) {
|
|
|
55
56
|
const retStr = ret.includes(" ") ? `(${ret})` : ret;
|
|
56
57
|
return [...params, retStr].join(" → ");
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
// Out-of-subset (`any`/`unknown`) — an opaque carrier, so unmodeled payloads
|
|
60
|
+
// (e.g. a `details` field never inspected by the verified code) pass through
|
|
61
|
+
// but real ops on them fail loudly. Mirrors the Dafny backend's
|
|
62
|
+
// `type Unknown(==, 0)`. The decl is emitted once, in the first file that
|
|
63
|
+
// needs it (the def file imports the types file, so no duplicate).
|
|
64
|
+
case "unknown":
|
|
65
|
+
_needsUnknown = true;
|
|
66
|
+
return "Unknown";
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
// ── Lean keyword escaping ────────────────────────────────────
|
|
@@ -83,8 +91,119 @@ const PREC = {
|
|
|
83
91
|
"+": 5, "-": 5, "++": 5, "arrayConcat": 5, "*": 6, "/": 6, "%": 6,
|
|
84
92
|
};
|
|
85
93
|
function prec(op) { return PREC[op] ?? 10; }
|
|
86
|
-
|
|
94
|
+
const _unionCtors = new Map();
|
|
95
|
+
// Types that transitively reference an `opaque` type can't derive `Repr` or
|
|
96
|
+
// `DecidableEq` (the opaque type provides neither). `Inhabited` still derives
|
|
97
|
+
// (via the empty array / first constructor), so only those two are dropped.
|
|
98
|
+
// "Unknown" (the `any`/`unknown` carrier) is opaque by construction.
|
|
99
|
+
const _opaqueNames = new Set(["Unknown"]);
|
|
100
|
+
const _typeRefs = new Map();
|
|
101
|
+
const _taintedTypes = new Set();
|
|
102
|
+
function collectUserRefs(ty, into) {
|
|
103
|
+
switch (ty.kind) {
|
|
104
|
+
case "array":
|
|
105
|
+
case "set":
|
|
106
|
+
collectUserRefs(ty.elem, into);
|
|
107
|
+
break;
|
|
108
|
+
case "optional":
|
|
109
|
+
collectUserRefs(ty.inner, into);
|
|
110
|
+
break;
|
|
111
|
+
case "map":
|
|
112
|
+
collectUserRefs(ty.key, into);
|
|
113
|
+
collectUserRefs(ty.value, into);
|
|
114
|
+
break;
|
|
115
|
+
case "fn":
|
|
116
|
+
ty.params.forEach(p => collectUserRefs(p, into));
|
|
117
|
+
collectUserRefs(ty.result, into);
|
|
118
|
+
break;
|
|
119
|
+
case "user":
|
|
120
|
+
into.add(ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name);
|
|
121
|
+
break;
|
|
122
|
+
case "unknown":
|
|
123
|
+
into.add("Unknown");
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function registerInductives(decls) {
|
|
128
|
+
for (const d of decls) {
|
|
129
|
+
if (d.kind === "inductive") {
|
|
130
|
+
_unionCtors.set(d.name, d.constructors);
|
|
131
|
+
const refs = new Set();
|
|
132
|
+
for (const c of d.constructors)
|
|
133
|
+
for (const f of c.fields)
|
|
134
|
+
collectUserRefs(f.type, refs);
|
|
135
|
+
_typeRefs.set(d.name, refs);
|
|
136
|
+
}
|
|
137
|
+
else if (d.kind === "structure") {
|
|
138
|
+
const refs = new Set();
|
|
139
|
+
for (const f of d.fields)
|
|
140
|
+
collectUserRefs(f.type, refs);
|
|
141
|
+
_typeRefs.set(d.name, refs);
|
|
142
|
+
}
|
|
143
|
+
else if (d.kind === "opaque-type") {
|
|
144
|
+
_opaqueNames.add(d.name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Fixpoint: a type is tainted if it references an opaque or already-tainted type.
|
|
148
|
+
let changed = true;
|
|
149
|
+
while (changed) {
|
|
150
|
+
changed = false;
|
|
151
|
+
for (const [name, refs] of _typeRefs) {
|
|
152
|
+
if (_taintedTypes.has(name))
|
|
153
|
+
continue;
|
|
154
|
+
if ([...refs].some(r => _opaqueNames.has(r) || _taintedTypes.has(r))) {
|
|
155
|
+
_taintedTypes.add(name);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Deriving clause, dropping `Repr`/`DecidableEq` for opaque-tainted types. */
|
|
162
|
+
function emitDeriving(name, deriving) {
|
|
163
|
+
const der = _taintedTypes.has(name) ? deriving.filter(x => x !== "Repr" && x !== "DecidableEq") : deriving;
|
|
164
|
+
return der.length > 0 ? `\nderiving ${der.join(", ")}` : "";
|
|
165
|
+
}
|
|
87
166
|
let _needsJSString = false;
|
|
167
|
+
let _needsUnknown = false;
|
|
168
|
+
let _unknownEmitted = false; // across files in one run — the def file imports the types file
|
|
169
|
+
// Bool-vs-Prop context. Lean keeps `Bool` and `Prop` distinct; the IR uses the
|
|
170
|
+
// Prop connectives (∧/∨/¬) uniformly. In a computational position the connectives
|
|
171
|
+
// may need to be the Bool ones (&&/||/!). They are only *required* when a connective
|
|
172
|
+
// has an operand that does not coerce Bool→Prop — see `needsBoolConnectives`. A body
|
|
173
|
+
// built only from decidable atoms (comparisons, Bool-returning calls) coerces fine
|
|
174
|
+
// and stays in the more proof-friendly Prop form.
|
|
175
|
+
let _boolCtx = false;
|
|
176
|
+
// A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
|
|
177
|
+
// (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
|
|
178
|
+
// `match` used as a Bool — neither has a `Decidable` instance Lean can synthesize
|
|
179
|
+
// through the `match`. Under a Prop connective (∧/∨/¬) such an operand is a type
|
|
180
|
+
// error, so its presence forces the whole body to Bool connectives.
|
|
181
|
+
function isNonCoercibleBoolAtom(e) {
|
|
182
|
+
if (e.kind === "match")
|
|
183
|
+
return true;
|
|
184
|
+
if (e.kind === "binop" && (e.op === "=" || e.op === "≠") && e.right.kind === "constructor") {
|
|
185
|
+
const rhs = e.right; // capture the narrowed node so it survives the closure below
|
|
186
|
+
const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
|
|
187
|
+
return !!ctor && ctor.fields.length > 0;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
// A ∧/∨/¬ connective with a non-coercible operand — the specific node that would
|
|
192
|
+
// fail to elaborate if emitted in Prop form.
|
|
193
|
+
function connectiveHasNonCoercibleOperand(e) {
|
|
194
|
+
if (e.kind === "binop" && (e.op === "∧" || e.op === "∨"))
|
|
195
|
+
return isNonCoercibleBoolAtom(e.left) || isNonCoercibleBoolAtom(e.right);
|
|
196
|
+
if (e.kind === "unop" && e.op === "¬")
|
|
197
|
+
return isNonCoercibleBoolAtom(e.expr);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
// True iff some ∧/∨/¬ connective in `e` has a non-coercible operand, i.e. emitting
|
|
201
|
+
// the body with Prop connectives would fail to elaborate. (A decidable atom coerces,
|
|
202
|
+
// so a body with no such operand stays Prop — this keeps arithmetic predicates like
|
|
203
|
+
// `x ≥ 0 ∧ x < n` in `∧` form rather than forcing `&&`.)
|
|
204
|
+
function needsBoolConnectives(e) {
|
|
205
|
+
return anyExpr(e, connectiveHasNonCoercibleOperand);
|
|
206
|
+
}
|
|
88
207
|
function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
89
208
|
// Array methods
|
|
90
209
|
if (tyKind === "array") {
|
|
@@ -100,10 +219,14 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
100
219
|
return args.length > 1 ? `(${obj}.extract ${args[1]} ${obj}.size).contains ${args[0]}` : `${obj}.contains ${args[0]}`;
|
|
101
220
|
if (method === "find")
|
|
102
221
|
return `${obj}.find? ${args[0]}`;
|
|
222
|
+
if (method === "join")
|
|
223
|
+
return `(String.intercalate ${args[0]} ${obj}.toList)`;
|
|
103
224
|
if (method === "with")
|
|
104
225
|
return `${obj}.set! ${args[0]} ${args[1]}`;
|
|
105
226
|
if (method === "push")
|
|
106
227
|
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
228
|
+
if (method === "unshift")
|
|
229
|
+
return `(#[${args.join(", ")}] ++ ${obj})`;
|
|
107
230
|
if (method === "concat")
|
|
108
231
|
return args.length === 1 ? `Array.push ${obj} ${args[0]}` : `${obj} ++ #[${args.join(", ")}]`;
|
|
109
232
|
// arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
|
|
@@ -162,7 +285,8 @@ function wrapOperand(sub, parentPrec) {
|
|
|
162
285
|
}
|
|
163
286
|
function emitExpr(e, parentPrec) {
|
|
164
287
|
switch (e.kind) {
|
|
165
|
-
|
|
288
|
+
// `undefined` is the IR's spelling of the absent optional (mirrors dafny-emit's None)
|
|
289
|
+
case "var": return e.name === "undefined" ? "none" : escapeName(e.name);
|
|
166
290
|
case "num": return `${e.value}`;
|
|
167
291
|
case "bool": return e.value ? "true" : "false";
|
|
168
292
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
@@ -183,6 +307,7 @@ function emitExpr(e, parentPrec) {
|
|
|
183
307
|
return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
|
|
184
308
|
case "emptyMap": return `Std.HashMap.empty`;
|
|
185
309
|
case "emptySet": return `Std.HashSet.empty`;
|
|
310
|
+
case "default": return `(default : ${tyToLean(e.type)})`;
|
|
186
311
|
case "methodCall": {
|
|
187
312
|
const obj = emitExpr(e.obj);
|
|
188
313
|
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall" || e.obj.kind === "if" || e.obj.kind === "let";
|
|
@@ -201,11 +326,25 @@ function emitExpr(e, parentPrec) {
|
|
|
201
326
|
}
|
|
202
327
|
case "unop":
|
|
203
328
|
if (e.op === "¬")
|
|
204
|
-
return `¬(${emitExpr(e.expr)})`;
|
|
329
|
+
return _boolCtx ? `!(${emitExpr(e.expr)})` : `¬(${emitExpr(e.expr)})`;
|
|
205
330
|
if (e.op === "-" && e.expr.kind === "num")
|
|
206
331
|
return `-${e.expr.value}`;
|
|
207
332
|
return `(-${emitExpr(e.expr)})`;
|
|
208
333
|
case "binop": {
|
|
334
|
+
// Discriminator test against a constructor that carries fields:
|
|
335
|
+
// `x = .Ctor` → `(match x with | .Ctor .. => true | _ => false)`. A
|
|
336
|
+
// multi-field constructor is a function, not a value, so a bare
|
|
337
|
+
// `x = Type.Ctor` is ill-typed. The Bool result coerces to Prop in spec
|
|
338
|
+
// positions, mirroring how the backend already treats decidable atoms.
|
|
339
|
+
// Nullary constructors (enums) keep the cheap `DecidableEq` comparison.
|
|
340
|
+
if ((e.op === "=" || e.op === "≠") && e.right.kind === "constructor") {
|
|
341
|
+
const rhs = e.right; // capture the narrowed node so it survives into the closure below
|
|
342
|
+
const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
|
|
343
|
+
if (ctor && ctor.fields.length > 0) {
|
|
344
|
+
const [yes, no] = e.op === "=" ? ["true", "false"] : ["false", "true"];
|
|
345
|
+
return `(match ${emitExpr(e.left)} with | .${escapeName(rhs.name)} .. => ${yes} | _ => ${no})`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
209
348
|
// `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
|
|
210
349
|
// native `in`; Lean uses the method form for HashMap/HashSet.
|
|
211
350
|
if (e.op === "in") {
|
|
@@ -213,7 +352,10 @@ function emitExpr(e, parentPrec) {
|
|
|
213
352
|
const wrap = e.right.kind === "binop" || e.right.kind === "app" || e.right.kind === "methodCall";
|
|
214
353
|
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
215
354
|
}
|
|
216
|
-
const op = e.op === "arrayConcat" ? "++"
|
|
355
|
+
const op = e.op === "arrayConcat" ? "++"
|
|
356
|
+
: _boolCtx && e.op === "∧" ? "&&"
|
|
357
|
+
: _boolCtx && e.op === "∨" ? "||"
|
|
358
|
+
: e.op;
|
|
217
359
|
// ↔ does not chain in Lean — a nested iff operand needs parens.
|
|
218
360
|
const childPrec = e.op === "↔" ? prec(e.op) + 1 : prec(e.op);
|
|
219
361
|
// `-`, `/`, `%` are left-associative and non-associative, so an equal-
|
|
@@ -241,6 +383,12 @@ function emitExpr(e, parentPrec) {
|
|
|
241
383
|
// the bare form, so its output is unaffected.)
|
|
242
384
|
if (e.ctorOf)
|
|
243
385
|
return args.length ? `${e.ctorOf}.${e.fn} ${args.join(" ")}` : `${e.ctorOf}.${e.fn}`;
|
|
386
|
+
// Option constructors arrive Dafny-spelled from transform (`app "Some"`);
|
|
387
|
+
// Lean core exports the lowercase forms as top-level names.
|
|
388
|
+
if (e.fn === "Some" && args.length === 1)
|
|
389
|
+
return `some ${args[0]}`;
|
|
390
|
+
if (e.fn === "None" && args.length === 0)
|
|
391
|
+
return `none`;
|
|
244
392
|
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
245
393
|
if (e.fn === "SetToSeq" && args.length === 1)
|
|
246
394
|
return `${args[0]}.toArray`;
|
|
@@ -263,6 +411,24 @@ function emitExpr(e, parentPrec) {
|
|
|
263
411
|
return `${e.fn} ${args.join(" ")}`;
|
|
264
412
|
}
|
|
265
413
|
case "field": {
|
|
414
|
+
// Union destructor (tagged by transform): `x.field` where x is a
|
|
415
|
+
// multi-constructor inductive. Lean has no projection there, so match the
|
|
416
|
+
// owning constructor, bind the field positionally, and ignore the rest.
|
|
417
|
+
// Other constructors fall to `default` — the source guards every such
|
|
418
|
+
// access with a discriminator test, so that branch is never reached.
|
|
419
|
+
if (e.fromUnion) {
|
|
420
|
+
const ctors = _unionCtors.get(e.fromUnion);
|
|
421
|
+
// Pin the owning ctor when given (field names repeat across variants);
|
|
422
|
+
// otherwise fall back to the sole variant carrying this field name.
|
|
423
|
+
const owner = (e.ctor ? ctors?.find(c => c.name === e.ctor) : undefined)
|
|
424
|
+
?? ctors?.find(c => c.fields.some(f => f.name === e.field));
|
|
425
|
+
if (owner) {
|
|
426
|
+
const idx = owner.fields.findIndex(f => f.name === e.field);
|
|
427
|
+
const pats = owner.fields.map((_, i) => (i === idx ? "_v" : "_")).join(" ");
|
|
428
|
+
const fty = tyToLean(owner.fields[idx].type);
|
|
429
|
+
return `(match ${emitExpr(e.obj)} with | .${escapeName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
266
432
|
const obj = emitExpr(e.obj);
|
|
267
433
|
if (e.field === "collectionSize")
|
|
268
434
|
return `${obj}.size`;
|
|
@@ -328,10 +494,19 @@ function emitStmts(stmts, indent) {
|
|
|
328
494
|
function emitStmt(s, indent) {
|
|
329
495
|
const pad = " ".repeat(indent);
|
|
330
496
|
switch (s.kind) {
|
|
331
|
-
case "let":
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
497
|
+
case "let": {
|
|
498
|
+
// `mut` lets always carry their type (assignments must re-elaborate at it).
|
|
499
|
+
// Immutable lets are ascribed only when the initializer is a `match` (the
|
|
500
|
+
// `??` / Option-unwrap lowerings): Velvet's WP elaboration runs *backwards*,
|
|
501
|
+
// so without the ascription the binding's type is a metavariable that a
|
|
502
|
+
// later use pins first — e.g. `lines.size ≤ maxLines` pins `maxLines : ℕ`
|
|
503
|
+
// and the ℤ-valued unwrap arms then fail to elaborate. Other initializer
|
|
504
|
+
// shapes determine their own type, and leaving them bare keeps previously
|
|
505
|
+
// generated (and proven) artifacts byte-stable.
|
|
506
|
+
const ascribe = s.mutable || s.value.kind === "match";
|
|
507
|
+
const ty = ascribe && s.type.kind !== "unknown" ? ` : ${tyToLean(s.type)}` : "";
|
|
508
|
+
return `${pad}let ${s.mutable ? "mut " : ""}${escapeName(s.name)}${ty} := ${emitExpr(s.value)}`;
|
|
509
|
+
}
|
|
335
510
|
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
336
511
|
case "ghostLet":
|
|
337
512
|
return `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`;
|
|
@@ -344,7 +519,10 @@ function emitStmt(s, indent) {
|
|
|
344
519
|
// to WPGen.default, which drops the assertion. Coerce to Prop via `= true`.
|
|
345
520
|
// Top-level Prop constructs (`=`, `<`, `∧`, `¬`, `∀`, `∃`, `→`) already
|
|
346
521
|
// land in Prop — Lean auto-coerces inner Bools there.
|
|
522
|
+
const prevBoolCtx = _boolCtx;
|
|
523
|
+
_boolCtx = false; // assertions are Prop
|
|
347
524
|
const inner = emitExpr(s.expr);
|
|
525
|
+
_boolCtx = prevBoolCtx;
|
|
348
526
|
const wrapped = isPropValued(s.expr) ? inner : `(${inner}) = true`;
|
|
349
527
|
return `${pad}assertGadget (${wrapped})`;
|
|
350
528
|
}
|
|
@@ -411,21 +589,30 @@ function emitStmt(s, indent) {
|
|
|
411
589
|
return lines.join("\n");
|
|
412
590
|
}
|
|
413
591
|
case "while": {
|
|
592
|
+
// Guard is computational (Bool); invariants / done_with are Prop.
|
|
414
593
|
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
594
|
+
const prevBoolCtx = _boolCtx;
|
|
595
|
+
_boolCtx = false;
|
|
415
596
|
for (const inv of s.invariants)
|
|
416
597
|
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
598
|
+
// `done_with True` (the auto-supplied fact for breaking loops) is a Prop;
|
|
599
|
+
// the bool literal would need a coercion, so emit the Prop `True` directly.
|
|
417
600
|
if (s.doneWith)
|
|
418
|
-
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
601
|
+
lines.push(`${pad} done_with ${s.doneWith.kind === "bool" && s.doneWith.value ? "True" : emitExpr(s.doneWith)}`);
|
|
419
602
|
if (s.decreasing)
|
|
420
603
|
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
604
|
+
_boolCtx = prevBoolCtx;
|
|
421
605
|
lines.push(`${pad}do`);
|
|
422
606
|
lines.push(emitStmts(s.body, indent + 1));
|
|
423
607
|
return lines.join("\n");
|
|
424
608
|
}
|
|
425
609
|
case "forin": {
|
|
426
610
|
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
611
|
+
const prevBoolCtx = _boolCtx;
|
|
612
|
+
_boolCtx = false; // invariants are Prop
|
|
427
613
|
for (const inv of s.invariants)
|
|
428
614
|
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
615
|
+
_boolCtx = prevBoolCtx;
|
|
429
616
|
lines.push(`${pad}do`);
|
|
430
617
|
lines.push(emitStmts(s.body, indent + 1));
|
|
431
618
|
return lines.join("\n");
|
|
@@ -446,17 +633,13 @@ function emitDecl(d) {
|
|
|
446
633
|
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
447
634
|
}
|
|
448
635
|
}
|
|
449
|
-
|
|
450
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
451
|
-
return lines.join("\n");
|
|
636
|
+
return lines.join("\n") + emitDeriving(d.name, d.deriving);
|
|
452
637
|
}
|
|
453
638
|
case "structure": {
|
|
454
639
|
const lines = [`structure ${d.name} where`];
|
|
455
640
|
for (const f of d.fields)
|
|
456
641
|
lines.push(` ${escapeName(f.name)} : ${tyToLean(f.type)}`);
|
|
457
|
-
|
|
458
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
459
|
-
return lines.join("\n");
|
|
642
|
+
return lines.join("\n") + emitDeriving(d.name, d.deriving);
|
|
460
643
|
}
|
|
461
644
|
case "type-alias": {
|
|
462
645
|
return `abbrev ${d.name} := ${tyToLean(d.target)}`;
|
|
@@ -467,7 +650,17 @@ function emitDecl(d) {
|
|
|
467
650
|
}
|
|
468
651
|
case "def": {
|
|
469
652
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
470
|
-
|
|
653
|
+
// A Bool-returning pure function is a computation, not a proposition, so its
|
|
654
|
+
// connectives *may* need the Bool operators (&&/||/!) — but only when a
|
|
655
|
+
// connective has a non-coercible operand (e.g. an inlined union discriminator).
|
|
656
|
+
// A predicate built from decidable atoms (`x ≥ 0 ∧ x < n`) coerces to Bool as
|
|
657
|
+
// a whole and stays in the more proof-friendly Prop form. Other return types
|
|
658
|
+
// only have connectives inside (Decidable) conditions, where Prop is fine.
|
|
659
|
+
const prevBoolCtx = _boolCtx;
|
|
660
|
+
_boolCtx = tyToLean(d.returnType) === "Bool" && needsBoolConnectives(d.body);
|
|
661
|
+
const body = emitPureExpr(d.body, 1);
|
|
662
|
+
_boolCtx = prevBoolCtx;
|
|
663
|
+
let out = `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${body}`;
|
|
471
664
|
// A `//@ decreases` on a pure function marks it recursive and names its
|
|
472
665
|
// termination measure — emit it as Lean's `termination_by`. This is
|
|
473
666
|
// required when the recursion is on `arr.slice(...)` (→ `Array.extract`,
|
|
@@ -484,13 +677,18 @@ function emitDecl(d) {
|
|
|
484
677
|
throw new Error("function by method is not supported for Lean backend");
|
|
485
678
|
case "method": {
|
|
486
679
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
680
|
+
// Spec clauses are Prop; the `do` body is computational (Bool).
|
|
681
|
+
const prevBoolCtx = _boolCtx;
|
|
682
|
+
_boolCtx = false;
|
|
487
683
|
const lines = [`method ${d.name} ${params} return (res : ${tyToLean(d.returnType)})`];
|
|
488
684
|
for (const r of d.requires)
|
|
489
685
|
lines.push(` require ${emitExpr(r)}`);
|
|
490
686
|
for (const e of d.ensures)
|
|
491
687
|
lines.push(` ensures ${emitExpr(e)}`);
|
|
492
688
|
lines.push(" do");
|
|
689
|
+
_boolCtx = true;
|
|
493
690
|
lines.push(emitStmts(d.body, 2));
|
|
691
|
+
_boolCtx = prevBoolCtx;
|
|
494
692
|
return lines.join("\n");
|
|
495
693
|
}
|
|
496
694
|
case "namespace": {
|
|
@@ -504,10 +702,25 @@ function emitDecl(d) {
|
|
|
504
702
|
throw new Error(`Lean class support not yet implemented: ${d.name}`);
|
|
505
703
|
case "const":
|
|
506
704
|
return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
|
|
507
|
-
case "extern":
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
|
|
705
|
+
case "extern": {
|
|
706
|
+
// Mirror Dafny's `function {:axiom}`: an uninterpreted total function.
|
|
707
|
+
// In Lean that is an `opaque` declaration (sound — it commits to no body,
|
|
708
|
+
// only to the type being inhabited). Any `requires`/`ensures` the source
|
|
709
|
+
// carried become a characterizing `axiom`; `\result` was already replaced
|
|
710
|
+
// by the call expression in the transform, so ensures reference `name args`.
|
|
711
|
+
const tp = d.typeParams.length > 0 ? ` {${d.typeParams.join(" ")} : Type}` : "";
|
|
712
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
713
|
+
const sig = `opaque ${escapeName(d.name)}${tp}${params ? ` ${params}` : ""} : ${tyToLean(d.returnType)}`;
|
|
714
|
+
if (d.requires.length === 0 && d.ensures.length === 0)
|
|
715
|
+
return sig;
|
|
716
|
+
// Spec axiom: ∀ params, req1 → … → (ens1 ∧ … ∧ ensN). Tagged `@[grind]` so
|
|
717
|
+
// the proof automation can use it, matching the ghost-function convention.
|
|
718
|
+
const hyps = d.requires.map(emitExpr);
|
|
719
|
+
const concl = d.ensures.map(emitExpr).join(" ∧ ");
|
|
720
|
+
const axBody = [...hyps, concl].join(" → ");
|
|
721
|
+
const axiom = `@[grind] axiom ${escapeName(d.name)}_spec${params ? ` ${params}` : ""} : ${axBody}`;
|
|
722
|
+
return `${sig}\n${axiom}`;
|
|
723
|
+
}
|
|
511
724
|
}
|
|
512
725
|
}
|
|
513
726
|
/** Emit a pure expression with indented if/match blocks. */
|
|
@@ -533,12 +746,19 @@ function emitPureExpr(e, indent) {
|
|
|
533
746
|
// ── File emission ────────────────────────────────────────────
|
|
534
747
|
export function emitLeanFile(file) {
|
|
535
748
|
_needsJSString = false;
|
|
536
|
-
|
|
749
|
+
_needsUnknown = false;
|
|
750
|
+
registerInductives(file.decls);
|
|
751
|
+
// Emit declarations first so _needsJSString / _needsUnknown are set
|
|
537
752
|
const declLines = [];
|
|
538
753
|
for (const decl of file.decls) {
|
|
539
754
|
declLines.push("");
|
|
540
755
|
declLines.push(emitDecl(decl));
|
|
541
756
|
}
|
|
757
|
+
// The `unknown` carrier must precede every use (Lean is definition-before-use).
|
|
758
|
+
if (_needsUnknown && !_unknownEmitted) {
|
|
759
|
+
declLines.unshift("", "/-- Opaque carrier for `unknown`-typed values (mirrors Dafny's `type Unknown(==, 0)`). -/", "opaque Unknown : Type");
|
|
760
|
+
_unknownEmitted = true;
|
|
761
|
+
}
|
|
542
762
|
const lines = [];
|
|
543
763
|
if (file.comment) {
|
|
544
764
|
lines.push("/-");
|
package/tools/dist/lsc.js
CHANGED
|
@@ -82,6 +82,13 @@ function main() {
|
|
|
82
82
|
}
|
|
83
83
|
// File-level directives consumed by the Dafny emitter.
|
|
84
84
|
const safeSlice = /\/\/@ safe-slice\b/.test(fullText);
|
|
85
|
+
// `//@ lean-module <name>` overrides the Lean module base (default: file
|
|
86
|
+
// basename). Lean module names are flat/global, so two identically-named
|
|
87
|
+
// `.ts` files (e.g. an in-place fork's duplicated `compaction.ts`) would emit
|
|
88
|
+
// colliding `foo.types`/`foo.def` modules; this gives one a distinct base so
|
|
89
|
+
// both can be separate Lean libraries. Lean-only — Dafny is unaffected.
|
|
90
|
+
const leanModuleDirective = fullText.match(/\/\/@ lean-module ([A-Za-z0-9_.\-]+)/);
|
|
91
|
+
const leanModuleOverride = leanModuleDirective ? leanModuleDirective[1] : undefined;
|
|
85
92
|
// Extract: ts-morph → Raw IR
|
|
86
93
|
const raw = extractModule(sourceFile);
|
|
87
94
|
if (cmd === "extract") {
|
|
@@ -140,15 +147,16 @@ function main() {
|
|
|
140
147
|
process.exit(1);
|
|
141
148
|
}
|
|
142
149
|
// ── Lean backend ──────────────────────────────────────────
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
150
|
+
const leanBase = leanModuleOverride ?? base;
|
|
151
|
+
const specPath = path.join(dir, `${leanBase}.spec.lean`);
|
|
152
|
+
const specImport = existsSync(specPath) ? `«${leanBase}.spec»` : undefined;
|
|
153
|
+
let { typesFile, defFile } = transformModuleLean(typed, specImport, leanModuleOverride);
|
|
146
154
|
if (typesFile)
|
|
147
155
|
typesFile = peepholeModule(typesFile, "lean");
|
|
148
156
|
defFile = peepholeModule(defFile, "lean");
|
|
149
|
-
const typesPath = typesFile ? path.join(dir, `${
|
|
157
|
+
const typesPath = typesFile ? path.join(dir, `${leanBase}.types.lean`) : null;
|
|
150
158
|
const typesText = typesFile ? emitLeanFile(typesFile) : null;
|
|
151
|
-
const defPath = path.join(dir, `${
|
|
159
|
+
const defPath = path.join(dir, `${leanBase}.def.lean`);
|
|
152
160
|
const defText = emitLeanFile(defFile);
|
|
153
161
|
if (cmd === "gen") {
|
|
154
162
|
leanGen(typesPath, defPath, typesText, defText);
|
|
@@ -156,7 +164,7 @@ function main() {
|
|
|
156
164
|
}
|
|
157
165
|
if (cmd === "check") {
|
|
158
166
|
leanGen(typesPath, defPath, typesText, defText);
|
|
159
|
-
if (!leanCheck(dir,
|
|
167
|
+
if (!leanCheck(dir, leanBase))
|
|
160
168
|
process.exit(1);
|
|
161
169
|
return;
|
|
162
170
|
}
|
package/tools/dist/narrow.js
CHANGED
|
@@ -144,7 +144,7 @@ function walkStmt(s) {
|
|
|
144
144
|
// array rule fires; independent narrows commute, so the order is harmless.)
|
|
145
145
|
// && rules fire before the simple rule because they produce nested ifs whose
|
|
146
146
|
// inner shape doesn't match the simple rule directly.
|
|
147
|
-
return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? r;
|
|
147
|
+
return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r) ?? r;
|
|
148
148
|
}
|
|
149
149
|
function walkStmts(stmts) {
|
|
150
150
|
const result = [];
|
|
@@ -653,6 +653,38 @@ function ruleIfAndOptional(s) {
|
|
|
653
653
|
noneBody: [],
|
|
654
654
|
};
|
|
655
655
|
}
|
|
656
|
+
/** Rule: a bare expression statement `x !== undefined && rest` (the `if`-less
|
|
657
|
+
* guard idiom, TS-equivalent to `if (x !== undefined) rest;`) where `x` is a
|
|
658
|
+
* pure access path.
|
|
659
|
+
* → `someMatch x { Some(_x_val) => rest;, None => {} }`.
|
|
660
|
+
* Runs `rest` for effect inside the narrowed scope. Wrapping `rest` as an
|
|
661
|
+
* expr-statement and walking it lets chained checks
|
|
662
|
+
* (`a !== undefined && a.b !== undefined && a.b.f()`) nest into someMatches.
|
|
663
|
+
* Unlike the ternary rule (`ruleConditionalAndOptional`), a method call in
|
|
664
|
+
* `rest` is fine here: a statement-level someMatch arm keeps it in statement
|
|
665
|
+
* position, so transform never ANF-lifts it out of the arm (which would drop
|
|
666
|
+
* the guard and reference the un-narrowed optional). */
|
|
667
|
+
function ruleExprStmtAndOptional(s) {
|
|
668
|
+
if (s.kind !== "expr")
|
|
669
|
+
return null;
|
|
670
|
+
if (s.expr.kind !== "binop" || s.expr.op !== "&&")
|
|
671
|
+
return null;
|
|
672
|
+
const extracted = extractLeftmostOptionalCheck(s.expr);
|
|
673
|
+
if (!extracted)
|
|
674
|
+
return null;
|
|
675
|
+
const { check, restCond } = extracted;
|
|
676
|
+
const innerStmt = { kind: "expr", expr: restCond };
|
|
677
|
+
const someBody = canBeFalsy(check)
|
|
678
|
+
? [{ kind: "if", cond: bound(check), then: [walkStmt(innerStmt)], else: [] }]
|
|
679
|
+
: [walkStmt(innerStmt)];
|
|
680
|
+
return {
|
|
681
|
+
kind: "someMatch",
|
|
682
|
+
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
683
|
+
binder: check.binderHint,
|
|
684
|
+
someBody,
|
|
685
|
+
noneBody: [],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
656
688
|
// ── Discriminant narrowing ──────────────────────────────────
|
|
657
689
|
/** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
|
|
658
690
|
* field accesses rooted at a var, and the path's type is a synthesized
|
package/tools/dist/peephole.js
CHANGED
|
@@ -13,6 +13,7 @@ function mapExpr(e, f) {
|
|
|
13
13
|
case "emptyMap":
|
|
14
14
|
case "emptySet":
|
|
15
15
|
case "havoc":
|
|
16
|
+
case "default":
|
|
16
17
|
case "mapLiteral": return e;
|
|
17
18
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
18
19
|
case "unop": return { ...e, expr: r(e.expr) };
|
|
@@ -358,6 +359,7 @@ function rewriteChildrenExpr(e) {
|
|
|
358
359
|
case "emptyMap":
|
|
359
360
|
case "emptySet":
|
|
360
361
|
case "havoc":
|
|
362
|
+
case "default":
|
|
361
363
|
case "mapLiteral": return e;
|
|
362
364
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
363
365
|
case "unop": return { ...e, expr: r(e.expr) };
|
package/tools/dist/transform.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Consumes resolved types and classifications.
|
|
5
5
|
* No type lookups, no string parsing, no re-inference.
|
|
6
6
|
*/
|
|
7
|
+
import { anyExprInStmts } from "./ir.js";
|
|
7
8
|
import { parseTsType } from "./types.js";
|
|
8
9
|
// ── Generic IR walkers ──────────────────────────────────────
|
|
9
10
|
/**
|
|
@@ -23,7 +24,8 @@ function mapExpr(e, f) {
|
|
|
23
24
|
case "str":
|
|
24
25
|
case "emptyMap":
|
|
25
26
|
case "emptySet":
|
|
26
|
-
case "havoc":
|
|
27
|
+
case "havoc":
|
|
28
|
+
case "default": return e;
|
|
27
29
|
case "mapLiteral": return { ...e, entries: e.entries.map(en => ({ key: r(en.key), value: r(en.value) })) };
|
|
28
30
|
case "constructor": return e.args ? { ...e, args: e.args.map(r) } : e;
|
|
29
31
|
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
@@ -630,6 +632,17 @@ function lowerExpr(e, binds) {
|
|
|
630
632
|
return { kind: "field", obj: transformExpr(e.obj), field: "true_?" };
|
|
631
633
|
}
|
|
632
634
|
}
|
|
635
|
+
// Union destructor: `x.field` where x is a discriminated union and `field`
|
|
636
|
+
// is a data field of one of its variants. Dafny reads the destructor
|
|
637
|
+
// directly; Lean has no field projection on a multi-ctor inductive, so tag
|
|
638
|
+
// the node with the union's base name and let the Lean emitter `match`.
|
|
639
|
+
if (e.obj.ty.kind === "user") {
|
|
640
|
+
const baseName = e.obj.ty.name.includes("<") ? e.obj.ty.name.slice(0, e.obj.ty.name.indexOf("<")) : e.obj.ty.name;
|
|
641
|
+
const decl = _typeDecls.find(d => d.name === baseName && d.kind === "discriminated-union");
|
|
642
|
+
if (decl?.variants?.some(v => v.fields.some(f => f.name === e.field))) {
|
|
643
|
+
return { kind: "field", obj: transformExpr(e.obj), field: e.field, fromUnion: baseName };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
633
646
|
return { kind: "field", obj: transformExpr(e.obj), field: e.field };
|
|
634
647
|
case "index": {
|
|
635
648
|
const idx = transformExpr(e.idx);
|
|
@@ -1106,6 +1119,188 @@ function buildPopLowering(arrName, arrTy) {
|
|
|
1106
1119
|
const guardedTrunc = { kind: "if", cond: isNonEmpty, then: truncated, else: arrVar };
|
|
1107
1120
|
return { optValue, guardedTrunc };
|
|
1108
1121
|
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Velvet (Lean backend) rejects `return` inside a loop. For a function shaped as
|
|
1124
|
+
* `…; while (…) { … early returns … }; return fallthrough`, hoist a mutable
|
|
1125
|
+
* result variable: each in-loop `return e` becomes `_loopRet := e; break`, then
|
|
1126
|
+
* `return _loopRet` after the loop. The trailing fallthrough return seeds
|
|
1127
|
+
* `_loopRet`, so a normal loop exit returns it unchanged. Dafny is untouched —
|
|
1128
|
+
* it keeps the native early returns.
|
|
1129
|
+
*/
|
|
1130
|
+
function stmtsContainReturn(stmts) {
|
|
1131
|
+
for (const s of stmts) {
|
|
1132
|
+
if (s.kind === "return")
|
|
1133
|
+
return true;
|
|
1134
|
+
if (s.kind === "if" && (stmtsContainReturn(s.then) || stmtsContainReturn(s.else)))
|
|
1135
|
+
return true;
|
|
1136
|
+
if (s.kind === "match" && s.arms.some(a => stmtsContainReturn(a.body)))
|
|
1137
|
+
return true;
|
|
1138
|
+
// Don't descend into a nested while — its returns would target that loop.
|
|
1139
|
+
}
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
function replaceReturnsWithBreak(stmts, retVar) {
|
|
1143
|
+
return stmts.flatMap((s) => {
|
|
1144
|
+
if (s.kind === "return")
|
|
1145
|
+
return [{ kind: "assign", target: retVar, value: s.value }, { kind: "break" }];
|
|
1146
|
+
if (s.kind === "if")
|
|
1147
|
+
return [{ ...s, then: replaceReturnsWithBreak(s.then, retVar), else: replaceReturnsWithBreak(s.else, retVar) }];
|
|
1148
|
+
if (s.kind === "match")
|
|
1149
|
+
return [{ ...s, arms: s.arms.map(a => ({ ...a, body: replaceReturnsWithBreak(a.body, retVar) })) }];
|
|
1150
|
+
return [s];
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
// Seed value for `_loopRet` when a return-in-loop function has no trailing
|
|
1154
|
+
// fallthrough return. Readable literals for the primitives; everything else
|
|
1155
|
+
// (user types, arrays, maps, optionals) gets a typed `default : T` rather than a
|
|
1156
|
+
// type-incorrect `0` — these types derive `Inhabited`, so the default exists.
|
|
1157
|
+
function defaultExprForTy(ty) {
|
|
1158
|
+
switch (ty.kind) {
|
|
1159
|
+
case "bool": return { kind: "bool", value: false };
|
|
1160
|
+
case "string": return { kind: "str", value: "" };
|
|
1161
|
+
case "nat":
|
|
1162
|
+
case "int":
|
|
1163
|
+
case "real": return { kind: "num", value: 0 };
|
|
1164
|
+
default: return { kind: "default", type: ty };
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function eliminateReturnInLoops(stmts, retTy, resultInvariants) {
|
|
1168
|
+
const out = [];
|
|
1169
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
1170
|
+
const s = stmts[i];
|
|
1171
|
+
if (s.kind === "while" && stmtsContainReturn(s.body)) {
|
|
1172
|
+
const retVar = "_loopRet";
|
|
1173
|
+
const next = stmts[i + 1];
|
|
1174
|
+
const init = next && next.kind === "return" ? next.value : defaultExprForTy(retTy);
|
|
1175
|
+
out.push({ kind: "let", name: retVar, type: retTy, mutable: true, value: init });
|
|
1176
|
+
// The result variable carries the postcondition (the function's `ensures`
|
|
1177
|
+
// with `\result` → `_loopRet`) as a loop invariant: it holds initially (the
|
|
1178
|
+
// fallthrough seed) and after each `_loopRet := e; break`, so the
|
|
1179
|
+
// postcondition is re-established from the invariant when the loop exits.
|
|
1180
|
+
// Where Dafny discharged the postcondition at each `return` site, the
|
|
1181
|
+
// break-rewrite discharges it once, after the loop.
|
|
1182
|
+
out.push({ ...s, invariants: [...s.invariants, ...resultInvariants], body: replaceReturnsWithBreak(s.body, retVar) });
|
|
1183
|
+
out.push({ kind: "return", value: { kind: "var", name: retVar } });
|
|
1184
|
+
if (next && next.kind === "return")
|
|
1185
|
+
i++; // consume the seeded fallthrough return
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
out.push(s);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
/** True if `name` is referenced as a variable anywhere in `stmts`. Conservative:
|
|
1194
|
+
* counts every occurrence and ignores shadowing — a spurious hit only costs an
|
|
1195
|
+
* unused `let`, whereas a miss would leave a binder unbound. */
|
|
1196
|
+
function stmtsUseVar(stmts, name) {
|
|
1197
|
+
return anyExprInStmts(stmts, e => e.kind === "var" && e.name === name);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Velvet (Lean backend) cannot synthesize a `WPGen` for a monadic statement
|
|
1201
|
+
* `match` on a user inductive in a method body (loom's matcher-WPGen drops a
|
|
1202
|
+
* `sorry`). loom *can* handle method-body `if`s (cf. examples/arrayEquals), so
|
|
1203
|
+
* lower such matches to discriminator `if`-chains: `match x with | .C f.. => B`
|
|
1204
|
+
* becomes `if x.C? then (let f := x.f-destructor; B) else …`. Constructor field
|
|
1205
|
+
* binders become `let`s bound to the (already-supported) union destructor. Option
|
|
1206
|
+
* matches and other non-user matches are left alone (loom handles those). Dafny
|
|
1207
|
+
* is untouched — it keeps the native match.
|
|
1208
|
+
*/
|
|
1209
|
+
function matchToIfChains(stmts) {
|
|
1210
|
+
return stmts.flatMap((s) => {
|
|
1211
|
+
if (s.kind === "if")
|
|
1212
|
+
return [{ ...s, then: matchToIfChains(s.then), else: matchToIfChains(s.else) }];
|
|
1213
|
+
if (s.kind === "while")
|
|
1214
|
+
return [{ ...s, body: matchToIfChains(s.body) }];
|
|
1215
|
+
if (s.kind === "forin")
|
|
1216
|
+
return [{ ...s, body: matchToIfChains(s.body) }];
|
|
1217
|
+
if (s.kind !== "match")
|
|
1218
|
+
return [s];
|
|
1219
|
+
const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
|
|
1220
|
+
const ctorArms = arms.filter(a => a.pattern.trim() !== "_");
|
|
1221
|
+
const firstCtor = ctorArms[0]?.pattern.trim().split(/\s+/)[0].replace(/^\./, "");
|
|
1222
|
+
const decl = firstCtor
|
|
1223
|
+
? _typeDecls.find(d => (d.kind === "discriminated-union" || d.kind === "string-union") &&
|
|
1224
|
+
((d.variants?.some(v => v.name === firstCtor)) || (d.values?.includes(firstCtor))))
|
|
1225
|
+
: undefined;
|
|
1226
|
+
if (!decl)
|
|
1227
|
+
return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
|
|
1228
|
+
const scrutExpr = typeof s.scrutinee === "string" ? { kind: "var", name: s.scrutinee } : s.scrutinee;
|
|
1229
|
+
const defaultArm = arms.find(a => a.pattern.trim() === "_");
|
|
1230
|
+
let elseBranch = defaultArm ? defaultArm.body : [];
|
|
1231
|
+
for (let k = ctorArms.length - 1; k >= 0; k--) {
|
|
1232
|
+
const armBody = ctorArms[k].body;
|
|
1233
|
+
if (armBody.length === 0)
|
|
1234
|
+
continue; // empty arm (no-op) — let it fall through to `else`
|
|
1235
|
+
const toks = ctorArms[k].pattern.trim().split(/\s+/);
|
|
1236
|
+
const ctor = toks[0].replace(/^\./, "");
|
|
1237
|
+
const binders = toks.slice(1);
|
|
1238
|
+
const variant = decl.variants?.find(v => v.name === ctor);
|
|
1239
|
+
// Discriminator condition. A nullary discriminated-union constructor would
|
|
1240
|
+
// need `DecidableEq` for `x = .Ctor` (which such unions don't derive), so
|
|
1241
|
+
// use a match-bool instead. Multi-field ctors already lower to a match-bool;
|
|
1242
|
+
// string-unions derive DecidableEq, so `=` is fine there.
|
|
1243
|
+
const cond = decl.kind === "discriminated-union" && binders.length === 0
|
|
1244
|
+
? { kind: "match", scrutinee: scrutExpr, arms: [
|
|
1245
|
+
{ pattern: `.${ctor}`, body: { kind: "bool", value: true } },
|
|
1246
|
+
{ pattern: "_", body: { kind: "bool", value: false } }
|
|
1247
|
+
] }
|
|
1248
|
+
: { kind: "binop", op: "=", left: scrutExpr, right: { kind: "constructor", name: ctor, type: decl.name } };
|
|
1249
|
+
// Bind only the constructor-field binders the body actually uses, pinning the
|
|
1250
|
+
// owning ctor so the destructor doesn't guess (variants share field names).
|
|
1251
|
+
const lets = [];
|
|
1252
|
+
binders.forEach((b, i) => {
|
|
1253
|
+
const f = variant?.fields[i];
|
|
1254
|
+
if (b !== "_" && f && stmtsUseVar(armBody, b))
|
|
1255
|
+
lets.push({
|
|
1256
|
+
kind: "let", name: b, type: f.type ?? { kind: "unknown" }, mutable: false,
|
|
1257
|
+
value: { kind: "field", obj: scrutExpr, field: f.name, fromUnion: decl.name, ctor },
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
elseBranch = [{ kind: "if", cond, then: [...lets, ...armBody], else: elseBranch }];
|
|
1261
|
+
}
|
|
1262
|
+
return elseBranch;
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
/** True if `stmts` contain a `break` not nested inside another while. */
|
|
1266
|
+
function stmtsContainBreak(stmts) {
|
|
1267
|
+
for (const s of stmts) {
|
|
1268
|
+
if (s.kind === "break")
|
|
1269
|
+
return true;
|
|
1270
|
+
if (s.kind === "if" && (stmtsContainBreak(s.then) || stmtsContainBreak(s.else)))
|
|
1271
|
+
return true;
|
|
1272
|
+
if (s.kind === "match" && s.arms.some(a => stmtsContainBreak(a.body)))
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* loom's default loop-exit fact is `¬guard`, which does not hold for a loop that
|
|
1279
|
+
* `break`s (a break exits with the guard still true). Such loops need an explicit
|
|
1280
|
+
* `//@ done_with` in the TS source (`//@ done_with true` when the loop invariant
|
|
1281
|
+
* alone carries the exit facts). Rather than silently supplying one, reject —
|
|
1282
|
+
* the exit fact is part of the spec and belongs beside the invariants.
|
|
1283
|
+
* Lean-only; Dafny derives loop-exit facts from the break sites themselves.
|
|
1284
|
+
*/
|
|
1285
|
+
function requireDoneWithForBreaks(stmts, fnName) {
|
|
1286
|
+
for (const s of stmts) {
|
|
1287
|
+
if (s.kind === "if") {
|
|
1288
|
+
requireDoneWithForBreaks(s.then, fnName);
|
|
1289
|
+
requireDoneWithForBreaks(s.else, fnName);
|
|
1290
|
+
}
|
|
1291
|
+
if (s.kind === "match")
|
|
1292
|
+
for (const a of s.arms)
|
|
1293
|
+
requireDoneWithForBreaks(a.body, fnName);
|
|
1294
|
+
if (s.kind === "while") {
|
|
1295
|
+
if (!s.doneWith && stmtsContainBreak(s.body)) {
|
|
1296
|
+
throw new Error(`${fnName}: loop with break needs //@ done_with on the Lean backend ` +
|
|
1297
|
+
`(an early return in a loop also lowers to a break). ` +
|
|
1298
|
+
`Use //@ done_with true when the loop invariants carry the exit facts.`);
|
|
1299
|
+
}
|
|
1300
|
+
requireDoneWithForBreaks(s.body, fnName);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1109
1304
|
function eliminateTopLevelContinue(stmts) {
|
|
1110
1305
|
const out = [];
|
|
1111
1306
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -1914,11 +2109,11 @@ function replaceVar(e, name, replacement, narrowing) {
|
|
|
1914
2109
|
}
|
|
1915
2110
|
// ── Top-level transform ──────────────────────────────────────
|
|
1916
2111
|
/** Transform for Lean backend — same logic, Lean options. */
|
|
1917
|
-
export function transformModuleLean(mod, specImport) {
|
|
2112
|
+
export function transformModuleLean(mod, specImport, moduleBase) {
|
|
1918
2113
|
const prev = _opts;
|
|
1919
2114
|
_opts = LEAN_OPTIONS;
|
|
1920
2115
|
try {
|
|
1921
|
-
return transformModule(mod, specImport);
|
|
2116
|
+
return transformModule(mod, specImport, moduleBase);
|
|
1922
2117
|
}
|
|
1923
2118
|
finally {
|
|
1924
2119
|
_opts = prev;
|
|
@@ -1935,7 +2130,7 @@ export function transformModuleDafny(mod) {
|
|
|
1935
2130
|
_opts = prev;
|
|
1936
2131
|
}
|
|
1937
2132
|
}
|
|
1938
|
-
export function transformModule(mod, specImport) {
|
|
2133
|
+
export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
1939
2134
|
_forofCounters.clear();
|
|
1940
2135
|
_liftCounter = 0;
|
|
1941
2136
|
_typeDecls = mod.typeDecls;
|
|
@@ -1988,6 +2183,9 @@ export function transformModule(mod, specImport) {
|
|
|
1988
2183
|
}
|
|
1989
2184
|
}
|
|
1990
2185
|
const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
|
|
2186
|
+
// Lean module base — overridable via `//@ lean-module` (see lsc.ts). Only the
|
|
2187
|
+
// def→types import below reads it; Dafny never passes an override.
|
|
2188
|
+
const moduleBase = moduleBaseOverride ?? base;
|
|
1991
2189
|
// Externs: emit as top-of-file `function {:axiom}` (Dafny) declarations.
|
|
1992
2190
|
// Any `requires`/`ensures` from the source declaration come along so callers
|
|
1993
2191
|
// see the same spec the source itself verified. Substitute `\result` with the
|
|
@@ -2011,12 +2209,18 @@ export function transformModule(mod, specImport) {
|
|
|
2011
2209
|
? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
|
|
2012
2210
|
: [];
|
|
2013
2211
|
if (typeDecls.length > 0 || pureDefs.length > 0 || externDecls.length > 0) {
|
|
2212
|
+
// Declaration order differs by backend. Dafny allows forward references, so
|
|
2213
|
+
// externs go first to be in scope everywhere. Lean requires definition-before-use:
|
|
2214
|
+
// an extern's signature may reference a declared type (e.g. `estimateTokens(m: AgentMessage)`),
|
|
2215
|
+
// so types must precede externs, which in turn precede the pure mirrors that may call them.
|
|
2216
|
+
const decls = _opts.backend === "lean"
|
|
2217
|
+
? [...typeDecls, ...externDecls, ...pureNamespace]
|
|
2218
|
+
: [...externDecls, ...typeDecls, ...pureNamespace];
|
|
2014
2219
|
typesFile = {
|
|
2015
2220
|
comment: " Generated by lsc — Lean types and pure function mirrors.",
|
|
2016
2221
|
imports: typesImports,
|
|
2017
2222
|
options: [],
|
|
2018
|
-
|
|
2019
|
-
decls: [...externDecls, ...typeDecls, ...pureNamespace],
|
|
2223
|
+
decls,
|
|
2020
2224
|
};
|
|
2021
2225
|
}
|
|
2022
2226
|
// Def file: Velvet methods
|
|
@@ -2036,6 +2240,19 @@ export function transformModule(mod, specImport) {
|
|
|
2036
2240
|
let body = pureDefNames.has(fn.name)
|
|
2037
2241
|
? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
|
|
2038
2242
|
: promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
|
|
2243
|
+
// Lean-only method-body rewrites (Velvet can't WP-synthesize monadic matches
|
|
2244
|
+
// and forbids `return` in loops):
|
|
2245
|
+
// 1. monadic statement-`match` on a user union → discriminator `if`-chains
|
|
2246
|
+
// 2. `return` inside a loop → result var + break (postcondition as invariant)
|
|
2247
|
+
// Dafny keeps the native forms. Breaking loops (including those synthesized
|
|
2248
|
+
// by rewrite 2) must carry a `//@ done_with` in the TS source — enforced here,
|
|
2249
|
+
// since loom's default loop-exit fact `¬guard` does not hold across a break.
|
|
2250
|
+
if (_opts.backend === "lean" && !pureDefNames.has(fn.name)) {
|
|
2251
|
+
body = matchToIfChains(body);
|
|
2252
|
+
const resultInvariants = fn.ensures.map(e => replaceVar(transformExpr(e), "\\result", { kind: "var", name: "_loopRet" }));
|
|
2253
|
+
body = eliminateReturnInLoops(body, fn.returnTy, resultInvariants);
|
|
2254
|
+
requireDoneWithForBreaks(body, fn.name);
|
|
2255
|
+
}
|
|
2039
2256
|
// Shadow reassigned parameters with mutable locals
|
|
2040
2257
|
const paramNames = new Set(fn.params.map(p => p.name));
|
|
2041
2258
|
const reassigned = findReassignedNames(fn.body, paramNames);
|
|
@@ -2082,7 +2299,7 @@ export function transformModule(mod, specImport) {
|
|
|
2082
2299
|
methods: classMethods,
|
|
2083
2300
|
};
|
|
2084
2301
|
});
|
|
2085
|
-
const defImport = specImport ?? (typesFile ? `«${
|
|
2302
|
+
const defImport = specImport ?? (typesFile ? `«${moduleBase}.types»` : null);
|
|
2086
2303
|
const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
|
|
2087
2304
|
const defFile = {
|
|
2088
2305
|
comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
|
package/tools/dist/emit.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lean IR → text. Trivial pretty-printer.
|
|
3
|
-
* No logic, no type decisions — just serialization.
|
|
4
|
-
*/
|
|
5
|
-
// ── Lean keyword escaping ────────────────────────────────────
|
|
6
|
-
const LEAN_KEYWORDS = new Set([
|
|
7
|
-
"def", "theorem", "lemma", "example", "structure", "class", "instance",
|
|
8
|
-
"inductive", "where", "match", "with", "if", "then", "else", "do",
|
|
9
|
-
"let", "mut", "return", "for", "in", "while", "break", "continue",
|
|
10
|
-
"import", "open", "section", "namespace", "end", "set_option",
|
|
11
|
-
"variable", "axiom", "constant", "private", "protected", "noncomputable",
|
|
12
|
-
"partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
|
|
13
|
-
"at", "from", "to", "deriving", "extends", "true", "false",
|
|
14
|
-
]);
|
|
15
|
-
function escapeName(name) {
|
|
16
|
-
return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
|
|
17
|
-
}
|
|
18
|
-
// ── Operator precedence (for parenthesization) ──────────────
|
|
19
|
-
const PREC = {
|
|
20
|
-
"→": 1, "∨": 2, "∧": 3,
|
|
21
|
-
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
22
|
-
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
|
|
23
|
-
};
|
|
24
|
-
function prec(op) { return PREC[op] ?? 10; }
|
|
25
|
-
// ── Expression emission ─────────────────────────────────────
|
|
26
|
-
function emitExpr(e, parentPrec) {
|
|
27
|
-
switch (e.kind) {
|
|
28
|
-
case "var": return escapeName(e.name);
|
|
29
|
-
case "num": return `${e.value}`;
|
|
30
|
-
case "bool": return e.value ? "true" : "false";
|
|
31
|
-
case "str": return `"${e.value}"`;
|
|
32
|
-
case "constructor": return `.${e.name}`;
|
|
33
|
-
case "arrayLiteral":
|
|
34
|
-
if (e.elems.length === 0)
|
|
35
|
-
return `#[]`;
|
|
36
|
-
return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
|
|
37
|
-
case "dotCall": {
|
|
38
|
-
const obj = emitExpr(e.obj);
|
|
39
|
-
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "dotCall";
|
|
40
|
-
const receiver = wrap ? `(${obj})` : obj;
|
|
41
|
-
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
42
|
-
return args.length > 0 ? `${receiver}.${e.method} ${args.join(" ")}` : `${receiver}.${e.method}`;
|
|
43
|
-
}
|
|
44
|
-
case "lambda": {
|
|
45
|
-
const params = e.params.map(p => p.name).join(" ");
|
|
46
|
-
// Single return statement → expression lambda
|
|
47
|
-
if (e.body.length === 1 && e.body[0].kind === "return") {
|
|
48
|
-
return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
|
|
49
|
-
}
|
|
50
|
-
// Multi-statement → do block
|
|
51
|
-
return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
|
|
52
|
-
}
|
|
53
|
-
case "unop":
|
|
54
|
-
if (e.op === "¬")
|
|
55
|
-
return `¬(${emitExpr(e.expr)})`;
|
|
56
|
-
if (e.op === "-" && e.expr.kind === "num")
|
|
57
|
-
return `-${e.expr.value}`;
|
|
58
|
-
return `(-${emitExpr(e.expr)})`;
|
|
59
|
-
case "binop": {
|
|
60
|
-
const s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
|
|
61
|
-
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
62
|
-
}
|
|
63
|
-
case "implies": {
|
|
64
|
-
const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
|
|
65
|
-
const s = parts.join(" → ");
|
|
66
|
-
return parentPrec !== undefined ? `(${s})` : s;
|
|
67
|
-
}
|
|
68
|
-
case "app": {
|
|
69
|
-
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
70
|
-
return `${e.fn} ${args.join(" ")}`;
|
|
71
|
-
}
|
|
72
|
-
case "field": {
|
|
73
|
-
const obj = emitExpr(e.obj);
|
|
74
|
-
const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
|
|
75
|
-
return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
|
|
76
|
-
}
|
|
77
|
-
case "toNat": {
|
|
78
|
-
const inner = emitExpr(e.expr);
|
|
79
|
-
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
80
|
-
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
81
|
-
}
|
|
82
|
-
case "index":
|
|
83
|
-
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
84
|
-
case "record": {
|
|
85
|
-
const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
86
|
-
if (e.spread)
|
|
87
|
-
return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
|
|
88
|
-
return `{ ${fields.join(", ")} }`;
|
|
89
|
-
}
|
|
90
|
-
case "if":
|
|
91
|
-
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
92
|
-
case "match": {
|
|
93
|
-
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
94
|
-
return `match ${e.scrutinee} with ${arms.join(" ")}`;
|
|
95
|
-
}
|
|
96
|
-
case "forall": return `∀ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
97
|
-
case "exists": return `∃ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
98
|
-
case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// ── Statement emission ──────────────────────────────────────
|
|
102
|
-
function emitStmts(stmts, indent) {
|
|
103
|
-
const pad = " ".repeat(indent);
|
|
104
|
-
return stmts.map(s => emitStmt(s, indent)).join("\n");
|
|
105
|
-
}
|
|
106
|
-
function emitStmt(s, indent) {
|
|
107
|
-
const pad = " ".repeat(indent);
|
|
108
|
-
switch (s.kind) {
|
|
109
|
-
case "let":
|
|
110
|
-
return s.mutable
|
|
111
|
-
? `${pad}let mut ${escapeName(s.name)} : ${s.type} := ${emitExpr(s.value)}`
|
|
112
|
-
: `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
|
|
113
|
-
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
114
|
-
case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
|
|
115
|
-
case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
|
|
116
|
-
case "return": return `${pad}return ${emitExpr(s.value)}`;
|
|
117
|
-
case "break": return `${pad}break`;
|
|
118
|
-
case "continue": return `${pad}continue`;
|
|
119
|
-
case "if": {
|
|
120
|
-
let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
|
|
121
|
-
if (s.else.length > 0) {
|
|
122
|
-
if (s.else.length === 1 && s.else[0].kind === "if") {
|
|
123
|
-
const ei = s.else[0];
|
|
124
|
-
out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
|
|
125
|
-
if (ei.else.length > 0)
|
|
126
|
-
out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return out;
|
|
133
|
-
}
|
|
134
|
-
case "match": {
|
|
135
|
-
const lines = [`${pad}match ${s.scrutinee} with`];
|
|
136
|
-
for (const arm of s.arms) {
|
|
137
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
138
|
-
lines.push(emitStmts(arm.body, indent + 1));
|
|
139
|
-
}
|
|
140
|
-
return lines.join("\n");
|
|
141
|
-
}
|
|
142
|
-
case "while": {
|
|
143
|
-
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
144
|
-
for (const inv of s.invariants)
|
|
145
|
-
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
146
|
-
if (s.doneWith)
|
|
147
|
-
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
148
|
-
if (s.decreasing)
|
|
149
|
-
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
150
|
-
lines.push(`${pad}do`);
|
|
151
|
-
lines.push(emitStmts(s.body, indent + 1));
|
|
152
|
-
return lines.join("\n");
|
|
153
|
-
}
|
|
154
|
-
case "forin": {
|
|
155
|
-
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
156
|
-
for (const inv of s.invariants)
|
|
157
|
-
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
158
|
-
lines.push(`${pad}do`);
|
|
159
|
-
lines.push(emitStmts(s.body, indent + 1));
|
|
160
|
-
return lines.join("\n");
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// ── Declaration emission ─────────────────────────────────────
|
|
165
|
-
function emitDecl(d) {
|
|
166
|
-
switch (d.kind) {
|
|
167
|
-
case "inductive": {
|
|
168
|
-
const lines = [`inductive ${d.name} where`];
|
|
169
|
-
for (const c of d.constructors) {
|
|
170
|
-
if (c.fields.length === 0) {
|
|
171
|
-
lines.push(` | ${c.name} : ${d.name}`);
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
const params = c.fields.map(f => `(${escapeName(f.name)} : ${f.type})`).join(" ");
|
|
175
|
-
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (d.deriving.length > 0)
|
|
179
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
180
|
-
return lines.join("\n");
|
|
181
|
-
}
|
|
182
|
-
case "structure": {
|
|
183
|
-
const lines = [`structure ${d.name} where`];
|
|
184
|
-
for (const f of d.fields)
|
|
185
|
-
lines.push(` ${escapeName(f.name)} : ${f.type}`);
|
|
186
|
-
if (d.deriving.length > 0)
|
|
187
|
-
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
188
|
-
return lines.join("\n");
|
|
189
|
-
}
|
|
190
|
-
case "def": {
|
|
191
|
-
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
192
|
-
return `def ${d.name} ${params} : ${d.returnType} :=\n${emitPureExpr(d.body, 1)}`;
|
|
193
|
-
}
|
|
194
|
-
case "method": {
|
|
195
|
-
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
196
|
-
const lines = [`method ${d.name} ${params} return (res : ${d.returnType})`];
|
|
197
|
-
for (const r of d.requires)
|
|
198
|
-
lines.push(` require ${emitExpr(r)}`);
|
|
199
|
-
for (const e of d.ensures)
|
|
200
|
-
lines.push(` ensures ${emitExpr(e)}`);
|
|
201
|
-
lines.push(" do");
|
|
202
|
-
lines.push(emitStmts(d.body, 2));
|
|
203
|
-
return lines.join("\n");
|
|
204
|
-
}
|
|
205
|
-
case "namespace": {
|
|
206
|
-
const lines = [`namespace ${d.name}`];
|
|
207
|
-
for (const inner of d.decls)
|
|
208
|
-
lines.push("", emitDecl(inner));
|
|
209
|
-
lines.push("", `end ${d.name}`);
|
|
210
|
-
return lines.join("\n");
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/** Emit a pure expression with indented if/match blocks. */
|
|
215
|
-
function emitPureExpr(e, indent) {
|
|
216
|
-
const pad = " ".repeat(indent);
|
|
217
|
-
switch (e.kind) {
|
|
218
|
-
case "if":
|
|
219
|
-
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
220
|
-
case "match": {
|
|
221
|
-
const lines = [`${pad}match ${e.scrutinee} with`];
|
|
222
|
-
for (const arm of e.arms) {
|
|
223
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
224
|
-
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
225
|
-
}
|
|
226
|
-
return lines.join("\n");
|
|
227
|
-
}
|
|
228
|
-
case "let":
|
|
229
|
-
return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
|
|
230
|
-
default:
|
|
231
|
-
return `${pad}${emitExpr(e)}`;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// ── File emission ────────────────────────────────────────────
|
|
235
|
-
export function emitFile(file) {
|
|
236
|
-
const lines = [];
|
|
237
|
-
if (file.comment) {
|
|
238
|
-
lines.push("/-");
|
|
239
|
-
lines.push(file.comment);
|
|
240
|
-
lines.push("-/");
|
|
241
|
-
}
|
|
242
|
-
for (const imp of file.imports)
|
|
243
|
-
lines.push(`import ${imp}`);
|
|
244
|
-
if (file.options.length > 0)
|
|
245
|
-
lines.push("");
|
|
246
|
-
for (const opt of file.options)
|
|
247
|
-
lines.push(`set_option ${opt.key} ${opt.value}`);
|
|
248
|
-
for (const decl of file.decls) {
|
|
249
|
-
lines.push("");
|
|
250
|
-
lines.push(emitDecl(decl));
|
|
251
|
-
}
|
|
252
|
-
return lines.join("\n") + "\n";
|
|
253
|
-
}
|