lemmascript 0.5.18 → 0.5.20

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.
@@ -6,15 +6,43 @@
6
6
  */
7
7
  import { Project, Node, SyntaxKind, ScriptTarget, ts } from "ts-morph";
8
8
  import { initTypeParser } from "./types.js";
9
+ import { normalizeBigIntLiteral } from "./rawir.js";
9
10
  import { setUserNames, freshName } from "./names.js";
10
11
  // ── Expression extraction ────────────────────────────────────
11
12
  /** When set, calls whose function/method name matches this key are replaced with havoc. */
12
13
  let _havocKey = null;
14
+ /** A leading `//@ havoc`, `//@ havoc : Type`, or `//@ havoc <key>` directive. */
15
+ function havocDirective(s) {
16
+ const m = s.getLeadingCommentRanges()
17
+ .map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
18
+ .find(m => m !== null);
19
+ return m ? { type: m[1]?.trim() ?? null, key: m[2] ?? null } : null;
20
+ }
21
+ /** Run `fn` with `key` as the subexpression havoc key, restoring the outer one. */
22
+ function withHavocKey(key, fn) {
23
+ const saved = _havocKey;
24
+ if (key)
25
+ _havocKey = key;
26
+ try {
27
+ return fn();
28
+ }
29
+ finally {
30
+ _havocKey = saved;
31
+ }
32
+ }
13
33
  /** Auto-detected cross-file calls. Populated by `extractExpr` whenever it sees
14
34
  * a call `Obj.method(...)` or `foo(...)` whose ts-morph symbol resolves to a
15
35
  * different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat>`.
16
36
  * Cleared at the start of every `extractModule`. */
17
37
  const _externs = new Map();
38
+ /** Signature types of *kept* externs, for the imported-type resolver: a type
39
+ * reachable ONLY through an imported function's signature (e.g. an extern
40
+ * returning PresentFact that no local signature mentions) must still be
41
+ * resolved into a full decl, or it synthesizes opaque. Filled only after the
42
+ * extern survives the `_externs` dedup — a cross-file signature superseded by a
43
+ * same-file `//@ extern` contributes nothing to the output, so resolving its
44
+ * types would emit decls that no emitted declaration mentions. */
45
+ const _externSigTypes = [];
18
46
  let _currentSourceFile = null;
19
47
  /** True only while extracting a function body. Module-level constants that
20
48
  * reference cross-file callees (e.g., `BusEvent.define(...)` inside a
@@ -30,10 +58,14 @@ let _destrCounter = 0;
30
58
  * lifted `requires`/`ensures` see all the symbols they reference). Idempotent
31
59
  * via the `_externs` dedup. */
32
60
  function registerExternIfCrossFile(callee, sourceFile) {
33
- const ext = detectCrossFileExtern(callee, sourceFile);
61
+ const sigTypes = [];
62
+ const ext = detectCrossFileExtern(callee, sourceFile, sigTypes);
34
63
  if (!ext || _externs.has(ext.qualified))
35
64
  return;
36
65
  _externs.set(ext.qualified, ext);
66
+ // Only now that the extern is kept do its signature types become resolver
67
+ // seeds — see `_externSigTypes`.
68
+ _externSigTypes.push(...sigTypes);
37
69
  // Recurse: scan the source decl's body for nested cross-file calls so any
38
70
  // symbol referenced by the copied spec is itself declared in the output.
39
71
  let symbol = callee.getSymbol();
@@ -56,7 +88,7 @@ function registerExternIfCrossFile(callee, sourceFile) {
56
88
  }
57
89
  }
58
90
  }
