lemmascript 0.5.1 → 0.5.2
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 +9 -3
- package/package.json +1 -1
- package/tools/dist/dafny-emit.js +50 -11
- package/tools/dist/extract.js +69 -9
- package/tools/dist/lean-emit.js +64 -9
- package/tools/dist/narrow.js +32 -4
- package/tools/dist/peephole.js +3 -0
- package/tools/dist/resolve.js +90 -6
- package/tools/dist/transform.js +92 -8
- package/tools/dist/typedir.js +4 -1
- package/tools/dist/types.js +12 -2
package/README.md
CHANGED
|
@@ -27,6 +27,12 @@ See the external case studies:
|
|
|
27
27
|
- **[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. Dafny only.
|
|
28
28
|
- **[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.
|
|
29
29
|
- **[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.
|
|
30
|
+
- **[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 only.
|
|
31
|
+
- **[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.
|
|
32
|
+
- **[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.
|
|
33
|
+
- **[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.
|
|
34
|
+
- **[quota-lemmascript](https://github.com/midspiral/quota-lemmascript)** — greenfield verified booking app: providers publish a page of limited-capacity *featured slots*, signed-in users grab them (React + Cloudflare Durable Objects + D1). A deliberate **inverse** of [quorum-lemmascript](https://github.com/midspiral/quorum-lemmascript): where Quorum counts *up to* a threshold over data partitioned **per participant** (no conflicts ⇒ optimistic, lock-free, no rollback), Quota's bookings **contend** for shared inventory, so the load-bearing fact flips from a count to a **bound** — for every slot `j`, `confirmedCount(bookings, j) <= slots[j].capacity` — and so does the concurrency story: the *same* `domain.ts` runs in the browser and the Durable Object, but **server-authoritatively** (it never oversells under contention; no optimistic client apply). Proven: no overbooking (invariant preservation across `tryBook` / cancel), accept-iff-room, an **idempotent three-way `tryBook`** (a retry reads as success, not rejection — only "confirmed" mutates), cancellation frees seats, replay determinism, and full order-invariance of availability under contention — `confirmedCountPerm` / `hasRoomPermInvariant` show availability depends only on the *multiset* of the booking log (any reordering, not just a pairwise swap), via the same `perm(...)` predicate Quorum drove into LemmaScript. NDJSON export is built on the verified `confirmedOnly`. The counting kernel is Quorum's `countFree` re-pointed at bookings — total and precondition-free so it composes. 80 Dafny VCs, 0 errors. Trust boundary (stated plainly): auth, the React UI, WebSocket/DO/D1 I/O, email, slot date/time labeling, and abuse/rate-limiting. Dafny only.
|
|
35
|
+
- **[henri-lemmascript](https://github.com/midspiral/henri-lemmascript)** — a runnable TypeScript coding-agent CLI (a port of [henri](https://github.com/metareflection/henri/); multi-provider, Anthropic + AWS Bedrock) whose security- and protocol-critical core is verified and **imported directly by the live agent** — `decide()` gates every real tool call and the conversation invariant is asserted on every turn (it streams end-to-end against Bedrock). Three modules, 48 Dafny VCs, 0 errors. (1) **Permission gate**: soundness (`decide == Allow ⟺ isAllowed`), **path-traversal containment** — auto-allow-in-cwd can never resolve outside cwd, with `.`/`..` normalization proved in-core so the shell is trusted only to `resolve().split('/')` — grant monotonicity, and `rejectPrompts` is deny-only. (2) **Conversation protocol**: tool-call/result pairing plus the [pi-lemmascript](https://github.com/midspiral/pi-lemmascript)-style **no-orphaned-`tool_result`** property, proved as an invariant *preserved by the loop* — `wellFormed(msgs + [assistant(calls), tool(makeResults(calls))])` — not checked after the fact. (3) **Hook/config merge**: removal, **tool-name uniqueness — a fix** (henri concatenated hook tool lists with no dedup, so two hooks could shadow a name), order-independence, and additivity composed **cross-module** with the gate's monotonicity (merging only grows the allow-sets, which by P3 never revokes an `Allow`). The merge is verified **in place** via `//@ declare-type Tool { name: string }`, shadowing the real `Tool`'s function-valued `execute` so the actual `mergeTools(Tool[])` is the proof target rather than a parallel model. Dafny only.
|
|
30
36
|
|
|
31
37
|
## Setup
|
|
32
38
|
|
|
@@ -71,9 +77,7 @@ npx lsc gen --backend=lean src/myModule.ts
|
|
|
71
77
|
lake build
|
|
72
78
|
```
|
|
73
79
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
### Annotations
|
|
80
|
+
## Annotations
|
|
77
81
|
|
|
78
82
|
```typescript
|
|
79
83
|
//@ requires arr.length > 0
|
|
@@ -83,6 +87,8 @@ lake build
|
|
|
83
87
|
//@ type i nat
|
|
84
88
|
```
|
|
85
89
|
|
|
90
|
+
For the full surface, see [SPEC.md](SPEC.md).
|
|
91
|
+
|
|
86
92
|
## File Structure
|
|
87
93
|
|
|
88
94
|
### Dafny backend
|
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -166,6 +166,12 @@ function emitExpr(e) {
|
|
|
166
166
|
return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
|
|
167
167
|
if (e.method === "filter")
|
|
168
168
|
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
169
|
+
// filterMap (synthesized in resolve): drop Nones and unwrap to seq<T>.
|
|
170
|
+
if (e.method === "filterSome") {
|
|
171
|
+
needPreamble("SeqFilterSome");
|
|
172
|
+
needPreamble("OptionType");
|
|
173
|
+
return `SeqFilterSome(${obj})`;
|
|
174
|
+
}
|
|
169
175
|
if (e.method === "every")
|
|
170
176
|
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
171
177
|
if (e.method === "findLast") {
|
|
@@ -350,16 +356,9 @@ function emitExpr(e) {
|
|
|
350
356
|
needPreamble("BitAnd");
|
|
351
357
|
return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
|
|
352
358
|
}
|
|
353
|
-
// int
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const rightIsReal = e.right.kind === "num" && !Number.isInteger(e.right.value);
|
|
357
|
-
if (leftIsReal !== rightIsReal) {
|
|
358
|
-
const left = leftIsReal ? emitExpr(e.left) : `(${emitExpr(e.left)} as real)`;
|
|
359
|
-
const right = rightIsReal ? emitExpr(e.right) : `(${emitExpr(e.right)} as real)`;
|
|
360
|
-
return `(${left} ${op} ${right})`;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
359
|
+
// int→real coercion is now injected upstream in transform (toReal nodes),
|
|
360
|
+
// which has full type information — including real-typed variables, not
|
|
361
|
+
// just literals — so no literal-based coercion is needed here.
|
|
363
362
|
return `(${wrapQuantifier(e.left)} ${op} ${emitExpr(e.right)})`;
|
|
364
363
|
}
|
|
365
364
|
case "implies": {
|
|
@@ -399,6 +398,8 @@ function emitExpr(e) {
|
|
|
399
398
|
needPreamble("MathMin");
|
|
400
399
|
needPreamble("MinOfSeq");
|
|
401
400
|
}
|
|
401
|
+
if (e.fn === "Perm")
|
|
402
|
+
needPreamble("Perm");
|
|
402
403
|
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
403
404
|
}
|
|
404
405
|
case "field": {
|
|
@@ -414,6 +415,8 @@ function emitExpr(e) {
|
|
|
414
415
|
case "toNat":
|
|
415
416
|
// Dafny doesn't need toNat — just emit the inner expression
|
|
416
417
|
return emitExpr(e.expr);
|
|
418
|
+
case "toReal":
|
|
419
|
+
return `(${emitExpr(e.expr)} as real)`;
|
|
417
420
|
case "index": {
|
|
418
421
|
const obj = emitExpr(e.arr);
|
|
419
422
|
const idx = emitExpr(e.idx);
|
|
@@ -596,10 +599,29 @@ function emitDecl(d) {
|
|
|
596
599
|
switch (d.kind) {
|
|
597
600
|
case "inductive": {
|
|
598
601
|
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
602
|
+
// Dafny requires a destructor shared across constructors to have a single
|
|
603
|
+
// type. Two TS variants can legitimately share a field name with different
|
|
604
|
+
// types (e.g. label.targetId: string vs leaf.targetId: string?). Detect
|
|
605
|
+
// such collisions and make those destructors per-constructor unique. Safe:
|
|
606
|
+
// variant-field reads lower to positional match bindings, never named
|
|
607
|
+
// destructors on the union (a name shared with differing types isn't even
|
|
608
|
+
// accessible on the union type in TS), so nothing references the old name.
|
|
609
|
+
const typesByField = new Map();
|
|
610
|
+
for (const c of d.constructors)
|
|
611
|
+
for (const f of c.fields) {
|
|
612
|
+
let s = typesByField.get(f.name);
|
|
613
|
+
if (!s) {
|
|
614
|
+
s = new Set();
|
|
615
|
+
typesByField.set(f.name, s);
|
|
616
|
+
}
|
|
617
|
+
s.add(tyToDafny(f.type));
|
|
618
|
+
}
|
|
619
|
+
const collides = new Set([...typesByField].filter(([, s]) => s.size > 1).map(([n]) => n));
|
|
599
620
|
const ctors = d.constructors.map(c => {
|
|
600
621
|
if (c.fields.length === 0)
|
|
601
622
|
return escapeName(c.name);
|
|
602
|
-
|
|
623
|
+
const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name}` } : f);
|
|
624
|
+
return `${escapeName(c.name)}(${paramList(fields)})`;
|
|
603
625
|
});
|
|
604
626
|
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
605
627
|
}
|
|
@@ -609,6 +631,11 @@ function emitDecl(d) {
|
|
|
609
631
|
case "type-alias": {
|
|
610
632
|
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
611
633
|
}
|
|
634
|
+
case "opaque-type": {
|
|
635
|
+
// Abstract type — no definition. `(==)` so it can sit inside datatypes
|
|
636
|
+
// that derive structural equality. Never constructed or destructured.
|
|
637
|
+
return `type ${d.name}(==)`;
|
|
638
|
+
}
|
|
612
639
|
case "def": {
|
|
613
640
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
614
641
|
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
@@ -743,6 +770,12 @@ const CEIL_REAL = `function CeilReal(x: real): int
|
|
|
743
770
|
if x == (x.Floor as real) then x.Floor
|
|
744
771
|
else x.Floor + 1
|
|
745
772
|
}`;
|
|
773
|
+
const SEQ_FILTER_SOME = `function SeqFilterSome<T>(xs: seq<Option<T>>): seq<T>
|
|
774
|
+
ensures |SeqFilterSome(xs)| <= |xs|
|
|
775
|
+
{
|
|
776
|
+
if |xs| == 0 then []
|
|
777
|
+
else (if xs[0].Some? then [xs[0].value] else []) + SeqFilterSome(xs[1..])
|
|
778
|
+
}`;
|
|
746
779
|
const SEQ_FIND_INDEX = `function SeqFindIndex<T>(s: seq<T>, p: T -> bool): int
|
|
747
780
|
ensures -1 <= SeqFindIndex(s, p) < |s|
|
|
748
781
|
ensures SeqFindIndex(s, p) >= 0 ==> p(s[SeqFindIndex(s, p)])
|
|
@@ -956,6 +989,10 @@ const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
|
956
989
|
else NatToString(n / 10) + [digit]
|
|
957
990
|
}`;
|
|
958
991
|
const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
|
|
992
|
+
// perm(a, b) — `a` and `b` are reorderings of each other (equal as multisets).
|
|
993
|
+
// Transparent (Dafny unfolds it), so hand-proofs can reason with `multiset`
|
|
994
|
+
// directly. The `(==)` bound requires the element type to support equality.
|
|
995
|
+
const PERM = `predicate Perm<T(==)>(a: seq<T>, b: seq<T>) { multiset(a) == multiset(b) }`;
|
|
959
996
|
const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
960
997
|
ensures forall x :: x in s <==> x in res
|
|
961
998
|
ensures |res| == |s|
|
|
@@ -986,6 +1023,7 @@ const PREAMBLE_CODE = [
|
|
|
986
1023
|
["FloorReal", FLOOR_REAL],
|
|
987
1024
|
["SeqIndexOf", SEQ_INDEX_OF],
|
|
988
1025
|
["SeqFindIndex", SEQ_FIND_INDEX],
|
|
1026
|
+
["SeqFilterSome", SEQ_FILTER_SOME],
|
|
989
1027
|
["SeqFindLast", SEQ_FIND_LAST],
|
|
990
1028
|
["SeqFlatten", SEQ_FLATTEN],
|
|
991
1029
|
["SeqJoin", SEQ_JOIN],
|
|
@@ -1001,6 +1039,7 @@ const PREAMBLE_CODE = [
|
|
|
1001
1039
|
["MathMax", MATH_MAX],
|
|
1002
1040
|
["MaxOfSeq", MAX_OF_SEQ],
|
|
1003
1041
|
["MinOfSeq", MIN_OF_SEQ],
|
|
1042
|
+
["Perm", PERM],
|
|
1004
1043
|
];
|
|
1005
1044
|
// ── Constructor and record helpers ───────────────────────────
|
|
1006
1045
|
let _recordCtors = new Map();
|
package/tools/dist/extract.js
CHANGED
|
@@ -159,6 +159,26 @@ function _synthName(elemName, otherName) {
|
|
|
159
159
|
const sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
160
160
|
return `ArrayOf_${sanitize(elemName)}_Or_${sanitize(otherName)}`;
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Fall-through for a union LS can't model as a tagged union (no runtime type
|
|
164
|
+
* test maps to a tag — e.g. members are unreachable imports with no visible
|
|
165
|
+
* discriminant). Registers an opaque-type TypeDeclInfo and returns its name, so
|
|
166
|
+
* the union becomes one abstract `type` rather than invalid raw-union Dafny.
|
|
167
|
+
*
|
|
168
|
+
* Sound because an opaque type has no constructor and no tag predicate: any
|
|
169
|
+
* attempt to build or type-test the value fails to lower, so it can only be
|
|
170
|
+
* passed through — the one sound use of a union we can't discriminate. Distinct
|
|
171
|
+
* from dropping the field (which collapses values and is unsound): the value is
|
|
172
|
+
* preserved, just uninspectable.
|
|
173
|
+
*/
|
|
174
|
+
function _synthOpaque(memberNames) {
|
|
175
|
+
const sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
176
|
+
const name = `Opaque_${memberNames.map(sanitize).join("_or_")}`;
|
|
177
|
+
if (_synthArrayUnions !== null && !_synthArrayUnions.some(d => d.name === name)) {
|
|
178
|
+
_synthArrayUnions.push({ name, kind: "opaque" });
|
|
179
|
+
}
|
|
180
|
+
return name;
|
|
181
|
+
}
|
|
162
182
|
/**
|
|
163
183
|
* String-level fallback for synth detection on a `T[] | U` shape, used by
|
|
164
184
|
* declare-type field parsing (where no ts-morph TypeNode is available). The
|
|
@@ -277,10 +297,10 @@ function extractExpr(node) {
|
|
|
277
297
|
if (Node.isNumericLiteral(node)) {
|
|
278
298
|
return { kind: "num", value: Number(node.getLiteralValue()) };
|
|
279
299
|
}
|
|
280
|
-
// BigInt literal (e.g. 32n, 0xffffn) —
|
|
300
|
+
// BigInt literal (e.g. 32n, 0xffffn) — integer, with bigint division semantics
|
|
281
301
|
if (Node.isBigIntLiteral(node)) {
|
|
282
302
|
const text = node.getText().replace(/n$/, '');
|
|
283
|
-
return { kind: "num", value: Number(text) };
|
|
303
|
+
return { kind: "num", value: Number(text), big: true };
|
|
284
304
|
}
|
|
285
305
|
// Template literal: `foo${x}bar` → "foo" + x + "bar"
|
|
286
306
|
if (Node.isTemplateExpression(node)) {
|
|
@@ -431,12 +451,16 @@ function extractExpr(node) {
|
|
|
431
451
|
const typeNode = p.getTypeNode();
|
|
432
452
|
return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
|
|
433
453
|
});
|
|
454
|
+
// Capture an explicit return annotation (`(x): Out => …`) so resolve can type
|
|
455
|
+
// return-position record literals to their named type instead of a tuple.
|
|
456
|
+
const retNode = node.getReturnTypeNode();
|
|
457
|
+
const returnTsType = retNode ? typeToString(node.getReturnType()) : undefined;
|
|
434
458
|
const body = node.getBody();
|
|
435
459
|
if (Node.isExpression(body)) {
|
|
436
|
-
return { kind: "lambda", params, body: extractExpr(body) };
|
|
460
|
+
return { kind: "lambda", params, body: extractExpr(body), returnTsType };
|
|
437
461
|
}
|
|
438
462
|
if (Node.isBlock(body)) {
|
|
439
|
-
return { kind: "lambda", params, body: extractStmts(body.getStatements()) };
|
|
463
|
+
return { kind: "lambda", params, body: extractStmts(body.getStatements()), returnTsType };
|
|
440
464
|
}
|
|
441
465
|
throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
|
|
442
466
|
}
|
|
@@ -552,6 +576,12 @@ function extractExpr(node) {
|
|
|
552
576
|
if (Node.isNullLiteral(node)) {
|
|
553
577
|
return { kind: "var", name: "undefined" };
|
|
554
578
|
}
|
|
579
|
+
// `typeof X` — only meaningful in a `typeof X === "string"` discriminator over
|
|
580
|
+
// a synth `U | T[]` union (see narrow's parseTypeofStringCheck); any other use
|
|
581
|
+
// survives to emit and errors there.
|
|
582
|
+
if (Node.isTypeOfExpression(node)) {
|
|
583
|
+
return { kind: "unop", op: "typeof", expr: extractExpr(node.getExpression()) };
|
|
584
|
+
}
|
|
555
585
|
throw new Error(`Unsupported expression: ${node.getText()}`);
|
|
556
586
|
}
|
|
557
587
|
// ── Annotation parsing ───────────────────────────────────────
|
|
@@ -796,7 +826,10 @@ function typeToString(type) {
|
|
|
796
826
|
return "bigint";
|
|
797
827
|
if (type.isString() || type.isStringLiteral())
|
|
798
828
|
return "string";
|
|
799
|
-
|
|
829
|
+
// TS expands `boolean` to the literal union `false | true`; normalize the
|
|
830
|
+
// literals back so the union dedupes to a single `boolean` rather than being
|
|
831
|
+
// mistaken for an unmodelable multi-member union.
|
|
832
|
+
if (type.isBoolean() || type.isBooleanLiteral())
|
|
800
833
|
return "boolean";
|
|
801
834
|
// Named type alias (e.g. Priority = "low" | "medium" | "high") — use the alias name
|
|
802
835
|
if (type.getAliasSymbol()) {
|
|
@@ -838,7 +871,15 @@ function typeToString(type) {
|
|
|
838
871
|
}
|
|
839
872
|
}
|
|
840
873
|
const parts = [...new Set(unionTypes.map(typeToString))];
|
|
841
|
-
|
|
874
|
+
// `undefined`/`null` are optional markers; a single real member with them
|
|
875
|
+
// is an Option, left as `X | undefined` for the optional lowering.
|
|
876
|
+
const real = parts.filter(p => p !== "undefined" && p !== "null");
|
|
877
|
+
if (real.length <= 1)
|
|
878
|
+
return parts.join(" | ");
|
|
879
|
+
// A genuine multi-member union with no tagged-union shape LS can model →
|
|
880
|
+
// a single opaque type. (See _synthOpaque.) Preserve an outer optional.
|
|
881
|
+
const opaque = _synthOpaque(real);
|
|
882
|
+
return real.length === parts.length ? opaque : `${opaque} | undefined`;
|
|
842
883
|
}
|
|
843
884
|
if (type.isTuple()) {
|
|
844
885
|
return `[${type.getTupleElements().map(t => typeToString(t)).join(", ")}]`;
|
|
@@ -1895,11 +1936,14 @@ export function extractModule(sourceFile) {
|
|
|
1895
1936
|
if (!sig)
|
|
1896
1937
|
continue;
|
|
1897
1938
|
const typeParams = sig.getTypeParameters().map(tp => tp.getText());
|
|
1939
|
+
// Normalize via typeToString (not raw getText): resolves declare-type
|
|
1940
|
+
// shadows and yields bare names, so a param typed by an unreachable import
|
|
1941
|
+
// becomes `AgentMessage`, not `import("/abs/path").AgentMessage`.
|
|
1898
1942
|
const params = sig.getParameters().map(p => ({
|
|
1899
1943
|
name: p.getName(),
|
|
1900
|
-
tsType: p.getTypeAtLocation(f.node)
|
|
1944
|
+
tsType: typeToString(p.getTypeAtLocation(f.node)),
|
|
1901
1945
|
}));
|
|
1902
|
-
const returnType = sig.getReturnType()
|
|
1946
|
+
const returnType = typeToString(sig.getReturnType());
|
|
1903
1947
|
const annots = collectFunctionAnnotations(f.node);
|
|
1904
1948
|
const requires = annots.filter(a => a.kind === "requires").map(a => a.expr);
|
|
1905
1949
|
const ensures = annots.filter(a => a.kind === "ensures").map(a => a.expr);
|
|
@@ -2121,10 +2165,17 @@ export function extractModule(sourceFile) {
|
|
|
2121
2165
|
collectNamesExpr(e.else);
|
|
2122
2166
|
}
|
|
2123
2167
|
}
|
|
2168
|
+
// Signature types (params + return) get base-name stripping below; body /
|
|
2169
|
+
// spec references stay exact-match (so a body `let xs: Hunk[]` doesn't pull
|
|
2170
|
+
// `Hunk` into the filter early and reorder output that resolveType re-adds).
|
|
2171
|
+
const sigTypes = new Set();
|
|
2124
2172
|
for (const fn of functions) {
|
|
2125
|
-
for (const p of fn.params)
|
|
2173
|
+
for (const p of fn.params) {
|
|
2126
2174
|
referencedNames.add(p.tsType);
|
|
2175
|
+
sigTypes.add(p.tsType);
|
|
2176
|
+
}
|
|
2127
2177
|
referencedNames.add(fn.returnType);
|
|
2178
|
+
sigTypes.add(fn.returnType);
|
|
2128
2179
|
collectNames(fn.body);
|
|
2129
2180
|
// Also scan spec annotations for identifier references
|
|
2130
2181
|
for (const spec of [...fn.requires, ...fn.ensures]) {
|
|
@@ -2153,6 +2204,15 @@ export function extractModule(sourceFile) {
|
|
|
2153
2204
|
}
|
|
2154
2205
|
for (const name of referencedNames)
|
|
2155
2206
|
markType(name);
|
|
2207
|
+
// Signature types also mark their base after stripping array/optional
|
|
2208
|
+
// WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
|
|
2209
|
+
// a local `Out[]` keeps `Out`. Wrappers only — never dig into generic args
|
|
2210
|
+
// (`Omit<FilePathOptions, …>` must not pull in the inner type).
|
|
2211
|
+
for (const name of sigTypes) {
|
|
2212
|
+
const base = name.replace(/\s*\|\s*(undefined|null)\s*$/, "").replace(/(\[\])+$/, "").trim();
|
|
2213
|
+
if (base !== name && /^[A-Za-z_]\w*$/.test(base))
|
|
2214
|
+
markType(base);
|
|
2215
|
+
}
|
|
2156
2216
|
typeDecls.splice(0, typeDecls.length, ...typeDecls.filter(d => neededTypes.has(d.name) || declaredNames.has(d.name)));
|
|
2157
2217
|
}
|
|
2158
2218
|
// Resolve imported types: extract types referenced in function signatures but not in this file
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -7,7 +7,22 @@ function tyToLean(ty) {
|
|
|
7
7
|
switch (ty.kind) {
|
|
8
8
|
case "nat": return "Nat";
|
|
9
9
|
case "int": return "Int";
|
|
10
|
-
case "real":
|
|
10
|
+
case "real":
|
|
11
|
+
// Real arithmetic isn't supported by the Lean backend yet: ℝ is
|
|
12
|
+
// noncomputable and needs Mathlib's real-number development, so we fail
|
|
13
|
+
// fast here rather than emit Lean that can't compile.
|
|
14
|
+
//
|
|
15
|
+
// Workarounds, in order of preference:
|
|
16
|
+
// 1. If integer division was intended, write `Math.floor(a / b)` — it
|
|
17
|
+
// lowers to flooring integer division on Lean (no real involved).
|
|
18
|
+
// 2. For `bigint` operands, `/` is already integer division — declaring
|
|
19
|
+
// the value `bigint` instead of `number` keeps it off the real path.
|
|
20
|
+
// 3. If the file genuinely needs reals, restrict it to Dafny with a
|
|
21
|
+
// `//@ backend dafny` directive.
|
|
22
|
+
// Full Lean real support is feasible but was set aside: it needs
|
|
23
|
+
// `import Mathlib.Data.Real.Basic`, `noncomputable def`s for real-valued
|
|
24
|
+
// functions, and the Int→ℝ coercion (see the stashed WIP for a sketch).
|
|
25
|
+
throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
|
|
11
26
|
case "bool": return "Bool";
|
|
12
27
|
case "string": return "String";
|
|
13
28
|
case "void": return "Unit";
|
|
@@ -90,6 +105,16 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
90
105
|
return `Array.push ${obj} ${args[0]}`;
|
|
91
106
|
if (method === "concat")
|
|
92
107
|
return `Array.push ${obj} ${args[0]}`;
|
|
108
|
+
// arr.slice → Array.extract. No-arg slice is a full copy (Array is a value
|
|
109
|
+
// type in Lean, so the receiver itself); one arg drops the prefix, two args
|
|
110
|
+
// give the half-open range. Matches JS for non-negative bounds (negative
|
|
111
|
+
// indices are unsupported — same caveat as the Dafny backend's direct slice).
|
|
112
|
+
if (method === "slice" && args.length === 0)
|
|
113
|
+
return obj;
|
|
114
|
+
if (method === "slice" && args.length === 1)
|
|
115
|
+
return `${obj}.extract ${args[0]} ${obj}.size`;
|
|
116
|
+
if (method === "slice" && args.length === 2)
|
|
117
|
+
return `${obj}.extract ${args[0]} ${args[1]}`;
|
|
93
118
|
}
|
|
94
119
|
// String methods
|
|
95
120
|
if (tyKind === "string") {
|
|
@@ -122,12 +147,17 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
122
147
|
throw new Error(`Unsupported Lean method call: .${method}() on ${tyKind}`);
|
|
123
148
|
}
|
|
124
149
|
// ── Expression emission ─────────────────────────────────────
|
|
125
|
-
// Lean
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
150
|
+
// Some Lean term forms extend their body as far as possible: `∀`/`∃` bodies,
|
|
151
|
+
// and `if`/`let` tails. As an operator operand they would swallow the operator
|
|
152
|
+
// — `(if c then 1 else 0) + r` written bare parses as `if c then 1 else (0 + r)`.
|
|
153
|
+
// Wrap these forms in parens so the operand is closed before the operator.
|
|
154
|
+
// (`match` self-parenthesizes in `emitExpr`, and other forms close via
|
|
155
|
+
// precedence, so neither needs wrapping here.)
|
|
156
|
+
function wrapOperand(sub, parentPrec) {
|
|
129
157
|
const inner = emitExpr(sub, parentPrec);
|
|
130
|
-
return (sub.kind === "forall" || sub.kind === "exists"
|
|
158
|
+
return (sub.kind === "forall" || sub.kind === "exists" ||
|
|
159
|
+
sub.kind === "if" || sub.kind === "let")
|
|
160
|
+
? `(${inner})` : inner;
|
|
131
161
|
}
|
|
132
162
|
function emitExpr(e, parentPrec) {
|
|
133
163
|
switch (e.kind) {
|
|
@@ -183,11 +213,11 @@ function emitExpr(e, parentPrec) {
|
|
|
183
213
|
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
184
214
|
}
|
|
185
215
|
const op = e.op === "arrayConcat" ? "++" : e.op;
|
|
186
|
-
const s = `${
|
|
216
|
+
const s = `${wrapOperand(e.left, prec(e.op))} ${op} ${wrapOperand(e.right, prec(e.op))}`;
|
|
187
217
|
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
188
218
|
}
|
|
189
219
|
case "implies": {
|
|
190
|
-
const parts = [...e.premises.map(p =>
|
|
220
|
+
const parts = [...e.premises.map(p => wrapOperand(p)), emitExpr(e.conclusion)];
|
|
191
221
|
const s = parts.join(" → ");
|
|
192
222
|
return parentPrec !== undefined ? `(${s})` : s;
|
|
193
223
|
}
|
|
@@ -196,6 +226,12 @@ function emitExpr(e, parentPrec) {
|
|
|
196
226
|
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
197
227
|
if (e.fn === "SetToSeq" && args.length === 1)
|
|
198
228
|
return `${args[0]}.toArray`;
|
|
229
|
+
// perm(a, b) → `List.Perm` on the underlying lists. Dafny lowers it to
|
|
230
|
+
// `multiset(a) == multiset(b)`; the Lean image is `a.toList ~ b.toList`,
|
|
231
|
+
// which mathlib's `List.Perm` provides (reflexivity, symmetry,
|
|
232
|
+
// `perm_append_comm`, and `Perm.count_eq` for the count-invariance payoff).
|
|
233
|
+
if (e.fn === "Perm" && args.length === 2)
|
|
234
|
+
return `(${args[0]}.toList).Perm (${args[1]}.toList)`;
|
|
199
235
|
return `${e.fn} ${args.join(" ")}`;
|
|
200
236
|
}
|
|
201
237
|
case "field": {
|
|
@@ -210,6 +246,10 @@ function emitExpr(e, parentPrec) {
|
|
|
210
246
|
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
211
247
|
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
212
248
|
}
|
|
249
|
+
case "toReal":
|
|
250
|
+
// A real value reached the Lean backend via coercion (e.g. number `/`).
|
|
251
|
+
// Same unsupported-real story as the `real` type case in tyToLean.
|
|
252
|
+
throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
|
|
213
253
|
case "index":
|
|
214
254
|
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
215
255
|
case "record": {
|
|
@@ -393,9 +433,24 @@ function emitDecl(d) {
|
|
|
393
433
|
case "type-alias": {
|
|
394
434
|
return `abbrev ${d.name} := ${tyToLean(d.target)}`;
|
|
395
435
|
}
|
|
436
|
+
case "opaque-type": {
|
|
437
|
+
// Abstract type — no definition. Never constructed or destructured.
|
|
438
|
+
return `opaque ${d.name} : Type`;
|
|
439
|
+
}
|
|
396
440
|
case "def": {
|
|
397
441
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
398
|
-
|
|
442
|
+
let out = `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
|
|
443
|
+
// A `//@ decreases` on a pure function marks it recursive and names its
|
|
444
|
+
// termination measure — emit it as Lean's `termination_by`. This is
|
|
445
|
+
// required when the recursion is on `arr.slice(...)` (→ `Array.extract`,
|
|
446
|
+
// which Lean cannot see as a structural subterm); for a bare-Nat counter
|
|
447
|
+
// Lean could infer structural recursion on its own, but honoring the
|
|
448
|
+
// clause uniformly is simpler and harmless. Lean's default `decreasing_by`
|
|
449
|
+
// discharges the goal in both cases (it knows `Array.size_extract`), so no
|
|
450
|
+
// explicit tactic is needed.
|
|
451
|
+
if (d.decreases)
|
|
452
|
+
out += `\ntermination_by ${emitExpr(d.decreases)}`;
|
|
453
|
+
return out;
|
|
399
454
|
}
|
|
400
455
|
case "def-by-method":
|
|
401
456
|
throw new Error("function by method is not supported for Lean backend");
|
package/tools/dist/narrow.js
CHANGED
|
@@ -349,14 +349,18 @@ function ruleConditionalArrayIsArray(e) {
|
|
|
349
349
|
if (e.kind !== "conditional")
|
|
350
350
|
return null;
|
|
351
351
|
const pos = parseArrayIsArrayCall(e.cond);
|
|
352
|
-
|
|
352
|
+
// `typeof x === "string"` is a positive check like `Array.isArray`, but selects
|
|
353
|
+
// the NonArrayBranch — its then-branch is the matched-variant body.
|
|
354
|
+
const tof = pos ? null : parseTypeofStringCheck(e.cond);
|
|
355
|
+
const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
|
|
353
356
|
? parseArrayIsArrayCall(e.cond.expr)
|
|
354
357
|
: null;
|
|
355
|
-
const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
358
|
+
const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
|
|
356
359
|
if (!matched)
|
|
357
360
|
return null;
|
|
358
|
-
const
|
|
359
|
-
const
|
|
361
|
+
const positive = pos ?? tof;
|
|
362
|
+
const thenBody = positive ? e.then : e.else;
|
|
363
|
+
const elseBody = positive ? e.else : e.then;
|
|
360
364
|
return {
|
|
361
365
|
kind: "tagMatch",
|
|
362
366
|
scrutinee: matched.scrutinee,
|
|
@@ -602,6 +606,30 @@ function parseArrayIsArrayCall(call) {
|
|
|
602
606
|
return null;
|
|
603
607
|
return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
|
|
604
608
|
}
|
|
609
|
+
/** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
|
|
610
|
+
* union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
|
|
611
|
+
* The runtime `=== "string"` test matches that branch only when `U` is string —
|
|
612
|
+
* for any other non-array payload (`number | T[]`, …) it never holds, so we must
|
|
613
|
+
* NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
|
|
614
|
+
function parseTypeofStringCheck(e) {
|
|
615
|
+
if (e.kind !== "binop" || e.op !== "===")
|
|
616
|
+
return null;
|
|
617
|
+
const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
|
|
618
|
+
: e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
|
|
619
|
+
const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
|
|
620
|
+
if (!tof || lit !== "string")
|
|
621
|
+
return null;
|
|
622
|
+
if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
|
|
623
|
+
return null;
|
|
624
|
+
const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
|
|
625
|
+
const decl = _typeDecls.find(d => d.name === baseTyName);
|
|
626
|
+
if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
|
|
627
|
+
return null;
|
|
628
|
+
const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
|
|
629
|
+
if (valTy?.kind !== "string")
|
|
630
|
+
return null; // guard: the non-array branch must actually be `string`
|
|
631
|
+
return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
|
|
632
|
+
}
|
|
605
633
|
/** A "narrowable path" is a var or a chain of field accesses rooted at a var
|
|
606
634
|
* — i.e., pure and structurally addressable, so transforms can substitute
|
|
607
635
|
* occurrences inside a matched arm without worrying about re-evaluation. */
|
package/tools/dist/peephole.js
CHANGED
|
@@ -20,6 +20,7 @@ function mapExpr(e, f) {
|
|
|
20
20
|
case "app": return { ...e, args: e.args.map(r) };
|
|
21
21
|
case "field": return { ...e, obj: r(e.obj) };
|
|
22
22
|
case "toNat": return { ...e, expr: r(e.expr) };
|
|
23
|
+
case "toReal": return { ...e, expr: r(e.expr) };
|
|
23
24
|
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
24
25
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null,
|
|
25
26
|
fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
@@ -364,6 +365,7 @@ function rewriteChildrenExpr(e) {
|
|
|
364
365
|
case "app": return { ...e, args: e.args.map(r) };
|
|
365
366
|
case "field": return { ...e, obj: r(e.obj) };
|
|
366
367
|
case "toNat": return { ...e, expr: r(e.expr) };
|
|
368
|
+
case "toReal": return { ...e, expr: r(e.expr) };
|
|
367
369
|
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
368
370
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null,
|
|
369
371
|
fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
@@ -445,6 +447,7 @@ function peepholeDecl(d) {
|
|
|
445
447
|
case "inductive":
|
|
446
448
|
case "structure":
|
|
447
449
|
case "type-alias":
|
|
450
|
+
case "opaque-type":
|
|
448
451
|
case "extern":
|
|
449
452
|
return d;
|
|
450
453
|
}
|
package/tools/dist/resolve.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses linked environments (Scheme-style) for lexical scoping.
|
|
5
5
|
* No mutation — each let extends the chain, lookup walks it.
|
|
6
6
|
*/
|
|
7
|
+
import { isBigInt } from "./typedir.js";
|
|
7
8
|
import { parseTsType } from "./types.js";
|
|
8
9
|
import { parseExpr } from "./specparser.js";
|
|
9
10
|
function lookup(env, name) {
|
|
@@ -447,6 +448,12 @@ function tyToTsStr(ty) {
|
|
|
447
448
|
return "number";
|
|
448
449
|
if (ty.kind === "bool")
|
|
449
450
|
return "boolean";
|
|
451
|
+
// Optional element (e.g. a `.filter` over a `T | undefined`-typed map result):
|
|
452
|
+
// type the callback param so its `x !== undefined` check narrows correctly.
|
|
453
|
+
if (ty.kind === "optional") {
|
|
454
|
+
const inner = tyToTsStr(ty.inner);
|
|
455
|
+
return inner ? `${inner} | undefined` : undefined;
|
|
456
|
+
}
|
|
450
457
|
return undefined;
|
|
451
458
|
}
|
|
452
459
|
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
@@ -486,6 +493,21 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
486
493
|
}
|
|
487
494
|
return rawArgs;
|
|
488
495
|
}
|
|
496
|
+
/** A defined-check filter predicate: `(x) => x !== undefined` (expression or
|
|
497
|
+
* single-return body). Detected on the raw IR before narrowing rewrites it. */
|
|
498
|
+
function isDefinedCheckRawLambda(raw) {
|
|
499
|
+
if (raw.kind !== "lambda" || raw.params.length !== 1)
|
|
500
|
+
return false;
|
|
501
|
+
const p = raw.params[0].name;
|
|
502
|
+
const body = Array.isArray(raw.body)
|
|
503
|
+
? (raw.body.length === 1 && raw.body[0].kind === "return" ? raw.body[0].value : null)
|
|
504
|
+
: raw.body;
|
|
505
|
+
if (!body || body.kind !== "binop" || body.op !== "!==")
|
|
506
|
+
return false;
|
|
507
|
+
const isParam = (x) => x.kind === "var" && x.name === p;
|
|
508
|
+
const isUndef = (x) => x.kind === "var" && x.name === "undefined";
|
|
509
|
+
return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
|
|
510
|
+
}
|
|
489
511
|
/** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
|
|
490
512
|
function coerceCallArgs(args, fn, ctx) {
|
|
491
513
|
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
@@ -517,6 +539,16 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
517
539
|
if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
|
|
518
540
|
return { kind: "bool" };
|
|
519
541
|
}
|
|
542
|
+
// Math.* numeric builtins: abs/min/max preserve the operand's numeric type
|
|
543
|
+
// (real if any operand is real); floor/ceil/round/trunc return an integer.
|
|
544
|
+
if (fn.obj.kind === "var" && fn.obj.name === "Math") {
|
|
545
|
+
if (fn.field === "abs" && args.length === 1)
|
|
546
|
+
return args[0].ty;
|
|
547
|
+
if ((fn.field === "min" || fn.field === "max") && args.length >= 1)
|
|
548
|
+
return args.some(a => a.ty.kind === "real") ? { kind: "real" } : args[0].ty;
|
|
549
|
+
if (["floor", "ceil", "round", "trunc"].includes(fn.field))
|
|
550
|
+
return { kind: "int" };
|
|
551
|
+
}
|
|
520
552
|
const objTy = fn.obj.ty;
|
|
521
553
|
if (objTy.kind === "map") {
|
|
522
554
|
if (fn.field === "get")
|
|
@@ -558,8 +590,11 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
558
590
|
if (fn.field === "join" && objTy.elem.kind === "string")
|
|
559
591
|
return { kind: "string" };
|
|
560
592
|
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
561
|
-
const
|
|
562
|
-
|
|
593
|
+
const lam = args[0];
|
|
594
|
+
// Prefer the lambda's declared return type (handles multi-statement bodies
|
|
595
|
+
// where body[0] is an `if`, not a `return`); fall back to the body's return.
|
|
596
|
+
const retTy = lam.ty.kind === "fn" ? lam.ty.result
|
|
597
|
+
: lam.body.length > 0 && lam.body[0].kind === "return" ? lam.body[0].value.ty : { kind: "unknown" };
|
|
563
598
|
return { kind: "array", elem: retTy };
|
|
564
599
|
}
|
|
565
600
|
}
|
|
@@ -613,6 +648,8 @@ function resolveExpr(e, ctx) {
|
|
|
613
648
|
case "num":
|
|
614
649
|
if (!Number.isInteger(e.value))
|
|
615
650
|
return { kind: "num", value: e.value, ty: { kind: "real" } };
|
|
651
|
+
if (e.big)
|
|
652
|
+
return { kind: "num", value: e.value, ty: { kind: "int", big: true } };
|
|
616
653
|
return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
|
|
617
654
|
case "str":
|
|
618
655
|
return { kind: "str", value: e.value, ty: { kind: "string" } };
|
|
@@ -658,16 +695,38 @@ function resolveExpr(e, ctx) {
|
|
|
658
695
|
}
|
|
659
696
|
else if (e.op === "||")
|
|
660
697
|
ty = right.ty;
|
|
661
|
-
else if (
|
|
698
|
+
else if (e.op === "/") {
|
|
699
|
+
// `number / number` is real (floating-point) division: 3 / 2 === 1.5,
|
|
700
|
+
// never 1 — an integer quotient requires an explicit Math.floor (which
|
|
701
|
+
// lowers to JSFloorDiv). But `bigint / bigint` is genuinely integer
|
|
702
|
+
// division in JS (3n / 2n === 1n), so keep it integer.
|
|
703
|
+
ty = (isBigInt(left.ty) || isBigInt(right.ty)) ? { kind: "int", big: true } : { kind: "real" };
|
|
704
|
+
}
|
|
705
|
+
else if (["+", "-", "*", "%"].includes(e.op)) {
|
|
662
706
|
ty = (left.ty.kind === "real" || right.ty.kind === "real") ? { kind: "real" } : left.ty;
|
|
663
707
|
}
|
|
664
708
|
return { kind: "binop", op: e.op, left, right, ty };
|
|
665
709
|
}
|
|
666
710
|
case "unop": {
|
|
667
711
|
const expr = resolveExpr(e.expr, ctx);
|
|
668
|
-
|
|
712
|
+
const ty = e.op === "!" ? { kind: "bool" } : e.op === "typeof" ? { kind: "string" } : expr.ty;
|
|
713
|
+
return { kind: "unop", op: e.op, expr, ty };
|
|
669
714
|
}
|
|
670
715
|
case "call": {
|
|
716
|
+
// perm(a, b): spec-only permutation predicate — true iff `a` and `b` are
|
|
717
|
+
// reorderings of each other (equal as multisets). Lowers to the `Perm`
|
|
718
|
+
// preamble (Dafny `multiset(a) == multiset(b)`; Lean `a.toList ~ b.toList`).
|
|
719
|
+
// It has no runtime counterpart, so it is rejected outside `//@` specs.
|
|
720
|
+
if (e.fn.kind === "var" && e.fn.name === "perm" && e.args.length === 2) {
|
|
721
|
+
if (!ctx.inSpec)
|
|
722
|
+
throw new Error("perm(a, b) may only be used in //@ specifications");
|
|
723
|
+
const a = resolveExpr(e.args[0], ctx);
|
|
724
|
+
const b = resolveExpr(e.args[1], ctx);
|
|
725
|
+
if (a.ty.kind !== "array" || b.ty.kind !== "array")
|
|
726
|
+
throw new Error(`perm(a, b) requires two array arguments (got ${a.ty.kind} and ${b.ty.kind})`);
|
|
727
|
+
const fn = { kind: "var", name: "Perm", ty: { kind: "unknown" } };
|
|
728
|
+
return { kind: "call", fn, args: [a, b], ty: { kind: "bool" }, callKind: "pure" };
|
|
729
|
+
}
|
|
671
730
|
// Extern dispatch: `NS.method(args)` where NS.method is declared via
|
|
672
731
|
// `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
|
|
673
732
|
// rest of the pipeline sees an ordinary pure function. The extern's
|
|
@@ -711,6 +770,16 @@ function resolveExpr(e, ctx) {
|
|
|
711
770
|
if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
|
|
712
771
|
ty = ctx.fnReturns.get(fn.name);
|
|
713
772
|
}
|
|
773
|
+
// filterMap: `seqOfOption.filter(x => x !== undefined)` (a defined-check,
|
|
774
|
+
// typically with an `x is T` type guard) drops the Nones AND unwraps to
|
|
775
|
+
// seq<T>. Rewrite to a synthetic `filterSome` call lowered to the proven
|
|
776
|
+
// SeqFilterSome preamble (a plain `Map(.value, Filter(.Some?))` wouldn't
|
|
777
|
+
// verify — `.value` is partial).
|
|
778
|
+
if (e.fn.kind === "field" && e.fn.field === "filter" && e.args.length === 1
|
|
779
|
+
&& isDefinedCheckRawLambda(e.args[0])
|
|
780
|
+
&& fn.kind === "field" && fn.obj.ty.kind === "array" && fn.obj.ty.elem.kind === "optional") {
|
|
781
|
+
return { kind: "call", fn: { ...fn, field: "filterSome" }, args: [], ty: { kind: "array", elem: fn.obj.ty.elem.inner }, callKind: "method" };
|
|
782
|
+
}
|
|
714
783
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
715
784
|
}
|
|
716
785
|
case "index": {
|
|
@@ -895,12 +964,21 @@ function resolveExpr(e, ctx) {
|
|
|
895
964
|
let lambdaEnv = ctx.env;
|
|
896
965
|
for (const p of params)
|
|
897
966
|
lambdaEnv = extend(lambdaEnv, p.name, p.ty);
|
|
898
|
-
|
|
967
|
+
// Set returnTy to the lambda's own return annotation (not the enclosing
|
|
968
|
+
// function's), so return-position record literals in the body resolve to
|
|
969
|
+
// their named type rather than an anonymous tuple.
|
|
970
|
+
const lambdaReturnTy = e.returnTsType ? parseTsType(e.returnTsType) : { kind: "unknown" };
|
|
971
|
+
const lambdaCtx = { ...withEnv(ctx, lambdaEnv), inLambda: true, returnTy: lambdaReturnTy };
|
|
899
972
|
// Body: expression (wrap in return stmt) or statement block
|
|
900
973
|
const body = Array.isArray(e.body)
|
|
901
974
|
? resolveBlock(e.body, lambdaCtx)
|
|
902
975
|
: [{ kind: "return", value: resolveExpr(e.body, lambdaCtx) }];
|
|
903
|
-
|
|
976
|
+
// Carry the lambda's type as a fn type when its return is known, so chained
|
|
977
|
+
// array methods (`.map(...).filter(...)`) can infer downstream element types.
|
|
978
|
+
const lamTy = e.returnTsType
|
|
979
|
+
? { kind: "fn", params: params.map(p => p.ty), result: lambdaReturnTy }
|
|
980
|
+
: { kind: "unknown" };
|
|
981
|
+
return { kind: "lambda", params, body, ty: lamTy };
|
|
904
982
|
}
|
|
905
983
|
case "conditional": {
|
|
906
984
|
const cond = resolveExpr(e.cond, ctx);
|
|
@@ -1042,6 +1120,12 @@ function resolveStmt(s, ctx) {
|
|
|
1042
1120
|
? { kind: "optional", inner: init.ty }
|
|
1043
1121
|
: init.ty;
|
|
1044
1122
|
}
|
|
1123
|
+
else if ((declTy.kind === "int" || declTy.kind === "nat") && init.ty.kind === "real" && !ctx.overrides.has(s.name)) {
|
|
1124
|
+
// TS infers `number` (→ int/nat) for an expression LS computes as `real`
|
|
1125
|
+
// (e.g. `a / b`, now real division). `number` can't tell them apart, so
|
|
1126
|
+
// trust the real-valued initializer — unless the user pinned the type.
|
|
1127
|
+
ty = init.ty;
|
|
1128
|
+
}
|
|
1045
1129
|
else {
|
|
1046
1130
|
// Map indexing: TS says T, but access can fail → use Optional<T> from init
|
|
1047
1131
|
ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
|
package/tools/dist/transform.js
CHANGED
|
@@ -32,6 +32,7 @@ function mapExpr(e, f) {
|
|
|
32
32
|
case "app": return { ...e, args: e.args.map(r) };
|
|
33
33
|
case "field": return { ...e, obj: r(e.obj) };
|
|
34
34
|
case "toNat": return { ...e, expr: r(e.expr) };
|
|
35
|
+
case "toReal": return { ...e, expr: r(e.expr) };
|
|
35
36
|
case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
|
|
36
37
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
37
38
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
@@ -154,6 +155,7 @@ function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
|
154
155
|
}
|
|
155
156
|
const _forofCounters = new Map();
|
|
156
157
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
158
|
+
function isIntegral(ty) { return ty.kind === "int" || ty.kind === "nat"; }
|
|
157
159
|
function isArray(ty) { return ty.kind === "array"; }
|
|
158
160
|
function isUser(ty) { return ty.kind === "user"; }
|
|
159
161
|
/** Check if transformed lambda body contains monadic binds. */
|
|
@@ -186,6 +188,8 @@ const OP_MAP = {
|
|
|
186
188
|
const BOOL_OP_MAP = {
|
|
187
189
|
...OP_MAP, "===": "==", "!==": "!=",
|
|
188
190
|
};
|
|
191
|
+
/** Arithmetic + comparison ops eligible for int→real operand coercion. */
|
|
192
|
+
const NUMERIC_OPS = new Set(["+", "-", "*", "/", "===", "!==", ">=", "<=", ">", "<"]);
|
|
189
193
|
function transformExpr(e) { return lowerExpr(e, null); }
|
|
190
194
|
/** Reduce an if/let/return-shaped statement body to a single expression, for
|
|
191
195
|
* expression-only lambda bodies. Returns null for shapes that can't be a pure
|
|
@@ -219,6 +223,19 @@ function flattenLambdaBody(stmts) {
|
|
|
219
223
|
const elseExpr = flattenLambdaBody(first.else.length > 0 ? [...first.else, ...rest] : rest);
|
|
220
224
|
return thenExpr === null || elseExpr === null ? null : { kind: "if", cond: first.cond, then: thenExpr, else: elseExpr };
|
|
221
225
|
}
|
|
226
|
+
// A `switch` lowered to a match-statement: reduce each arm's body to an
|
|
227
|
+
// expression (an arm that doesn't return falls through into `rest`), giving a
|
|
228
|
+
// match-expression — same reduction the `if` case does, one level wider.
|
|
229
|
+
if (first.kind === "match") {
|
|
230
|
+
const arms = [];
|
|
231
|
+
for (const arm of first.arms) {
|
|
232
|
+
const armExpr = flattenLambdaBody([...arm.body, ...rest]);
|
|
233
|
+
if (armExpr === null)
|
|
234
|
+
return null;
|
|
235
|
+
arms.push({ pattern: arm.pattern, body: armExpr });
|
|
236
|
+
}
|
|
237
|
+
return { kind: "match", scrutinee: first.scrutinee, arms };
|
|
238
|
+
}
|
|
222
239
|
return null;
|
|
223
240
|
}
|
|
224
241
|
/**
|
|
@@ -419,6 +436,20 @@ function lowerExpr(e, binds) {
|
|
|
419
436
|
right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
|
|
420
437
|
}
|
|
421
438
|
}
|
|
439
|
+
// Numeric int→real coercion. After resolve, `/` is always real, and any
|
|
440
|
+
// arithmetic/comparison mixing real and integral operands is real-valued.
|
|
441
|
+
// Lift each integral operand to `real` so the backend sees homogeneous
|
|
442
|
+
// real operations (Dafny `as real`, Lean Int→Float).
|
|
443
|
+
if (NUMERIC_OPS.has(e.op)) {
|
|
444
|
+
const realCtx = e.ty.kind === "real" || e.left.ty.kind === "real" || e.right.ty.kind === "real";
|
|
445
|
+
if (realCtx) {
|
|
446
|
+
const lift = (operand) => {
|
|
447
|
+
const lowered = lowerExpr(operand, binds);
|
|
448
|
+
return isIntegral(operand.ty) ? { kind: "toReal", expr: lowered } : lowered;
|
|
449
|
+
};
|
|
450
|
+
return { kind: "binop", op: OP_MAP[e.op] ?? e.op, left: lift(e.left), right: lift(e.right) };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
422
453
|
return {
|
|
423
454
|
kind: "binop",
|
|
424
455
|
op: OP_MAP[e.op] ?? e.op,
|
|
@@ -494,13 +525,23 @@ function lowerExpr(e, binds) {
|
|
|
494
525
|
return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
|
|
495
526
|
return lowerExpr(arg, binds);
|
|
496
527
|
}
|
|
497
|
-
// Math.floor(x):
|
|
528
|
+
// Math.floor(x):
|
|
529
|
+
// - a / b on integral operands → integer floor division, kept in
|
|
530
|
+
// integer arithmetic (JSFloorDiv on Dafny; native Int/Nat `/` floors
|
|
531
|
+
// on Lean). Checked first: after resolve, `a / b` is typed `real`, so
|
|
532
|
+
// the real branch below would otherwise drag it into real arithmetic.
|
|
533
|
+
// - real arg → FloorReal (Dafny's .Floor)
|
|
534
|
+
// - int arg → identity
|
|
498
535
|
if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
499
536
|
const arg = e.args[0];
|
|
537
|
+
if (arg.kind === "binop" && arg.op === "/" && isIntegral(arg.left.ty) && isIntegral(arg.right.ty)) {
|
|
538
|
+
const l = lowerExpr(arg.left, binds), r = lowerExpr(arg.right, binds);
|
|
539
|
+
return _opts.backend === "dafny"
|
|
540
|
+
? { kind: "app", fn: "JSFloorDiv", args: [l, r] }
|
|
541
|
+
: { kind: "binop", op: "/", left: l, right: r };
|
|
542
|
+
}
|
|
500
543
|
if (arg.ty.kind === "real")
|
|
501
544
|
return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
|
|
502
|
-
if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
|
|
503
|
-
return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
|
|
504
545
|
return lowerExpr(arg, binds);
|
|
505
546
|
}
|
|
506
547
|
// Method call: receiver.method(args) → methodCall node
|
|
@@ -642,7 +683,10 @@ function lowerExpr(e, binds) {
|
|
|
642
683
|
return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
|
|
643
684
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
644
685
|
case "lambda": {
|
|
645
|
-
|
|
686
|
+
// Pass the module typeDecls (not []), so type lookups inside the lambda
|
|
687
|
+
// body — e.g. a `switch`'s variant fields — resolve. A bare `[]` left a
|
|
688
|
+
// discriminated-union switch in a lambda with binderless patterns.
|
|
689
|
+
const body = transformStmts(e.body, _typeDecls);
|
|
646
690
|
// Flatten an if/let/return-shaped multi-statement body into a single
|
|
647
691
|
// `return <expr>` so both backends' single-return-lambda fast path emits
|
|
648
692
|
// it (Dafny lambdas are expression-only; Lean prefers the expression form
|
|
@@ -1321,14 +1365,37 @@ function remainingVariant(typeName, cases, typeDecls) {
|
|
|
1321
1365
|
return null;
|
|
1322
1366
|
return remaining[0];
|
|
1323
1367
|
}
|
|
1368
|
+
/** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
|
|
1369
|
+
* `field`, assuming `obj` is a discriminated union with `field` as its
|
|
1370
|
+
* discriminant. When that's NOT so — e.g. `obj` is a plain record with an
|
|
1371
|
+
* enum-typed `field` — the switch is really on the enum VALUE. This returns the
|
|
1372
|
+
* enum scrutinee `obj.field` (+ the field's enum type) to match directly; null
|
|
1373
|
+
* for a genuine discriminant switch or `switch(localVar)`, which callers handle
|
|
1374
|
+
* their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
|
|
1375
|
+
function enumFieldSwitch(s, typeDecls) {
|
|
1376
|
+
if (!s.discriminant)
|
|
1377
|
+
return null;
|
|
1378
|
+
const objBase = s.expr.ty.kind === "user"
|
|
1379
|
+
? (s.expr.ty.name.includes("<") ? s.expr.ty.name.slice(0, s.expr.ty.name.indexOf("<")) : s.expr.ty.name)
|
|
1380
|
+
: undefined;
|
|
1381
|
+
const objDecl = objBase ? typeDecls.find(d => d.name === objBase) : undefined;
|
|
1382
|
+
if (objDecl?.kind === "discriminated-union" && objDecl.discriminant === s.discriminant)
|
|
1383
|
+
return null;
|
|
1384
|
+
const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
|
|
1385
|
+
return {
|
|
1386
|
+
scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
|
|
1387
|
+
enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1324
1390
|
function emitSwitchStmt(s, typeDecls) {
|
|
1325
|
-
const varName = s.expr.kind === "var" ? s.expr.name : "?";
|
|
1326
|
-
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
|
|
1327
1391
|
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1328
|
-
const
|
|
1392
|
+
const ef = enumFieldSwitch(s, typeDecls);
|
|
1393
|
+
const arms = ef
|
|
1394
|
+
? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
|
|
1395
|
+
: buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
|
|
1329
1396
|
if (s.defaultBody.length > 0)
|
|
1330
1397
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
1331
|
-
return { kind: "match", scrutinee:
|
|
1398
|
+
return { kind: "match", scrutinee: ef ? ef.scrutinee : (s.expr.kind === "var" ? s.expr.name : "?"), arms };
|
|
1332
1399
|
}
|
|
1333
1400
|
/** Replace obj.field → replacement var in typed IR.
|
|
1334
1401
|
* Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
|
|
@@ -1472,6 +1539,20 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1472
1539
|
return null;
|
|
1473
1540
|
}
|
|
1474
1541
|
function transformPureSwitch(s, typeDecls) {
|
|
1542
|
+
const ef = enumFieldSwitch(s, typeDecls);
|
|
1543
|
+
if (ef) {
|
|
1544
|
+
const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
|
|
1545
|
+
const arms = buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformPureBody(body, typeDecls));
|
|
1546
|
+
if (!arms)
|
|
1547
|
+
return null;
|
|
1548
|
+
if (s.defaultBody.length > 0) {
|
|
1549
|
+
const body = transformPureBody(s.defaultBody, typeDecls);
|
|
1550
|
+
if (!body)
|
|
1551
|
+
return null;
|
|
1552
|
+
arms.push({ pattern: "_", body });
|
|
1553
|
+
}
|
|
1554
|
+
return { kind: "match", scrutinee: ef.scrutinee, arms };
|
|
1555
|
+
}
|
|
1475
1556
|
const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
|
|
1476
1557
|
if (!typeDecls.find(d => d.name === typeName))
|
|
1477
1558
|
return null;
|
|
@@ -1570,6 +1651,9 @@ function transformTypeDecl(d) {
|
|
|
1570
1651
|
target: d.aliasOfTy,
|
|
1571
1652
|
};
|
|
1572
1653
|
}
|
|
1654
|
+
else if (d.kind === "opaque") {
|
|
1655
|
+
return { kind: "opaque-type", name: d.name };
|
|
1656
|
+
}
|
|
1573
1657
|
else {
|
|
1574
1658
|
return {
|
|
1575
1659
|
kind: "structure", name: d.name,
|
package/tools/dist/typedir.js
CHANGED
|
@@ -4,4 +4,7 @@
|
|
|
4
4
|
* Produced by the resolve pass. Consumed by the transform.
|
|
5
5
|
* Still TS-shaped (not Lean-shaped).
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/** True for an int/nat that came from a TS `bigint` (integer division semantics). */
|
|
8
|
+
export function isBigInt(ty) {
|
|
9
|
+
return (ty.kind === "int" || ty.kind === "nat") && !!ty.big;
|
|
10
|
+
}
|
package/tools/dist/types.js
CHANGED
|
@@ -32,6 +32,11 @@ export function parseTsType(tsType) {
|
|
|
32
32
|
function tyFromTypeNode(tn) {
|
|
33
33
|
if (Node.isParenthesizedTypeNode(tn))
|
|
34
34
|
return tyFromTypeNode(tn.getTypeNode());
|
|
35
|
+
// `readonly T[]` / `readonly [A, B]` — the modifier is a TypeOperator wrapping
|
|
36
|
+
// the array/tuple; verification treats it identically to the mutable form.
|
|
37
|
+
if (Node.isTypeOperatorTypeNode(tn) && tn.getOperator() === SyntaxKind.ReadonlyKeyword) {
|
|
38
|
+
return tyFromTypeNode(tn.getTypeNode());
|
|
39
|
+
}
|
|
35
40
|
if (Node.isUnionTypeNode(tn)) {
|
|
36
41
|
const arms = tn.getTypeNodes();
|
|
37
42
|
const isBoolLit = (a) => Node.isLiteralTypeNode(a) && (a.getLiteral().getKind() === SyntaxKind.TrueKeyword || a.getLiteral().getKind() === SyntaxKind.FalseKeyword);
|
|
@@ -91,8 +96,9 @@ function tyFromTypeNode(tn) {
|
|
|
91
96
|
}
|
|
92
97
|
switch (tn.getKind()) {
|
|
93
98
|
case SyntaxKind.NumberKeyword:
|
|
94
|
-
case SyntaxKind.BigIntKeyword:
|
|
95
99
|
return { kind: "int" };
|
|
100
|
+
case SyntaxKind.BigIntKeyword:
|
|
101
|
+
return { kind: "int", big: true };
|
|
96
102
|
case SyntaxKind.BooleanKeyword:
|
|
97
103
|
return { kind: "bool" };
|
|
98
104
|
case SyntaxKind.StringKeyword:
|
|
@@ -109,7 +115,11 @@ function tyFromTypeNode(tn) {
|
|
|
109
115
|
const args = tn.getTypeArguments();
|
|
110
116
|
if (name === "nat" && args.length === 0)
|
|
111
117
|
return { kind: "nat" };
|
|
112
|
-
if (name === "
|
|
118
|
+
if (name === "real" && args.length === 0)
|
|
119
|
+
return { kind: "real" };
|
|
120
|
+
if (name === "int" && args.length === 0)
|
|
121
|
+
return { kind: "int" };
|
|
122
|
+
if ((name === "Array" || name === "ReadonlyArray") && args.length === 1)
|
|
113
123
|
return { kind: "array", elem: tyFromTypeNode(args[0]) };
|
|
114
124
|
if (name === "Set" && args.length === 1)
|
|
115
125
|
return { kind: "set", elem: tyFromTypeNode(args[0]) };
|