lemmascript 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/README.md +101 -3
- package/package.json +30 -20
- package/tools/dist/dafny-commands.js +104 -0
- package/tools/dist/dafny-emit.js +449 -0
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +435 -0
- package/tools/dist/ir.js +7 -0
- package/tools/dist/lsc.js +118 -0
- package/tools/dist/rawir.js +10 -0
- package/tools/dist/resolve.js +451 -0
- package/tools/dist/specparser.js +251 -0
- package/tools/dist/transform.js +745 -0
- package/tools/dist/typedir.js +7 -0
- package/tools/dist/types.js +38 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -14
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 midspiral
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
|
-
# LemmaScript
|
|
1
|
+
# LemmaScript (Tech Preview)
|
|
2
2
|
|
|
3
|
-
A verification toolchain for TypeScript.
|
|
3
|
+
A verification toolchain for TypeScript. Write ordinary TypeScript with `//@ ` specification annotations. The toolchain generates verified code from your TypeScript — either Lean 4 (with Velvet/Loom) or Dafny.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
See [SPEC.md](SPEC.md) for the full specification, [DESIGN.md](DESIGN.md) for the Lean backend design, and [DESIGN_DAFNY.md](DESIGN_DAFNY.md) for the Dafny backend design.
|
|
6
|
+
|
|
7
|
+
This is a **Tech Preview**: the core idea is there, but support, semantics, and ergonomics are still evolving.
|
|
8
|
+
|
|
9
|
+
## Case Studies
|
|
10
|
+
|
|
11
|
+
Each case study is verified in both Lean 4 and Dafny from the same annotated TypeScript source.
|
|
12
|
+
|
|
13
|
+
- **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
|
|
14
|
+
- **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
|
|
15
|
+
- **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
**Prerequisites:** Node.js >= 18. For the Lean backend: [elan](https://github.com/leanprover/elan). For the Dafny backend: [Dafny](https://github.com/dafny-lang/dafny) >= 4.x.
|
|
20
|
+
|
|
21
|
+
**Install from npm:**
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install lemmascript
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Or from source:**
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
git clone https://github.com/midspiral/LemmaScript.git
|
|
31
|
+
cd LemmaScript && npm install && npm run build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Lean backend** additionally requires the Loom and Velvet forks:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
git clone https://github.com/namin/loom.git -b lemma ../loom
|
|
38
|
+
git clone https://github.com/namin/velvet.git -b lemma ../velvet
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Lean backend (default)
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx lsc gen src/myModule.ts
|
|
47
|
+
lake build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Dafny backend
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx lsc gen --backend=dafny src/myModule.ts
|
|
54
|
+
npx lsc check --backend=dafny src/myModule.ts
|
|
55
|
+
npx lsc regen --backend=dafny src/myModule.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The Dafny backend generates two files per TS source: `foo.dfy.gen` (always regeneratable) and `foo.dfy` (source of truth, with LLM/user proof additions). The diff between them must be additions-only.
|
|
59
|
+
|
|
60
|
+
## What's Supported
|
|
61
|
+
|
|
62
|
+
### Annotations
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
//@ requires arr.length > 0
|
|
66
|
+
//@ ensures \result >= -1 && \result < arr.length
|
|
67
|
+
//@ invariant 0 <= i && i <= arr.length
|
|
68
|
+
//@ decreases arr.length - i
|
|
69
|
+
//@ type i nat
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## File Structure
|
|
73
|
+
|
|
74
|
+
### Lean backend
|
|
75
|
+
|
|
76
|
+
| File | Generated? | Purpose |
|
|
77
|
+
|------|-----------|---------|
|
|
78
|
+
| `foo.ts` | — | TypeScript source with `//@ ` annotations |
|
|
79
|
+
| `foo.types.lean` | Yes | Lean types, `namespace Pure` defs |
|
|
80
|
+
| `foo.spec.lean` | No | Ghost definitions, helper lemmas |
|
|
81
|
+
| `foo.def.lean` | Yes | Velvet method definitions |
|
|
82
|
+
| `foo.proof.lean` | No | `prove_correct` with proof tactics |
|
|
83
|
+
|
|
84
|
+
### Dafny backend
|
|
85
|
+
|
|
86
|
+
| File | Generated? | Purpose |
|
|
87
|
+
|------|-----------|---------|
|
|
88
|
+
| `foo.ts` | — | TypeScript source with `//@ ` annotations |
|
|
89
|
+
| `foo.dfy.gen` | Yes | Generated Dafny (merge base, always regeneratable) |
|
|
90
|
+
| `foo.dfy` | Yes (initial) | Annotated Dafny (gen + proof additions) |
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
| Example | Pattern |
|
|
95
|
+
|---------|---------|
|
|
96
|
+
| `binarySearch` | Array search, break, Int arithmetic |
|
|
97
|
+
| `linearSearch` | Loop with break, Nat index |
|
|
98
|
+
| `arraySum` | Accumulator, recursive ghost function |
|
|
99
|
+
| `transition` | State machine, enum ADT, inter-method call |
|
|
100
|
+
| `packet` | Discriminated union with data, if-chain → match |
|
|
101
|
+
| `isSorted` | Loop with break, existential in invariant |
|
|
102
|
+
| `maxElement` | If-without-else in loop |
|
|
103
|
+
| `arrayContains` | For-of loop, boolean flag |
|
package/package.json
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemmascript",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "A verification toolchain for TypeScript",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"dev": "tsc --watch",
|
|
10
|
-
"prepublishOnly": "npm run build"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lsc": "tools/dist/lsc.js"
|
|
11
8
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"lean",
|
|
15
|
-
"verification",
|
|
16
|
-
"formal-verification",
|
|
17
|
-
"proof",
|
|
18
|
-
"toolchain"
|
|
9
|
+
"files": [
|
|
10
|
+
"tools/dist"
|
|
19
11
|
],
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tools/tsconfig.json",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"typecheck": "tsc -p tools/tsconfig.json --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"ts-morph": "^25.0.0"
|
|
23
19
|
},
|
|
24
20
|
"devDependencies": {
|
|
25
|
-
"
|
|
26
|
-
|
|
21
|
+
"@types/node": "^25.5.0",
|
|
22
|
+
"tsx": "^4.0.0",
|
|
23
|
+
"typescript": "^5.7.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/midspiral/LemmaScript"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"verification",
|
|
32
|
+
"typescript",
|
|
33
|
+
"lean4",
|
|
34
|
+
"dafny",
|
|
35
|
+
"formal-methods"
|
|
36
|
+
]
|
|
27
37
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dafny backend commands: gen, check, regen.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync } from "fs";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import path from "path";
|
|
7
|
+
function writeGen(genPath, text) {
|
|
8
|
+
writeFileSync(genPath, text);
|
|
9
|
+
console.log(`Generated: ${genPath}`);
|
|
10
|
+
}
|
|
11
|
+
export function dafnyGen(genPath, dfyPath, text) {
|
|
12
|
+
writeGen(genPath, text);
|
|
13
|
+
if (!existsSync(dfyPath)) {
|
|
14
|
+
writeFileSync(dfyPath, text);
|
|
15
|
+
console.log(`Created: ${dfyPath}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function dafnyCheckDiff(genPath, dfyPath) {
|
|
19
|
+
if (!existsSync(dfyPath))
|
|
20
|
+
return true;
|
|
21
|
+
try {
|
|
22
|
+
const diff = execSync(`git diff --no-index -- "${genPath}" "${dfyPath}" 2>/dev/null || true`, { encoding: "utf-8" });
|
|
23
|
+
const deletions = diff.split("\n").filter(l => l.startsWith("-") && !l.startsWith("---"));
|
|
24
|
+
if (deletions.length > 0) {
|
|
25
|
+
console.error(`WARNING: ${path.basename(dfyPath)} has modifications to generated lines (not additions-only):`);
|
|
26
|
+
for (const d of deletions.slice(0, 5))
|
|
27
|
+
console.error(" " + d);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* diff not available */ }
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
export function dafnyVerify(dfyPath, dir) {
|
|
35
|
+
console.log("Running dafny verify...");
|
|
36
|
+
try {
|
|
37
|
+
const content = readFileSync(dfyPath, "utf-8");
|
|
38
|
+
const stdLibFlag = content.includes("import Std.") ? " --standard-libraries" : "";
|
|
39
|
+
execSync(`dafny verify${stdLibFlag} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function dafnySavePatch(genPath, dfyPath, patchPath) {
|
|
47
|
+
try {
|
|
48
|
+
const patch = execSync(`diff -u "${genPath}" "${dfyPath}" || true`, { encoding: "utf-8" });
|
|
49
|
+
writeFileSync(patchPath, patch);
|
|
50
|
+
console.log(`Saved: ${patchPath}`);
|
|
51
|
+
}
|
|
52
|
+
catch { /* diff not available */ }
|
|
53
|
+
}
|
|
54
|
+
function dafnyApplyPatch(genPath, dfyPath, patchPath) {
|
|
55
|
+
if (!existsSync(patchPath))
|
|
56
|
+
return false;
|
|
57
|
+
const patch = readFileSync(patchPath, "utf-8").trim();
|
|
58
|
+
if (!patch)
|
|
59
|
+
return false;
|
|
60
|
+
copyFileSync(genPath, dfyPath);
|
|
61
|
+
try {
|
|
62
|
+
execSync(`patch --no-backup-if-mismatch -p0 "${dfyPath}" < "${patchPath}"`, { stdio: "pipe" });
|
|
63
|
+
console.log(`Applied: ${patchPath}`);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
copyFileSync(genPath, dfyPath);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function dafnyRegen(genPath, dfyPath, patchPath, text, dir) {
|
|
72
|
+
// 1. No .dfy yet — create both, verify, done
|
|
73
|
+
if (!existsSync(dfyPath)) {
|
|
74
|
+
dafnyGen(genPath, dfyPath, text);
|
|
75
|
+
if (!dafnyVerify(dfyPath, dir)) {
|
|
76
|
+
console.error(`FAILED: ${path.basename(dfyPath)} verification failed on first run.`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 2. Capture patch from current gen → dfy BEFORE overwriting gen
|
|
82
|
+
let hasPatch = false;
|
|
83
|
+
if (existsSync(genPath)) {
|
|
84
|
+
dafnySavePatch(genPath, dfyPath, patchPath);
|
|
85
|
+
hasPatch = readFileSync(patchPath, "utf-8").trim().length > 0;
|
|
86
|
+
}
|
|
87
|
+
// 3. Write new gen
|
|
88
|
+
writeGen(genPath, text);
|
|
89
|
+
// 4. Try verifying existing dfy as-is
|
|
90
|
+
if (dafnyVerify(dfyPath, dir))
|
|
91
|
+
return;
|
|
92
|
+
// 5. Failed — try applying captured patch to new gen
|
|
93
|
+
if (hasPatch) {
|
|
94
|
+
console.log("Verification failed. Trying to apply patch...");
|
|
95
|
+
if (dafnyApplyPatch(genPath, dfyPath, patchPath) && dafnyVerify(dfyPath, dir))
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 6. Needs LLM re-adaptation
|
|
99
|
+
console.error(`FAILED: ${path.basename(dfyPath)} needs manual re-adaptation.`);
|
|
100
|
+
console.error(` ${genPath} has the new generated code.`);
|
|
101
|
+
if (hasPatch)
|
|
102
|
+
console.error(` ${patchPath} has the captured patch.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|