lemmascript 0.5.22 → 0.6.1
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 +28 -26
- package/tools/dist/extract.js +29 -7
- package/tools/dist/lean-emit.js +3 -0
- package/tools/dist/lsc.js +7 -1
- package/tools/dist/resolve.js +58 -13
- package/tools/dist/specparser.js +10 -8
- package/tools/dist/transform.js +8 -5
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Each example and case study is verified in Lean 4 and/or Dafny from the same ann
|
|
|
14
14
|
|
|
15
15
|
See the internal [examples](examples).
|
|
16
16
|
|
|
17
|
-
See the external case studies:
|
|
17
|
+
See the external [case studies](https://github.com/search?q=topic%3Alemmascript+topic%3Acase-study&type=repositories):
|
|
18
18
|
- **[collab-todo-lemmascript](https://github.com/midspiral/collab-todo-lemmascript/)** — collaborative task management web app (React + Supabase) with a verified domain model. Single `domain.ts` imported directly by the UI, hooks, and edge functions — no adapter layer. 123 Dafny lemmas (120 in a separate `domain.proofs.dfy`): 16-conjunct invariant preserved across 25 single-project + 3 cross-project actions, NoOp completeness/soundness, initialization. Dafny only.
|
|
19
19
|
- **[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).
|
|
20
20
|
- **[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).
|
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -189,12 +189,12 @@ function escapeGeneratedName(name) {
|
|
|
189
189
|
function paramList(params) {
|
|
190
190
|
return params.map(p => `${escapeName(p.name)}: ${tyToDafny(p.type)}`).join(", ");
|
|
191
191
|
}
|
|
192
|
-
/** Format a method signature header, omitting `returns` for void
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
function methodHeader(prefix, params, returnType, scope) {
|
|
192
|
+
/** Format a method signature header, normally omitting `returns` for void
|
|
193
|
+
* methods. A body-less impure extern opts into a Unit out-parameter so the
|
|
194
|
+
* shared method-call lifting can bind its call like any other expression. */
|
|
195
|
+
function methodHeader(prefix, params, returnType, scope, includeVoidReturn = false) {
|
|
196
196
|
const sig = `${prefix}(${paramList(params)})`;
|
|
197
|
-
if (returnType.kind === "void")
|
|
197
|
+
if (returnType.kind === "void" && !includeVoidReturn)
|
|
198
198
|
return sig;
|
|
199
199
|
// The out-parameter is `res` by default, but a param (an Express handler's
|
|
200
200
|
// `(req, res)`), body local, or callee named `res` would shadow it. Check only
|
|
@@ -993,10 +993,20 @@ function emitDecl(d) {
|
|
|
993
993
|
return `const ${escapeName(d.name)}: ${tyToDafny(d.type)} := ${emitExpr(d.value)}`;
|
|
994
994
|
}
|
|
995
995
|
case "extern": {
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
//
|
|
996
|
+
// `:axiom` makes Dafny accept the missing body. Pure externs are
|
|
997
|
+
// uninterpreted functions (deterministic/extensional); impure externs are
|
|
998
|
+
// methods, so every invocation gets an independent arbitrary result
|
|
999
|
+
// constrained only by its per-call contract.
|
|
999
1000
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
1001
|
+
if (d.impure) {
|
|
1002
|
+
const scope = { requires: d.requires, ensures: d.ensures, body: [] };
|
|
1003
|
+
const lines = [methodHeader(`method {:axiom} ${escapeName(d.name)}${tp}`, d.params, d.returnType, scope, true)];
|
|
1004
|
+
for (const r of d.requires)
|
|
1005
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
1006
|
+
for (const e of d.ensures)
|
|
1007
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
1008
|
+
return lines.join("\n");
|
|
1009
|
+
}
|
|
1000
1010
|
const lines = [`function {:axiom} ${escapeName(d.name)}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
1001
1011
|
for (const r of d.requires)
|
|
1002
1012
|
lines.push(` requires ${emitExpr(r)}`);
|
|
@@ -1583,10 +1593,8 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1583
1593
|
// skipped when the corresponding pure def was actually emitted.
|
|
1584
1594
|
const emittedPureDefs = new Set();
|
|
1585
1595
|
// Emit a decl, rolling back any preamble requirements it registered if it
|
|
1586
|
-
// throws
|
|
1587
|
-
//
|
|
1588
|
-
// preamble (e.g. `type Unknown` from a skipped const whose head is
|
|
1589
|
-
// `unknown`-typed but whose value expr is unsupported).
|
|
1596
|
+
// throws, so callers that catch an emission error never observe partial
|
|
1597
|
+
// emitter state.
|
|
1590
1598
|
const emitDeclTx = (d) => {
|
|
1591
1599
|
const saved = new Set(_neededPreambles);
|
|
1592
1600
|
try {
|
|
@@ -1601,13 +1609,12 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1601
1609
|
};
|
|
1602
1610
|
// Emit declarations
|
|
1603
1611
|
const declLines = [];
|
|
1604
|
-
const skipped = [];
|
|
1605
1612
|
for (const decl of file.decls) {
|
|
1606
1613
|
if (decl.kind === "method" && emittedPureDefs.has(decl.name))
|
|
1607
1614
|
continue;
|
|
1608
1615
|
if (decl.kind === "namespace") {
|
|
1609
|
-
// Emit each inner
|
|
1610
|
-
//
|
|
1616
|
+
// Emit each inner declaration separately so failures name the exact
|
|
1617
|
+
// pure declaration that could not be translated.
|
|
1611
1618
|
for (const inner of decl.decls) {
|
|
1612
1619
|
try {
|
|
1613
1620
|
declLines.push("");
|
|
@@ -1617,10 +1624,9 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1617
1624
|
}
|
|
1618
1625
|
catch (e) {
|
|
1619
1626
|
const name = "name" in inner ? inner.name : "unknown";
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
skipped.push(name);
|
|
1627
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1628
|
+
const source = tsFileName ? ` in ${tsFileName}` : "";
|
|
1629
|
+
throw new Error(`Dafny emission failed for '${name}'${source}: ${reason}`);
|
|
1624
1630
|
}
|
|
1625
1631
|
}
|
|
1626
1632
|
continue;
|
|
@@ -1633,15 +1639,11 @@ export function emitDafnyFile(file, tsFileName, opts) {
|
|
|
1633
1639
|
}
|
|
1634
1640
|
catch (e) {
|
|
1635
1641
|
const name = "name" in decl ? decl.name : "unknown";
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
skipped.push(name);
|
|
1642
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1643
|
+
const source = tsFileName ? ` in ${tsFileName}` : "";
|
|
1644
|
+
throw new Error(`Dafny emission failed for '${name}'${source}: ${reason}`);
|
|
1640
1645
|
}
|
|
1641
1646
|
}
|
|
1642
|
-
if (skipped.length > 0) {
|
|
1643
|
-
console.error(`WARNING: ${skipped.length} declaration(s) skipped: ${skipped.join(", ")}`);
|
|
1644
|
-
}
|
|
1645
1647
|
// Build output with needed preambles
|
|
1646
1648
|
const lines = [];
|
|
1647
1649
|
if (tsFileName)
|
package/tools/dist/extract.js
CHANGED
|
@@ -32,7 +32,8 @@ function withHavocKey(key, fn) {
|
|
|
32
32
|
}
|
|
33
33
|
/** Auto-detected cross-file calls. Populated by `extractExpr` whenever it sees
|
|
34
34
|
* a call `Obj.method(...)` or `foo(...)` whose ts-morph symbol resolves to a
|
|
35
|
-
* different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat
|
|
35
|
+
* different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat>`
|
|
36
|
+
* by default, or as a body-less method when the source has `//@ impure`.
|
|
36
37
|
* Cleared at the start of every `extractModule`. */
|
|
37
38
|
const _externs = new Map();
|
|
38
39
|
/** Signature types of *kept* externs, for the imported-type resolver: a type
|
|
@@ -163,7 +164,8 @@ function detectCrossFileExtern(callee, sourceFile, sigTypesOut) {
|
|
|
163
164
|
const annots = collectFunctionAnnotations(externalDecl);
|
|
164
165
|
const requires = annots.filter(a => a.kind === "requires").map(a => a.expr);
|
|
165
166
|
const ensures = annots.filter(a => a.kind === "ensures").map(a => a.expr);
|
|
166
|
-
|
|
167
|
+
const impure = hasBareFunctionAnnotation(externalDecl, "impure");
|
|
168
|
+
return { qualified, flat, typeParams, params, returnType, requires, ensures, impure };
|
|
167
169
|
}
|
|
168
170
|
/** Build a concat-tree from a mixed list of literal and SpreadElement nodes.
|
|
169
171
|
* Literals collapse into arrayLiteral segments; spreads become bare expressions;
|
|
@@ -761,17 +763,27 @@ function collectFunctionAnnotations(fn) {
|
|
|
761
763
|
}
|
|
762
764
|
return collectAnnotations(fn);
|
|
763
765
|
}
|
|
764
|
-
/** Check for bare `//@ pure`
|
|
765
|
-
|
|
766
|
+
/** Check for a bare function annotation such as `//@ pure` or `//@ impure`.
|
|
767
|
+
* Function annotations may precede the declaration or its first statement. */
|
|
768
|
+
function hasBareFunctionAnnotation(node, keyword, body) {
|
|
769
|
+
if (!body) {
|
|
770
|
+
const fnBody = node.getBody?.();
|
|
771
|
+
if (fnBody && Node.isBlock(fnBody))
|
|
772
|
+
body = fnBody.getStatements();
|
|
773
|
+
}
|
|
766
774
|
const nodes = body && body.length > 0 ? [node, body[0]] : [node];
|
|
767
775
|
for (const n of nodes) {
|
|
768
776
|
for (const range of n.getLeadingCommentRanges()) {
|
|
769
|
-
if (range.getText().trim() ===
|
|
777
|
+
if (range.getText().trim() === `//@ ${keyword}`)
|
|
770
778
|
return true;
|
|
771
779
|
}
|
|
772
780
|
}
|
|
773
781
|
return false;
|
|
774
782
|
}
|
|
783
|
+
/** Check for bare `//@ pure` annotation (no expression). */
|
|
784
|
+
function hasPureAnnotation(node, body) {
|
|
785
|
+
return hasBareFunctionAnnotation(node, "pure", body);
|
|
786
|
+
}
|
|
775
787
|
// ── Type declaration extraction ──────────────────────────────
|
|
776
788
|
function extractTypeDecl(decl, extraDecls) {
|
|
777
789
|
const name = decl.getName();
|
|
@@ -2158,7 +2170,8 @@ export function extractModule(sourceFile) {
|
|
|
2158
2170
|
});
|
|
2159
2171
|
}
|
|
2160
2172
|
catch (e) {
|
|
2161
|
-
|
|
2173
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2174
|
+
throw new Error(`Failed to extract const '${decl.getName()}' at ${sourceFile.getBaseName()}:${decl.getStartLineNumber()}: ${reason}`);
|
|
2162
2175
|
}
|
|
2163
2176
|
}
|
|
2164
2177
|
}
|
|
@@ -2234,6 +2247,12 @@ export function extractModule(sourceFile) {
|
|
|
2234
2247
|
}
|
|
2235
2248
|
return false;
|
|
2236
2249
|
}
|
|
2250
|
+
function hasImpure(f) {
|
|
2251
|
+
if (hasBareFunctionAnnotation(f.node, "impure"))
|
|
2252
|
+
return true;
|
|
2253
|
+
return !!f.parentStmt && f.parentStmt.getLeadingCommentRanges()
|
|
2254
|
+
.some(r => r.getText().trim() === "//@ impure");
|
|
2255
|
+
}
|
|
2237
2256
|
// `//@ extern NS.method` registers the extern under a *dotted* qualified name,
|
|
2238
2257
|
// so a real `NS.method(args)` call dispatches to it (resolve.ts) with no
|
|
2239
2258
|
// wrapper — e.g. `//@ extern fs.readFileSync` lets you call `fs.readFileSync`
|
|
@@ -2276,7 +2295,10 @@ export function extractModule(sourceFile) {
|
|
|
2276
2295
|
const annots = collectFunctionAnnotations(f.node);
|
|
2277
2296
|
const requires = annots.filter(a => a.kind === "requires").map(a => a.expr);
|
|
2278
2297
|
const ensures = annots.filter(a => a.kind === "ensures").map(a => a.expr);
|
|
2279
|
-
_externs.set(qualified, {
|
|
2298
|
+
_externs.set(qualified, {
|
|
2299
|
+
qualified, flat, typeParams, params, returnType, requires, ensures,
|
|
2300
|
+
impure: hasImpure(f),
|
|
2301
|
+
});
|
|
2280
2302
|
}
|
|
2281
2303
|
// If any function has //@ verify, only extract those (brownfield mode).
|
|
2282
2304
|
// For expression-body arrows, //@ verify may be on the parent variable statement.
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -838,6 +838,9 @@ function emitDecl(d) {
|
|
|
838
838
|
case "const":
|
|
839
839
|
return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
|
|
840
840
|
case "extern": {
|
|
841
|
+
if (d.impure) {
|
|
842
|
+
throw new Error("//@ impure extern is not supported in the Lean backend");
|
|
843
|
+
}
|
|
841
844
|
// Mirror Dafny's `function {:axiom}`: an uninterpreted total function.
|
|
842
845
|
// In Lean that is an `opaque` declaration (sound — it commits to no body,
|
|
843
846
|
// only to the type being inhabited). Any `requires`/`ensures` the source
|
package/tools/dist/lsc.js
CHANGED
|
@@ -337,4 +337,10 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags, noVerify = false
|
|
|
337
337
|
console.error(`Unknown command: ${cmd}`);
|
|
338
338
|
process.exit(1);
|
|
339
339
|
}
|
|
340
|
-
|
|
340
|
+
try {
|
|
341
|
+
main();
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
console.error(`ERROR: ${e instanceof Error ? e.message : String(e)}`);
|
|
345
|
+
process.exitCode = 1;
|
|
346
|
+
}
|
package/tools/dist/resolve.js
CHANGED
|
@@ -453,12 +453,21 @@ function classifyCall(fn, ctx) {
|
|
|
453
453
|
return "pure";
|
|
454
454
|
if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
|
|
455
455
|
return "spec-pure";
|
|
456
|
-
// Bare-name
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
456
|
+
// Bare-name externs are pure by default. `//@ impure` externs are method
|
|
457
|
+
// calls: lift them to statement-level binds so repeated invocations remain
|
|
458
|
+
// independent. A method call cannot occur in a spec or lambda expression.
|
|
459
|
+
if (fn.kind === "var") {
|
|
460
|
+
const ext = ctx.externs.get(fn.name);
|
|
461
|
+
if (ext) {
|
|
462
|
+
if (!ext.impure)
|
|
463
|
+
return "pure";
|
|
464
|
+
if (ctx.inSpec)
|
|
465
|
+
throw new Error(`impure extern ${fn.name} cannot be called from a specification`);
|
|
466
|
+
if (ctx.inLambda)
|
|
467
|
+
throw new Error(`impure extern ${fn.name} cannot be called from a lambda`);
|
|
468
|
+
return "method";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
462
471
|
if (fn.kind === "var" && lookup(ctx.env, fn.name)?.kind === "fn")
|
|
463
472
|
return "pure";
|
|
464
473
|
if (fn.kind === "var" && ctx.inSpec) {
|
|
@@ -848,15 +857,23 @@ function resolveExpr(e, ctx) {
|
|
|
848
857
|
}
|
|
849
858
|
// Extern dispatch: `NS.method(args)` where NS.method is declared via
|
|
850
859
|
// `//@ extern`. Rewrite into a flat-name call (`NS_method(args)`) so the
|
|
851
|
-
// rest of the pipeline sees an ordinary
|
|
852
|
-
//
|
|
860
|
+
// rest of the pipeline sees an ordinary named call. Pure externs remain
|
|
861
|
+
// expressions; `//@ impure` externs are lifted as method calls.
|
|
853
862
|
if (e.fn.kind === "field" && e.fn.obj.kind === "var") {
|
|
854
863
|
const qualified = `${e.fn.obj.name}.${e.fn.field}`;
|
|
855
864
|
const ext = ctx.externs.get(qualified);
|
|
856
865
|
if (ext) {
|
|
866
|
+
if (ext.impure && ctx.inSpec)
|
|
867
|
+
throw new Error(`impure extern ${qualified} cannot be called from a specification`);
|
|
868
|
+
if (ext.impure && ctx.inLambda)
|
|
869
|
+
throw new Error(`impure extern ${qualified} cannot be called from a lambda`);
|
|
857
870
|
const args = e.args.map(a => resolveExpr(a, ctx));
|
|
858
871
|
const fn = { kind: "var", name: ext.flat, ty: { kind: "unknown" } };
|
|
859
|
-
return {
|
|
872
|
+
return {
|
|
873
|
+
kind: "call", fn, args, ty: ext.returnTy,
|
|
874
|
+
callKind: ext.impure ? "method" : "pure",
|
|
875
|
+
paramTys: ext.params,
|
|
876
|
+
};
|
|
860
877
|
}
|
|
861
878
|
}
|
|
862
879
|
const fn = resolveExpr(e.fn, ctx);
|
|
@@ -1618,8 +1635,33 @@ function collectCallsStmts(stmts, fns, out) {
|
|
|
1618
1635
|
}
|
|
1619
1636
|
}
|
|
1620
1637
|
}
|
|
1621
|
-
|
|
1638
|
+
/** Dotted/bare spelling of a raw call target, when statically named. */
|
|
1639
|
+
function rawCalleeName(e) {
|
|
1640
|
+
if (e.kind === "var")
|
|
1641
|
+
return e.name;
|
|
1642
|
+
if (e.kind === "field") {
|
|
1643
|
+
const obj = rawCalleeName(e.obj);
|
|
1644
|
+
return obj ? `${obj}.${e.field}` : null;
|
|
1645
|
+
}
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
/** Whether a raw function body invokes any extern marked `//@ impure`. */
|
|
1649
|
+
function containsImpureExternCall(v, names) {
|
|
1650
|
+
if (Array.isArray(v))
|
|
1651
|
+
return v.some(x => containsImpureExternCall(x, names));
|
|
1652
|
+
if (v === null || typeof v !== "object")
|
|
1653
|
+
return false;
|
|
1654
|
+
const node = v;
|
|
1655
|
+
if (node.kind === "call" && node.fn) {
|
|
1656
|
+
const callee = rawCalleeName(node.fn);
|
|
1657
|
+
if (callee && names.has(callee))
|
|
1658
|
+
return true;
|
|
1659
|
+
}
|
|
1660
|
+
return Object.values(v).some(x => containsImpureExternCall(x, names));
|
|
1661
|
+
}
|
|
1662
|
+
function computePureFns(functions, externDecls) {
|
|
1622
1663
|
const allFnNames = new Set(functions.map(fn => fn.name));
|
|
1664
|
+
const impureExternNames = new Set(externDecls.filter(ext => ext.impure).flatMap(ext => [ext.qualified, ext.flat]));
|
|
1623
1665
|
// //@ pure functions are always considered pure — never taint callers
|
|
1624
1666
|
const forcePure = new Set(functions.filter(fn => fn.pure).map(fn => fn.name));
|
|
1625
1667
|
// Build call graph: fn → set of same-file functions it calls
|
|
@@ -1630,7 +1672,9 @@ function computePureFns(functions) {
|
|
|
1630
1672
|
callGraph.set(fn.name, calls);
|
|
1631
1673
|
}
|
|
1632
1674
|
// Seed: syntactically non-pure functions (skip //@ pure)
|
|
1633
|
-
const nonPure = new Set(functions.filter(fn => !forcePure.has(fn.name) &&
|
|
1675
|
+
const nonPure = new Set(functions.filter(fn => !forcePure.has(fn.name) &&
|
|
1676
|
+
(!isSyntacticallyPure(fn.body) || containsImpureExternCall(fn.body, impureExternNames)))
|
|
1677
|
+
.map(fn => fn.name));
|
|
1634
1678
|
// Build reverse graph: fn → set of functions that call it
|
|
1635
1679
|
const callers = new Map();
|
|
1636
1680
|
for (const name of allFnNames)
|
|
@@ -1763,7 +1807,7 @@ function precomputeFieldTypesInner(typeDecls) {
|
|
|
1763
1807
|
export function resolveModule(raw) {
|
|
1764
1808
|
_warnedRefEq.clear();
|
|
1765
1809
|
precomputeFieldTypes(raw.typeDecls);
|
|
1766
|
-
const pureFns = computePureFns(raw.functions);
|
|
1810
|
+
const pureFns = computePureFns(raw.functions, raw.externs ?? []);
|
|
1767
1811
|
// Pre-compute function parameter and return types
|
|
1768
1812
|
const fnParams = new Map();
|
|
1769
1813
|
const fnReturns = new Map();
|
|
@@ -1783,7 +1827,7 @@ export function resolveModule(raw) {
|
|
|
1783
1827
|
for (const ext of raw.externs ?? []) {
|
|
1784
1828
|
const params = ext.params.map(p => parseTsType(p.tsType));
|
|
1785
1829
|
const returnTy = parseTsType(ext.returnType);
|
|
1786
|
-
externs.set(ext.qualified, { flat: ext.flat, params, returnTy });
|
|
1830
|
+
externs.set(ext.qualified, { flat: ext.flat, params, returnTy, impure: ext.impure });
|
|
1787
1831
|
if (!ext.qualified.includes("."))
|
|
1788
1832
|
fnReturns.set(ext.qualified, returnTy);
|
|
1789
1833
|
}
|
|
@@ -1821,6 +1865,7 @@ export function resolveModule(raw) {
|
|
|
1821
1865
|
returnTy: sig.returnTy,
|
|
1822
1866
|
requires,
|
|
1823
1867
|
ensures,
|
|
1868
|
+
impure: ext.impure,
|
|
1824
1869
|
};
|
|
1825
1870
|
});
|
|
1826
1871
|
const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, fnReturns, externs, inSpec: false, inLambda: false, narrowedPaths: [], narrowedIndices: [] };
|
package/tools/dist/specparser.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { normalizeBigIntLiteral } from "./rawir.js";
|
|
6
6
|
const MULTI_OPS = ["<==>", "==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
const
|
|
7
|
+
/** Numeric literals accepted in specs. BigInts are recognized first so their
|
|
8
|
+
* exact value never passes through Number; ordinary numbers additionally
|
|
9
|
+
* allow TypeScript's fractional and exponent forms. */
|
|
10
|
+
const BIGINT_LITERAL = /^(?:(?:0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*)|(?:0[bB][01](?:_?[01])*)|(?:0[oO][0-7](?:_?[0-7])*)|(?:[0-9](?:_?[0-9])*))n/;
|
|
11
|
+
const NUMBER_LITERAL = /^(?:(?:0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*)|(?:0[bB][01](?:_?[01])*)|(?:0[oO][0-7](?:_?[0-7])*)|(?:(?:[0-9](?:_?[0-9])*)(?:\.(?:[0-9](?:_?[0-9])*)?)?|\.(?:[0-9](?:_?[0-9])*))(?:[eE][+-]?(?:[0-9](?:_?[0-9])*))?)/;
|
|
12
12
|
function tokenize(input) {
|
|
13
13
|
const tokens = [];
|
|
14
14
|
let i = 0;
|
|
@@ -47,15 +47,17 @@ function tokenize(input) {
|
|
|
47
47
|
tokens.push({ type: "str", value: s });
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
|
-
if (/[0-9]/.test(input[i])) {
|
|
51
|
-
const
|
|
50
|
+
if (/[0-9]/.test(input[i]) || (input[i] === "." && /[0-9]/.test(input[i + 1]))) {
|
|
51
|
+
const rest = input.slice(i);
|
|
52
|
+
const bigintMatch = rest.match(BIGINT_LITERAL);
|
|
53
|
+
const match = bigintMatch ?? rest.match(NUMBER_LITERAL);
|
|
52
54
|
if (!match)
|
|
53
55
|
throw new Error(`Invalid numeric literal at ${i} in: ${input}`);
|
|
54
56
|
const text = match[0];
|
|
55
57
|
i += text.length;
|
|
56
58
|
// The `n` suffix is meaningful, not noise: a BigInt keeps its exact value
|
|
57
59
|
// as a decimal string instead of being rounded into a double.
|
|
58
|
-
if (
|
|
60
|
+
if (bigintMatch)
|
|
59
61
|
tokens.push({ type: "bigint", value: normalizeBigIntLiteral(text) });
|
|
60
62
|
else
|
|
61
63
|
tokens.push({ type: "num", value: Number(text.replace(/_/g, "")) });
|
package/tools/dist/transform.js
CHANGED
|
@@ -2508,10 +2508,10 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2508
2508
|
// Lean module base — overridable via `//@ lean-module` (see lsc.ts). Only the
|
|
2509
2509
|
// def→types import below reads it; Dafny never passes an override.
|
|
2510
2510
|
const moduleBase = moduleBaseOverride ?? base;
|
|
2511
|
-
// Externs:
|
|
2512
|
-
//
|
|
2513
|
-
//
|
|
2514
|
-
//
|
|
2511
|
+
// Externs: pure declarations become uninterpreted functions; `//@ impure`
|
|
2512
|
+
// declarations become body-less methods so calls have independent results.
|
|
2513
|
+
// Contracts come along in either case. A pure extern's `\result` denotes its
|
|
2514
|
+
// application; an impure extern keeps `\result` for the method out-parameter.
|
|
2515
2515
|
const externDecls = (mod.externs ?? []).map(ext => {
|
|
2516
2516
|
const fnCall = { kind: "app", fn: ext.flat, args: ext.params.map(p => ({ kind: "var", name: p.name })) };
|
|
2517
2517
|
return {
|
|
@@ -2521,7 +2521,10 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
|
|
|
2521
2521
|
params: ext.params.map(p => ({ name: p.name, type: p.ty })),
|
|
2522
2522
|
returnType: ext.returnTy,
|
|
2523
2523
|
requires: ext.requires.map(transformExpr),
|
|
2524
|
-
ensures: ext.ensures.map(e =>
|
|
2524
|
+
ensures: ext.ensures.map(e => ext.impure
|
|
2525
|
+
? transformExpr(e)
|
|
2526
|
+
: replaceVar(transformExpr(e), "\\result", fnCall)),
|
|
2527
|
+
impure: ext.impure,
|
|
2525
2528
|
};
|
|
2526
2529
|
});
|
|
2527
2530
|
// Def file: Velvet methods
|