lemmascript 0.5.21 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.5.21",
3
+ "version": "0.6.0",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1583,10 +1583,8 @@ export function emitDafnyFile(file, tsFileName, opts) {
1583
1583
  // skipped when the corresponding pure def was actually emitted.
1584
1584
  const emittedPureDefs = new Set();
1585
1585
  // Emit a decl, rolling back any preamble requirements it registered if it
1586
- // throws. A skipped decl must contribute neither text nor preambles — else a
1587
- // side-effecting `needPreamble` from a half-emitted decl leaves an unused
1588
- // preamble (e.g. `type Unknown` from a skipped const whose head is
1589
- // `unknown`-typed but whose value expr is unsupported).
1586
+ // throws, so callers that catch an emission error never observe partial
1587
+ // emitter state.
1590
1588
  const emitDeclTx = (d) => {
1591
1589
  const saved = new Set(_neededPreambles);
1592
1590
  try {
@@ -1601,13 +1599,12 @@ export function emitDafnyFile(file, tsFileName, opts) {
1601
1599
  };
1602
1600
  // Emit declarations
1603
1601
  const declLines = [];
1604
- const skipped = [];
1605
1602
  for (const decl of file.decls) {
1606
1603
  if (decl.kind === "method" && emittedPureDefs.has(decl.name))
1607
1604
  continue;
1608
1605
  if (decl.kind === "namespace") {
1609
- // Emit each inner decl individually if one fails, the rest survive
1610
- // and failed defs fall back to their method wrappers
1606
+ // Emit each inner declaration separately so failures name the exact
1607
+ // pure declaration that could not be translated.
1611
1608
  for (const inner of decl.decls) {
1612
1609
  try {
1613
1610
  declLines.push("");
@@ -1617,10 +1614,9 @@ export function emitDafnyFile(file, tsFileName, opts) {
1617
1614
  }
1618
1615
  catch (e) {
1619
1616
  const name = "name" in inner ? inner.name : "unknown";
1620
- const msg = e.message;
1621
- console.error(`WARNING: skipping pure '${name}': ${msg}`);
1622
- declLines.push(`\n// LemmaScript: skipped pure ${name}`);
1623
- skipped.push(name);
1617
+ const reason = e instanceof Error ? e.message : String(e);
1618
+ const source = tsFileName ? ` in ${tsFileName}` : "";
1619
+ throw new Error(`Dafny emission failed for '${name}'${source}: ${reason}`);
1624
1620
  }
1625
1621
  }
1626
1622
  continue;
@@ -1633,15 +1629,11 @@ export function emitDafnyFile(file, tsFileName, opts) {
1633
1629
  }
1634
1630
  catch (e) {
1635
1631
  const name = "name" in decl ? decl.name : "unknown";
1636
- const msg = e.message;
1637
- console.error(`WARNING: skipping '${name}': ${msg}`);
1638
- declLines.push(`// LemmaScript: skipped ${name}`);
1639
- skipped.push(name);
1632
+ const reason = e instanceof Error ? e.message : String(e);
1633
+ const source = tsFileName ? ` in ${tsFileName}` : "";
1634
+ throw new Error(`Dafny emission failed for '${name}'${source}: ${reason}`);
1640
1635
  }
1641
1636
  }
1642
- if (skipped.length > 0) {
1643
- console.error(`WARNING: ${skipped.length} declaration(s) skipped: ${skipped.join(", ")}`);
1644
- }
1645
1637
  // Build output with needed preambles
1646
1638
  const lines = [];
1647
1639
  if (tsFileName)
@@ -2028,6 +2028,17 @@ export function extractModule(sourceFile) {
2028
2028
  // here), deduped by qualified name.
2029
2029
  _externs.clear();
2030
2030
  _externSigTypes.length = 0;
2031
+ // Match a `//@ <kw>` directive only as the first non-whitespace on a line,
2032
+ // so prose mentioning an annotation does not activate it.
2033
+ function hasLineDirective(text, kw) {
2034
+ return new RegExp(String.raw `^[ \t]*//@ ${kw}\b`, "m").test(text);
2035
+ }
2036
+ // Declaration-level directives must be attached as leading comments. Do
2037
+ // not scan a function's whole body: a statement-level `//@ skip` inside it
2038
+ // must not omit the enclosing function.
2039
+ function hasLeadingDirective(node, kw) {
2040
+ return node.getLeadingCommentRanges().some(r => hasLineDirective(r.getText(), kw));
2041
+ }
2031
2042
  // Share the module's ts-morph Project with parseTsType (scratch source file
2032
2043
  // for type-string parsing). Done before declare-type parsing so any
2033
2044
  // parseTsType call downstream uses the same Project.
@@ -2124,6 +2135,8 @@ export function extractModule(sourceFile) {
2124
2135
  const constants = [];
2125
2136
  for (const stmt of sourceFile.getStatements()) {
2126
2137
  if (Node.isVariableStatement(stmt)) {
2138
+ if (hasLeadingDirective(stmt, "skip"))
2139
+ continue;
2127
2140
  for (const decl of stmt.getDeclarationList().getDeclarations()) {
2128
2141
  if (stmt.getDeclarationList().getFlags() & 2 /* const */) {
2129
2142
  const init = decl.getInitializer();
@@ -2145,7 +2158,8 @@ export function extractModule(sourceFile) {
2145
2158
  });
2146
2159
  }
2147
2160
  catch (e) {
2148
- console.error(`WARNING: skipping const '${decl.getName()}': ${e.message}`);
2161
+ const reason = e instanceof Error ? e.message : String(e);
2162
+ throw new Error(`Failed to extract const '${decl.getName()}' at ${sourceFile.getBaseName()}:${decl.getStartLineNumber()}: ${reason}`);
2149
2163
  }
2150
2164
  }
2151
2165
  }
@@ -2206,11 +2220,9 @@ export function extractModule(sourceFile) {
2206
2220
  // regex — but its callers should still be verifiable against an
2207
2221
  // uninterpreted predicate. Parallel to auto-extern for cross-file calls,
2208
2222
  // and emitted the same way (`function {:axiom} foo(...)` in Dafny).
2209
- // Match a `//@ <kw>` directive only as the first non-whitespace on a line, so
2210
- // a mention mid-line in prose or inside a block/JSDoc comment (e.g. "the
2211
- // `//@ extern` annotation", or ` * //@ extern`) doesn't falsely trigger it.
2212
- function hasLineDirective(text, kw) {
2213
- return new RegExp(String.raw `^[ \t]*//@ ${kw}\b`, "m").test(text);
2223
+ function hasSkip(f) {
2224
+ return hasLeadingDirective(f.parentStmt ?? f.node, "skip")
2225
+ || (!!f.parentStmt && hasLeadingDirective(f.node, "skip"));
2214
2226
  }
2215
2227
  function hasExtern(f) {
2216
2228
  if (hasLineDirective(f.node.getFullText(), "extern"))
@@ -2243,7 +2255,7 @@ export function extractModule(sourceFile) {
2243
2255
  return null;
2244
2256
  }
2245
2257
  for (const f of allFns) {
2246
- if (!hasExtern(f))
2258
+ if (hasSkip(f) || !hasExtern(f))
2247
2259
  continue;
2248
2260
  const qualified = externName(f) ?? f.name;
2249
2261
  const flat = qualified.replace(/\./g, "_");
@@ -2280,8 +2292,9 @@ export function extractModule(sourceFile) {
2280
2292
  }
2281
2293
  return false;
2282
2294
  }
2283
- const hasVerifyDirective = hasLineDirective(sourceFile.getFullText(), "verify");
2284
- const nonExternFns = allFns.filter(f => !hasExtern(f));
2295
+ const nonExternFns = allFns.filter(f => !hasSkip(f) && !hasExtern(f));
2296
+ const hasVerifiedClassMethod = sourceFile.getClasses().some(cls => !hasLeadingDirective(cls, "skip") && cls.getMethods().some(method => !hasLeadingDirective(method, "skip") && hasLineDirective(method.getFullText(), "verify")));
2297
+ const hasVerifyDirective = nonExternFns.some(hasVerify) || hasVerifiedClassMethod;
2285
2298
  const fnsToExtract = hasVerifyDirective ? nonExternFns.filter(hasVerify) : nonExternFns;
2286
2299
  // `//@ autohavoc` — enable the auto-havoc abstraction (see autohavoc.ts).
2287
2300
  // File-level: a directive at column 0 (top of file) enables it for every
@@ -2795,8 +2808,12 @@ export function extractModule(sourceFile) {
2795
2808
  // Extract classes with //@ verify methods
2796
2809
  const classes = [];
2797
2810
  for (const cls of sourceFile.getClasses()) {
2811
+ if (hasLeadingDirective(cls, "skip"))
2812
+ continue;
2798
2813
  const methods = [];
2799
2814
  for (const method of cls.getMethods()) {
2815
+ if (hasLeadingDirective(method, "skip"))
2816
+ continue;
2800
2817
  if (!method.getFullText().includes('//@ verify'))
2801
2818
  continue;
2802
2819
  methods.push(extractFunction(method));
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
- main();
340
+ try {
341
+ main();
342
+ }
343
+ catch (e) {
344
+ console.error(`ERROR: ${e instanceof Error ? e.message : String(e)}`);
345
+ process.exitCode = 1;
346
+ }
@@ -4,11 +4,11 @@
4
4
  */
5
5
  import { normalizeBigIntLiteral } from "./rawir.js";
6
6
  const MULTI_OPS = ["<==>", "==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
7
- /** Hex / binary / octal / decimal integer, with optional numeric separators and
8
- * an optional `n` (BigInt) suffix in capture group 1. Deliberately integer-only:
9
- * a trailing `.` is left for the tokenizer to emit as punctuation, exactly as
10
- * the previous digit-scanning loop did. */
11
- const INTEGER_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)?/;
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 match = input.slice(i).match(INTEGER_LITERAL);
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 (match[1] === "n")
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, "")) });
@@ -2525,7 +2525,8 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
2525
2525
  };
2526
2526
  });
2527
2527
  // Def file: Velvet methods
2528
- // Pure functions get a thin wrapper that calls Pure.fnName
2528
+ // Pure functions get a thin wrapper. Lean keeps pure definitions in the
2529
+ // `Pure` namespace; Dafny flattens that namespace, so its call is unqualified.
2529
2530
  // def-by-method functions also skip their method wrappers
2530
2531
  const pureDefNames = new Set([...pureDefs.map(d => d.name), ...defByMethods.map(d => d.name)]);
2531
2532
  const methods = mod.functions.map(fn => {
@@ -2539,7 +2540,7 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
2539
2540
  }
2540
2541
  _forofCounters.clear();
2541
2542
  let body = pureDefNames.has(fn.name)
2542
- ? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
2543
+ ? [{ kind: "return", value: { kind: "app", fn: _opts.backend === "lean" ? `Pure.${fn.name}` : fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
2543
2544
  : promoteAssignedLets(transformStmts(fn.body, mod.typeDecls));
2544
2545
  // Lean-only method-body rewrites (Velvet can't WP-synthesize monadic matches
2545
2546
  // and forbids `return` in loops):