lemmascript 0.5.0 → 0.5.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/package.json +1 -1
- package/tools/dist/dafny-emit.js +9 -0
- package/tools/dist/extract.js +39 -5
- package/tools/dist/resolve.js +41 -0
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -35,6 +35,15 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
35
35
|
"datatype", "type", "const", "ghost", "static",
|
|
36
36
|
"reads", "modifies", "assert", "assume", "print",
|
|
37
37
|
"by", "calc", "reveal",
|
|
38
|
+
// Further reserved words (validated against the Dafny parser) that are also
|
|
39
|
+
// legal TS identifiers. `this` stays excluded above — class methods emit it
|
|
40
|
+
// directly. `then`/`else` already covered.
|
|
41
|
+
"bool", "char", "int", "nat", "real", "string", "object", "array",
|
|
42
|
+
"as", "is", "label", "modify", "expect", "yield", "yields", "returns",
|
|
43
|
+
"unchanged", "witness", "constructor", "iterator", "abstract", "extends",
|
|
44
|
+
"refines", "opened", "provides", "reveals", "include", "newtype",
|
|
45
|
+
"codatatype", "nameonly", "twostate", "opaque", "replaceable", "colemma",
|
|
46
|
+
"copredicate", "inductive",
|
|
38
47
|
]);
|
|
39
48
|
function escapeName(name) {
|
|
40
49
|
// \result is carried through the IR as the var name "\\result"; render it
|
package/tools/dist/extract.js
CHANGED
|
@@ -1502,17 +1502,39 @@ function extractStmts(stmts) {
|
|
|
1502
1502
|
const switchExpr = exprAst.kind === "field" ? exprAst.obj : exprAst;
|
|
1503
1503
|
const cases = [];
|
|
1504
1504
|
let defaultBody = [];
|
|
1505
|
+
// Two JS-`switch` faithfulness concerns the Dafny `match` doesn't share:
|
|
1506
|
+
// (1) Fall-through: stacked `case A: case B: body` is several clauses
|
|
1507
|
+
// where the leading ones have no statements; those labels share the
|
|
1508
|
+
// next clause's body (we duplicate it per label).
|
|
1509
|
+
// (2) `break` is the switch exit, not a loop break. We extract the full
|
|
1510
|
+
// body (extractStmts flattens `{ }` blocks but keeps loop bodies
|
|
1511
|
+
// nested) and strip the *top-level* breaks — so a `break` written
|
|
1512
|
+
// inside a `{ }` case block is stripped, while a `break` inside a
|
|
1513
|
+
// nested loop stays put.
|
|
1514
|
+
const stripExitBreaks = (b) => b.filter(st => st.kind !== "break");
|
|
1515
|
+
let fallthrough = [];
|
|
1505
1516
|
for (const clause of s.getClauses()) {
|
|
1506
1517
|
if (Node.isCaseClause(clause)) {
|
|
1507
1518
|
const label = clause.getExpression().getText().replace(/^["']|["']$/g, "");
|
|
1508
|
-
|
|
1509
|
-
|
|
1519
|
+
if (clause.getStatements().length === 0) {
|
|
1520
|
+
fallthrough.push(label);
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const body = stripExitBreaks(extractStmts(clause.getStatements()));
|
|
1524
|
+
for (const l of fallthrough)
|
|
1525
|
+
cases.push({ label: l, body });
|
|
1526
|
+
cases.push({ label, body });
|
|
1527
|
+
fallthrough = [];
|
|
1510
1528
|
}
|
|
1511
1529
|
else {
|
|
1512
|
-
|
|
1513
|
-
|
|
1530
|
+
defaultBody = stripExitBreaks(extractStmts(clause.getStatements()));
|
|
1531
|
+
for (const l of fallthrough)
|
|
1532
|
+
cases.push({ label: l, body: defaultBody });
|
|
1533
|
+
fallthrough = [];
|
|
1514
1534
|
}
|
|
1515
1535
|
}
|
|
1536
|
+
for (const l of fallthrough)
|
|
1537
|
+
cases.push({ label: l, body: [] });
|
|
1516
1538
|
result.push({ kind: "switch", expr: switchExpr, discriminant, cases, defaultBody, line });
|
|
1517
1539
|
continue;
|
|
1518
1540
|
}
|
|
@@ -1735,7 +1757,19 @@ export function extractModule(sourceFile) {
|
|
|
1735
1757
|
}
|
|
1736
1758
|
const aliasMatch = body.match(/^(\w+)\s*=\s*(.+)$/);
|
|
1737
1759
|
if (aliasMatch) {
|
|
1738
|
-
|
|
1760
|
+
const rhs = aliasMatch[2].trim();
|
|
1761
|
+
// A string-literal union (`= "a" | "b" | …`) becomes an enum datatype —
|
|
1762
|
+
// the same shape a real string-union alias resolves to. Dafny has no
|
|
1763
|
+
// string-literal type, so a plain alias (`type X = "a" | "b"`) would be
|
|
1764
|
+
// invalid. Other RHS forms (`Rule[]`, `number`, `A | B`) fall through.
|
|
1765
|
+
const parts = rhs.split("|").map(s => s.trim());
|
|
1766
|
+
const lits = parts.map(p => p.match(/^["'](.+)["']$/));
|
|
1767
|
+
if (parts.length >= 2 && lits.every(m => m !== null)) {
|
|
1768
|
+
typeDecls.push({ name: aliasMatch[1], kind: "string-union", values: lits.map(m => m[1]) });
|
|
1769
|
+
}
|
|
1770
|
+
else {
|
|
1771
|
+
typeDecls.push({ name: aliasMatch[1], kind: "alias", aliasOf: rhs });
|
|
1772
|
+
}
|
|
1739
1773
|
}
|
|
1740
1774
|
}
|
|
1741
1775
|
for (const range of sourceFile.getLeadingCommentRanges()) {
|
package/tools/dist/resolve.js
CHANGED
|
@@ -290,6 +290,41 @@ function expandAlias(ty, typeDecls, seen = new Set()) {
|
|
|
290
290
|
function getDiscriminant(ctx, typeName) {
|
|
291
291
|
return findDecl(ctx, typeName)?.discriminant;
|
|
292
292
|
}
|
|
293
|
+
// ── Equality hazard: structural in the proof vs reference at runtime ─────────
|
|
294
|
+
// `===`/`!==` is modeled as Dafny structural equality, but the SAME TypeScript
|
|
295
|
+
// runs `===` as JS *reference* equality on objects/arrays. The two agree only
|
|
296
|
+
// when the operand is a primitive at runtime: number / string / bool, or a
|
|
297
|
+
// string-union enum (which runs as a plain string). Records, discriminated
|
|
298
|
+
// unions, arrays, maps, sets, and unresolved generics are reference-compared at
|
|
299
|
+
// runtime, so a structural proof over them is unsound. Returns true for those.
|
|
300
|
+
function refEqHazard(ty, typeDecls) {
|
|
301
|
+
if (ty.kind === "array" || ty.kind === "map" || ty.kind === "set")
|
|
302
|
+
return true;
|
|
303
|
+
if (ty.kind === "user") {
|
|
304
|
+
let decl = typeDecls.find(d => d.name === ty.name);
|
|
305
|
+
if (!decl && ty.name.includes(".")) {
|
|
306
|
+
const tail = ty.name.slice(ty.name.lastIndexOf(".") + 1);
|
|
307
|
+
decl = typeDecls.find(d => d.name === tail);
|
|
308
|
+
}
|
|
309
|
+
if (!decl)
|
|
310
|
+
return true; // generic type parameter / unknown → assume reference
|
|
311
|
+
if (decl.kind === "string-union")
|
|
312
|
+
return false; // runs as a JS string → `===` is structural
|
|
313
|
+
if (decl.kind === "alias")
|
|
314
|
+
return decl.aliasOfTy ? refEqHazard(decl.aliasOfTy, typeDecls) : false;
|
|
315
|
+
return true; // record / discriminated-union → reference at runtime
|
|
316
|
+
}
|
|
317
|
+
return false; // primitives, optional, unknown, fn, void
|
|
318
|
+
}
|
|
319
|
+
const _warnedRefEq = new Set();
|
|
320
|
+
function warnRefEq(op, l, r) {
|
|
321
|
+
const label = (t) => t.kind === "user" ? t.name : t.kind;
|
|
322
|
+
const msg = `'${op}' compares non-primitive operands (${label(l)} ${op} ${label(r)}): structural equality in the proof, but reference equality when this TypeScript runs. Sound only if operands are primitives or a canonical (string/number) encoding; otherwise compare via an explicit structural equals.`;
|
|
323
|
+
if (_warnedRefEq.has(msg))
|
|
324
|
+
return;
|
|
325
|
+
_warnedRefEq.add(msg);
|
|
326
|
+
console.error(`WARNING: ${msg}`);
|
|
327
|
+
}
|
|
293
328
|
/** A type ts-morph handed us that LemmaScript hasn't modeled: contains
|
|
294
329
|
* `unknown` (TS `any`), or a `user` type whose name isn't a known declaration
|
|
295
330
|
* (an opaque expanded union like `"AssistantMsg | ToolMsg"` that ts-morph
|
|
@@ -606,6 +641,11 @@ function resolveExpr(e, ctx) {
|
|
|
606
641
|
if (e.op === "===" || e.op === "!==") {
|
|
607
642
|
left = coerceStr(left, right.ty);
|
|
608
643
|
right = coerceStr(right, left.ty);
|
|
644
|
+
// Spec (`//@`) comparisons are proof-only, so they can't diverge at
|
|
645
|
+
// runtime; only warn on executable code.
|
|
646
|
+
if (!ctx.inSpec && refEqHazard(left.ty, ctx.typeDecls) && refEqHazard(right.ty, ctx.typeDecls)) {
|
|
647
|
+
warnRefEq(e.op, left.ty, right.ty);
|
|
648
|
+
}
|
|
609
649
|
}
|
|
610
650
|
let ty = { kind: "unknown" };
|
|
611
651
|
if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
|
|
@@ -1388,6 +1428,7 @@ function precomputeFieldTypesInner(typeDecls) {
|
|
|
1388
1428
|
}
|
|
1389
1429
|
}
|
|
1390
1430
|
export function resolveModule(raw) {
|
|
1431
|
+
_warnedRefEq.clear();
|
|
1391
1432
|
precomputeFieldTypes(raw.typeDecls);
|
|
1392
1433
|
const pureFns = computePureFns(raw.functions);
|
|
1393
1434
|
// Pre-compute function parameter and return types
|