59
- function detectCrossFileExtern(callee, sourceFile) {
91
+ function detectCrossFileExtern(callee, sourceFile, sigTypesOut) {
60
92
  let symbol = callee.getSymbol();
61
93
  if (!symbol)
62
94
  return null;
@@ -92,12 +124,32 @@ function detectCrossFileExtern(callee, sourceFile) {
92
124
  // kept): a bare `TMsg`, not `import("/abs/path/transcript").TMsg` — the
93
125
  // importing module declares the datatype locally, so the axiom must use the
94
126
  // local name.
95
- const externTypeText = (t) => t.getText(callee, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
127
+ // NoTruncation: a wide expansion (e.g. an alias for a large string-literal
128
+ // union, not importable at the call site) must print whole — a truncated
129
+ // union is unparseable and synthesizes an opaque decl named by its own text.
130
+ const externTypeText = (t) => t.getText(callee, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope | ts.TypeFormatFlags.NoTruncation);
96
131
  const params = sig.getParameters().map(p => ({
97
132
  name: p.getName(),
98
133
  tsType: externTypeText(p.getTypeAtLocation(callee)),
99
134
  }));
100
135
  const returnType = externTypeText(sig.getReturnType());
136
+ for (const p of sig.getParameters()) {
137
+ sigTypesOut.push({ type: p.getTypeAtLocation(callee), node: callee });
138
+ }
139
+ sigTypesOut.push({ type: sig.getReturnType(), node: callee });
140
+ // Also seed from the source declaration's syntactic type nodes: symbol-based
141
+ // types can drop the alias symbol (the printed signature then names an alias
142
+ // like `TypeDecls` that would otherwise synthesize opaque), while a type
143
+ // node's type keeps it.
144
+ const srcDecl = externalDecl;
145
+ for (const sp of srcDecl.getParameters?.() ?? []) {
146
+ const tn = sp.getTypeNode?.();
147
+ if (tn)
148
+ sigTypesOut.push({ type: tn.getType(), node: tn });
149
+ }
150
+ const rtn = srcDecl.getReturnTypeNode?.();
151
+ if (rtn)
152
+ sigTypesOut.push({ type: rtn.getType(), node: rtn });
101
153
  let qualified;
102
154
  if (Node.isPropertyAccessExpression(callee)) {
103
155
  qualified = `${callee.getExpression().getText()}.${callee.getName()}`;
@@ -293,24 +345,29 @@ function _eraseGenerics(tsType) {
293
345
  return tsType;
294
346
  }
295
347
  function extractExpr(node) {
296
- // Havoc key matching: replace matching calls with havoc expression
297
- if (_havocKey && Node.isCallExpression(node)) {
348
+ // Havoc key matching: replace matching calls or new expressions with havoc expression
349
+ if (_havocKey && (Node.isCallExpression(node) || Node.isNewExpression(node))) {
298
350
  const fnExpr = node.getExpression();
299
351
  const name = Node.isPropertyAccessExpression(fnExpr) ? fnExpr.getName()
300
352
  : Node.isIdentifier(fnExpr) ? fnExpr.getText()
301
353
  : null;
302
354
  if (name === _havocKey) {
303
- return { kind: "havoc", tsType: typeToString(node.getType()) };
355
+ // Prefer the contextual type: the abstracted value stands in where the
356
+ // surrounding code expects it, and a subclass (`new SdkError` into an
357
+ // `Error` field) has no subtyping relation once both are opaque.
358
+ const ty = node.getContextualType() ?? node.getType();
359
+ return { kind: "havoc", tsType: typeToString(ty) };
304
360
  }
305
361
  }
306
362
  // Numeric literal
307
363
  if (Node.isNumericLiteral(node)) {
308
364
  return { kind: "num", value: Number(node.getLiteralValue()) };
309
365
  }
310
- // BigInt literal (e.g. 32n, 0xffffn) — integer, with bigint division semantics
366
+ // BigInt literal (e.g. 32n, 0xffffn) — exact integer, with bigint division
367
+ // semantics. Kept as a decimal string: `getLiteralValue()`/`Number()` would
368
+ // round anything past 2^53 (`9007199254740993n` → `9007199254740992`).
311
369
  if (Node.isBigIntLiteral(node)) {
312
- const text = node.getText().replace(/n$/, '');
313
- return { kind: "num", value: Number(text), big: true };
370
+ return { kind: "bigint", value: normalizeBigIntLiteral(node.getText()) };
314
371
  }
315
372
  // Template literal: `foo${x}bar` → "foo" + x + "bar"
316
373
  if (Node.isTemplateExpression(node)) {
@@ -462,10 +519,9 @@ function extractExpr(node) {
462
519
  const typeNode = p.getTypeNode();
463
520
  return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
464
521
  });
465
- // Capture an explicit return annotation (`(x): Out => …`) so resolve can type
466
- // return-position record literals to their named type instead of a tuple.
467
- const retNode = node.getReturnTypeNode();
468
- const returnTsType = retNode ? typeToString(node.getReturnType()) : undefined;
522
+ // Return type from the checker inferred when unannotated so resolve can
523
+ // type return-position record literals and give the lambda a real fn type.
524
+ const returnTsType = typeToString(node.getReturnType());
469
525
  const body = node.getBody();
470
526
  if (Node.isExpression(body)) {
471
527
  return { kind: "lambda", params, body: extractExpr(body), returnTsType };
@@ -688,8 +744,14 @@ function extractTypeDecl(decl, extraDecls) {
688
744
  continue;
689
745
  let tsType = typeToString(prop.getTypeAtLocation(decl));
690
746
  const propDecl = prop.getDeclarations()[0];
691
- if (propDecl && propDecl.hasQuestionToken?.() && !tsType.includes(" | undefined")) {
692
- tsType = `${tsType} | undefined`;
747
+ tsType = declaredTypeTextIfBetter(propDecl, tsType);
748
+ if (propDecl && propDecl.hasQuestionToken?.()) {
749
+ // Normalize checker output that puts `undefined` first, then
750
+ // ensure exactly one trailing `| undefined`.
751
+ if (tsType.startsWith("undefined | "))
752
+ tsType = `${tsType.slice("undefined | ".length)} | undefined`;
753
+ else if (!tsType.includes(" | undefined"))
754
+ tsType = `${tsType} | undefined`;
693
755
  }
694
756
  fields.push({ name: prop.getName(), tsType });
695
757
  }
@@ -780,12 +842,18 @@ function extractRecord(name, type, locationNode, overrides, extraDecls) {
780
842
  }
781
843
  const propType = prop.getTypeAtLocation(locationNode);
782
844
  let tsType = typeToString(propType);
845
+ const propDecl = prop.getDeclarations()[0];
846
+ tsType = declaredTypeTextIfBetter(propDecl, tsType);
783
847
  // Optional property: `foo?: T` reports as `T` (ts-morph strips the
784
848
  // `| undefined` from a question-token type). Add it back so the field
785
849
  // resolves to `Optional<T>`.
786
- const propDecl = prop.getDeclarations()[0];
787
- if (propDecl && propDecl.hasQuestionToken?.() && !tsType.includes(" | undefined")) {
788
- tsType = `${tsType} | undefined`;
850
+ if (propDecl && propDecl.hasQuestionToken?.()) {
851
+ // Normalize checker output that puts `undefined` first, then ensure
852
+ // exactly one trailing `| undefined`.
853
+ if (tsType.startsWith("undefined | "))
854
+ tsType = `${tsType.slice("undefined | ".length)} | undefined`;
855
+ else if (!tsType.includes(" | undefined"))
856
+ tsType = `${tsType} | undefined`;
789
857
  }
790
858
  // Inline anonymous object types: ts-morph names them __type.
791
859
  // Generate a synthetic named record and reference it by name instead.
@@ -832,6 +900,23 @@ function findDiscriminant(members) {
832
900
  }
833
901
  return null;
834
902
  }
903
+ /** Recover a field's *declared* type text when the semantic printer degraded
904
+ * to ts-morph's anonymous `__type` — a self-referential alias reached
905
+ * through a `| null` union expands structurally and loses its name. The
906
+ * syntactic node text preserves the alias spelling (`TExpr | null`). Only
907
+ * plain reference text is used: inline object literals (containing `{`)
908
+ * keep the `__type` marker so record synthesis can handle them. */
909
+ function declaredTypeTextIfBetter(propDecl, tsType) {
910
+ if (!tsType.includes("__type") || !propDecl)
911
+ return tsType;
912
+ const tn = propDecl.getTypeNode?.();
913
+ if (!tn)
914
+ return tsType;
915
+ const text = tn.getText();
916
+ if (text.includes("__type") || text.includes("{"))
917
+ return tsType;
918
+ return text;
919
+ }
835
920
  function typeToString(type) {
836
921
  if (type.isUndefined())
837
922
  return "undefined";
@@ -1031,6 +1116,7 @@ function renameRawExpr(e, from, to) {
1031
1116
  switch (e.kind) {
1032
1117
  case "var": return e.name === from ? { kind: "var", name: to } : e;
1033
1118
  case "num":
1119
+ case "bigint":
1034
1120
  case "str":
1035
1121
  case "bool":
1036
1122
  case "result":
@@ -1179,12 +1265,10 @@ function extractStmts(stmts) {
1179
1265
  continue;
1180
1266
  }
1181
1267
  if (Node.isVariableStatement(s)) {
1182
- const havocMatch = s.getLeadingCommentRanges()
1183
- .map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
1184
- .find(m => m !== null);
1185
- const havocType = havocMatch?.[1]?.trim() ?? null; // //@ havoc : Type
1186
- const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
1187
- const isHavoc = !!havocMatch;
1268
+ const havoc = havocDirective(s);
1269
+ const havocType = havoc?.type ?? null; // //@ havoc : Type
1270
+ const havocKey = havoc?.key ?? null; // //@ havoc key
1271
+ const isHavoc = !!havoc;
1188
1272
  for (const d of s.getDeclarations()) {
1189
1273
  // Havoc on destructuring: emit each named binding as a separate havoced variable
1190
1274
  const nameNode = d.getNameNode();
@@ -1625,7 +1709,11 @@ function extractStmts(stmts) {
1625
1709
  // functions this would emit the wrong shape, but lsc has no current
1626
1710
  // examples of explicit bare return in void functions; revisit if one
1627
1711
  // appears.
1628
- result.push({ kind: "return", value: expr ? extractExpr(expr) : { kind: "var", name: "undefined" }, line });
1712
+ // `//@ havoc <key>` on a return abstracts the matching calls or new
1713
+ // expressions inside the returned expression — there is no variable to
1714
+ // hang a whole-value havoc on, so only the key form applies here.
1715
+ const value = withHavocKey(havocDirective(s)?.key ?? null, () => expr ? extractExpr(expr) : { kind: "var", name: "undefined" });
1716
+ result.push({ kind: "return", value, line });
1629
1717
  continue;
1630
1718
  }
1631
1719
  if (Node.isBreakStatement(s)) {
@@ -1641,14 +1729,12 @@ function extractStmts(stmts) {
1641
1729
  // //@ havoc before `x = e` — discard the RHS, assign a nondeterministic
1642
1730
  // value of x's type. Only applies to plain `=` with an identifier LHS;
1643
1731
  // compound assigns, `arr[i] = v`, and `x++` fall through to desugaring.
1644
- const havocMatch = s.getLeadingCommentRanges()
1645
- .map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+))?$/))
1646
- .find(m => m !== null);
1647
- if (havocMatch && Node.isBinaryExpression(expr)
1732
+ const havoc = havocDirective(s);
1733
+ if (havoc && !havoc.key && Node.isBinaryExpression(expr)
1648
1734
  && expr.getOperatorToken().getText() === "="
1649
1735
  && Node.isIdentifier(expr.getLeft())) {
1650
1736
  const target = expr.getLeft().getText();
1651
- const tsType = havocMatch[1]?.trim() ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
1737
+ const tsType = havoc.type ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
1652
1738
  result.push({ kind: "assign", target, value: { kind: "havoc", tsType }, line });
1653
1739
  continue;
1654
1740
  }
@@ -1838,6 +1924,12 @@ function extractFunctionInner(fn, parentAnnotations) {
1838
1924
  return "void"; // Promise<void>
1839
1925
  }
1840
1926
  const node = fn.getReturnTypeNode();
1927
+ // A type predicate (`x is T` / `asserts x is T`) is a `boolean` at
1928
+ // runtime; the narrowing it performs is a TS-only refinement with no
1929
+ // counterpart in the model. Without this, `getText()` yields "x is T"
1930
+ // and the type mapper reads the subject name as an opaque type.
1931
+ if (node && node.getKind() === SyntaxKind.TypePredicate)
1932
+ return "boolean";
1841
1933
  if (node && Node.isUnionTypeNode(node))
1842
1934
  return _eraseGenerics(_tsTypeFromUnionNode(node));
1843
1935
  if (node)
@@ -1870,6 +1962,7 @@ export function extractModule(sourceFile) {
1870
1962
  // `extractExpr` during call extraction (only symbols *actually used* end up
1871
1963
  // here), deduped by qualified name.
1872
1964
  _externs.clear();
1965
+ _externSigTypes.length = 0;
1873
1966
  // Share the module's ts-morph Project with parseTsType (scratch source file
1874
1967
  // for type-string parsing). Done before declare-type parsing so any
1875
1968
  // parseTsType call downstream uses the same Project.
@@ -2397,6 +2490,28 @@ export function extractModule(sourceFile) {
2397
2490
  }
2398
2491
  }
2399
2492
  }
2493
+ // A constant's initializer can reference other constants
2494
+ // (`const ZERO_NINE = ZERO + nthDigit(-1)`). Close over those initializers
2495
+ // before filtering — mirroring the transitive type filter below — or a
2496
+ // constant reachable only from another constant is dropped and the backend
2497
+ // sees an undefined name.
2498
+ // Snapshot first: what the closure adds are VALUE references, and a value
2499
+ // and a type can share a name (`const Action = Schema.Literals(…)` next to
2500
+ // `type Action = Schema.Schema.Type<typeof Action>`). Keeping the constant
2501
+ // alive must not also drag in the unrelated type alias, so the type filter
2502
+ // below runs off the pre-closure set.
2503
+ const typeReferencedNames = new Set(referencedNames);
2504
+ for (let grew = true; grew;) {
2505
+ grew = false;
2506
+ for (const c of constants) {
2507
+ if (!referencedNames.has(c.name))
2508
+ continue;
2509
+ const before = referencedNames.size;
2510
+ collectNamesExpr(c.value);
2511
+ if (referencedNames.size !== before)
2512
+ grew = true;
2513
+ }
2514
+ }
2400
2515
  constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
2401
2516
  // Filter types to only those referenced by verified functions (transitive)
2402
2517
  const neededTypes = new Set();
@@ -2415,7 +2530,7 @@ export function extractModule(sourceFile) {
2415
2530
  for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
2416
2531
  markType(m[1]);
2417
2532
  }
2418
- for (const name of referencedNames)
2533
+ for (const name of typeReferencedNames)
2419
2534
  markType(name);
2420
2535
  // Signature types also mark their base after stripping array/optional
2421
2536
  // WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
@@ -2431,13 +2546,12 @@ export function extractModule(sourceFile) {
2431
2546
  // Resolve imported types: extract types referenced in function signatures but not in this file
2432
2547
  const knownTypes = new Set(typeDecls.map(d => d.name));
2433
2548
  const builtins = new Set(["Map", "Set", "Array", "String", "Number", "Boolean", "Promise", "Date", "RegExp", "Error"]);
2549
+ const visitedTypes = new Set();
2434
2550
  function resolveType(t, locationNode) {
2435
- // Unwrap arrays and generics to find user-defined types
2436
- if (t.isArray()) {
2437
- resolveType(t.getArrayElementTypeOrThrow(), locationNode);
2438
- return;
2439
- }
2440
- // Resolve type aliases (e.g. string unions imported from other files)
2551
+ // Resolve type aliases (e.g. string unions imported from other files).
2552
+ // BEFORE the visited guard: the same interned compilerType can arrive both
2553
+ // with and without its alias symbol (getTypeAtLocation drops it), and an
2554
+ // aliasless first visit must not suppress the alias extraction.
2441
2555
  const alias = t.getAliasSymbol();
2442
2556
  if (alias) {
2443
2557
  const aliasName = alias.getName();
@@ -2470,6 +2584,18 @@ export function extractModule(sourceFile) {
2470
2584
  }
2471
2585
  }
2472
2586
  }
2587
+ // Recursion guard: recursive unions (Expr → variant → body: Expr) are
2588
+ // reachable now that anonymous variant fields are walked below. Keyed on
2589
+ // the compiler's interned Type object — alias names are not enough,
2590
+ // because getTypeAtLocation can drop the alias symbol.
2591
+ if (visitedTypes.has(t.compilerType))
2592
+ return;
2593
+ visitedTypes.add(t.compilerType);
2594
+ // Unwrap arrays and generics to find user-defined types
2595
+ if (t.isArray()) {
2596
+ resolveType(t.getArrayElementTypeOrThrow(), locationNode);
2597
+ return;
2598
+ }
2473
2599
  if (t.isUnion()) {
2474
2600
  for (const u of t.getUnionTypes())
2475
2601
  resolveType(u, locationNode);
@@ -2492,6 +2618,15 @@ export function extractModule(sourceFile) {
2492
2618
  }
2493
2619
  }
2494
2620
  }
2621
+ else if (t.isObject() && (!name || name.startsWith("__")) && t.getCallSignatures().length === 0) {
2622
+ // Anonymous object type — typically a variant of an imported
2623
+ // discriminated union. There is no decl to extract, but its fields can
2624
+ // reference named types (`arms: MatchArm[]`) that downstream passes
2625
+ // need declared, so walk them.
2626
+ for (const prop of t.getProperties()) {
2627
+ resolveType(prop.getTypeAtLocation(locationNode), locationNode);
2628
+ }
2629
+ }
2495
2630
  }
2496
2631
  for (let i = 0; i < fnsToExtract.length; i++) {
2497
2632
  const f = fnsToExtract[i];
@@ -2507,6 +2642,9 @@ export function extractModule(sourceFile) {
2507
2642
  resolveType(p.getType(), p);
2508
2643
  }
2509
2644
  }
2645
+ // Extern signature types: see _externSigTypes.
2646
+ for (const r of _externSigTypes)
2647
+ resolveType(r.type, r.node);
2510
2648
  // Resolve anonymous object return types into synthetic named types
2511
2649
  for (let i = 0; i < fnsToExtract.length; i++) {
2512
2650
  const f = fnsToExtract[i];
@@ -36,3 +36,71 @@ export function runInfo(raw, outPath) {
36
36
  writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
37
37
  console.log(`Wrote ${outPath}`);
38
38
  }
39
+ /** Statement/expression kinds present anywhere in a function body — a cheap
40
+ * structural census so consumers can classify (havoc/assume/throw usage)
41
+ * without receiving the whole body. */
42
+ function bodyKinds(fn) {
43
+ // Keys under which a Ty (not a statement/expression) hangs — their `kind`
44
+ // tags belong to the type grammar and would pollute the census.
45
+ const TY_KEYS = new Set(["ty", "binderTy", "varTy", "nameTypes"]);
46
+ const kinds = new Set();
47
+ const walk = (node) => {
48
+ if (Array.isArray(node)) {
49
+ node.forEach(walk);
50
+ return;
51
+ }
52
+ if (node && typeof node === "object") {
53
+ const o = node;
54
+ if (typeof o.kind === "string") {
55
+ kinds.add(o.kind === "assert" && o.assumed === true ? "assume" : o.kind);
56
+ }
57
+ for (const [k, v] of Object.entries(o))
58
+ if (!TY_KEYS.has(k))
59
+ walk(v);
60
+ }
61
+ };
62
+ walk(fn.body);
63
+ return [...kinds].sort();
64
+ }
65
+ function typedFn(fn, rawFn) {
66
+ // Carry the original TS type string per parameter: Ty conflates types whose
67
+ // runtime shapes differ (Map<K,V> and Record<K,V> both map to kind "map"),
68
+ // and satellites that construct runtime values need to tell them apart.
69
+ const rawTsTypes = new Map((rawFn?.params ?? []).map(p => [p.name, p.tsType]));
70
+ return {
71
+ name: fn.name,
72
+ exported: rawFn?.exported ?? false,
73
+ typeParams: fn.typeParams,
74
+ params: fn.params.map(p => ({ ...p, tsType: rawTsTypes.get(p.name) })),
75
+ returnTy: fn.returnTy,
76
+ requires: fn.requires,
77
+ ensures: fn.ensures,
78
+ decreases: fn.decreases,
79
+ contract: rawFn?.contract ?? [],
80
+ isPure: fn.isPure,
81
+ forcePure: fn.forcePure,
82
+ autohavoc: fn.autohavoc,
83
+ bodyKinds: bodyKinds(fn),
84
+ };
85
+ }
86
+ export function runTypedInfo(raw, typed, version, backendDirective, dafny) {
87
+ const rawByName = new Map(raw.functions.map(f => [f.name, f]));
88
+ const rawMethods = new Map(raw.classes.flatMap(c => c.methods.map(m => [`${c.name}.${m.name}`, m])));
89
+ const out = {
90
+ schema: 1,
91
+ lemmascript: version,
92
+ file: typed.file,
93
+ backendDirective,
94
+ typeDecls: typed.typeDecls,
95
+ externs: typed.externs,
96
+ constants: typed.constants,
97
+ functions: typed.functions.map(f => typedFn(f, rawByName.get(f.name))),
98
+ classes: typed.classes.map(c => ({
99
+ name: c.name,
100
+ fields: c.fields,
101
+ methods: c.methods.map(m => typedFn(m, rawMethods.get(`${c.name}.${m.name}`))),
102
+ })),
103
+ dafny,
104
+ };
105
+ console.log(JSON.stringify(out, null, 2));
106
+ }
package/tools/dist/ir.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * The emit phase pretty-prints them to backend syntax (Lean or Dafny).
6
6
  */
7
7
  export const pWild = () => ({ kind: "wild" });
8
- export const pCtor = (ctor, ...binders) => ({ kind: "ctor", ctor, binders });
8
+ export const pCtor = (c, ...binders) => ({ kind: "ctor", ctor: c, binders });
9
9
  /** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
10
10
  export function patternBinders(p) {
11
11
  return p.kind === "ctor" ? p.binders : [];
@@ -17,19 +17,40 @@ export function patternCtor(p) {
17
17
  export function patternBinds(p, name) {
18
18
  return patternBinders(p).includes(name);
19
19
  }
20
+ // ── Literal queries ──────────────────────────────────────────
21
+ /** The exact value of an integer-literal operand (possibly negated), or null
22
+ * when `e` isn't one. Answers in compiler-side `bigint` so a backend folding a
23
+ * literal into arithmetic stays exact: JS bitwise operators truncate to 32 bits
24
+ * and `Math.pow(2, n)` is a double, both of which lie past 2^53. A `num`
25
+ * outside the safe-integer range has already lost precision, so it is not a
26
+ * usable answer — hence null. */
27
+ export function exactIntegerLiteral(e) {
28
+ if (e.kind === "bigint")
29
+ return BigInt(e.value);
30
+ if (e.kind === "num")
31
+ return Number.isSafeInteger(e.value) ? BigInt(e.value) : null;
32
+ // Transform folds `-<num>` into a negative `num`, but leaves a negated
33
+ // `bigint` structural (folding it would coerce the payload through Number).
34
+ if (e.kind === "unop" && e.op === "-") {
35
+ const inner = exactIntegerLiteral(e.expr);
36
+ return inner === null ? null : -inner;
37
+ }
38
+ return null;
39
+ }
20
40
  export function anyExpr(e, pred) {
21
41
  if (pred(e))
22
42
  return true;
23
43
  switch (e.kind) {
24
44
  case "var":
25
45
  case "num":
46
+ case "bigint":
26
47
  case "bool":
27
48
  case "str":
28
49
  case "emptyMap":
29
50
  case "emptySet":
30
51
  case "havoc":
31
52
  case "default": return false;
32
- case "constructor": return (e.args ?? []).some(a => anyExpr(a, pred));
53
+ case "constructor": return e.args.some(a => anyExpr(a, pred));
33
54
  case "binop": return anyExpr(e.left, pred) || anyExpr(e.right, pred);
34
55
  case "unop":
35
56
  case "toNat":
@@ -46,7 +67,7 @@ export function anyExpr(e, pred) {
46
67
  case "methodCall": return anyExpr(e.obj, pred) || e.args.some(a => anyExpr(a, pred));
47
68
  case "lambda": return e.body.some(s => anyExprInStmt(s, pred));
48
69
  case "if": return anyExpr(e.cond, pred) || anyExpr(e.then, pred) || anyExpr(e.else, pred);
49
- case "match": return (typeof e.scrutinee !== "string" && anyExpr(e.scrutinee, pred)) || e.arms.some(a => anyExpr(a.body, pred));
70
+ case "match": return anyExpr(e.scrutinee, pred) || e.arms.some(a => anyExpr(a.body, pred));
50
71
  case "forall":
51
72
  case "exists": return anyExpr(e.body, pred);
52
73
  case "let": return anyExpr(e.value, pred) || anyExpr(e.body, pred);
@@ -65,7 +86,7 @@ export function anyExprInStmt(s, pred) {
65
86
  case "break":
66
87
  case "continue": return false;
67
88
  case "if": return anyExpr(s.cond, pred) || anyExprInStmts(s.then, pred) || anyExprInStmts(s.else, pred);
68
- case "match": return (typeof s.scrutinee !== "string" && anyExpr(s.scrutinee, pred)) || s.arms.some(a => anyExprInStmts(a.body, pred));
89
+ case "match": return anyExpr(s.scrutinee, pred) || s.arms.some(a => anyExprInStmts(a.body, pred));
69
90
  case "while": return anyExpr(s.cond, pred) || s.invariants.some(i => anyExpr(i, pred))
70
91
  || (s.decreasing ? anyExpr(s.decreasing, pred) : false) || (s.doneWith ? anyExpr(s.doneWith, pred) : false)
71
92
  || anyExprInStmts(s.body, pred);
@@ -80,10 +101,9 @@ export function anyExprInStmts(stmts, pred) {
80
101
  // function. These drive the *local* freshness checks for user-facing binders
81
102
  // (the result out-parameter, comprehension binders): a binder is checked only
82
103
  // against the expressions/scope it actually wraps, not the whole module.
83
- const _refsName = (name) => e => (e.kind === "var" && e.name === name) ||
104
+ const _refsName = (name) => (e) => (e.kind === "var" && e.name === name) ||
84
105
  (e.kind === "app" && e.fn === name) ||
85
- (e.kind === "constructor" && e.name === name) ||
86
- (e.kind === "match" && typeof e.scrutinee === "string" && e.scrutinee === name);
106
+ (e.kind === "constructor" && e.name === name);
87
107
  export function usesName(e, name) {
88
108
  return anyExpr(e, _refsName(name));
89
109
  }