lemmascript 0.5.19 → 0.5.21

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.19",
3
+ "version": "0.5.21",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "engines": {
@@ -87,6 +87,7 @@ export const BUILTINS = {
87
87
  "string.trimStart": { ret: STRING, pure: true },
88
88
  "string.toLowerCase": { ret: STRING, pure: true },
89
89
  "string.toUpperCase": { ret: STRING, pure: true },
90
+ "string.repeat": { ret: STRING, pure: true },
90
91
  "string.slice": { ret: STRING, pure: true },
91
92
  "string.substring": { ret: STRING, pure: true },
92
93
  "string.split": { pure: true,
@@ -80,6 +80,24 @@ const DAFNY_KEYWORDS = new Set([
80
80
  // by methodHeader and reset per decl. `\result` in an ensures must use the
81
81
  // *same* name, so escapeName routes it here.
82
82
  let _resultName = "res";
83
+ // User-type names appearing as the type of a havoc anywhere in the module —
84
+ // populated per file by emitDafnyFile, read by the opaque-type case.
85
+ const _havocedTypeNames = new Set();
86
+ /** Collect the user-type names of every havoc in a decl tree. */
87
+ function collectHavocedTypeNames(v, out) {
88
+ if (Array.isArray(v)) {
89
+ for (const x of v)
90
+ collectHavocedTypeNames(x, out);
91
+ return;
92
+ }
93
+ if (v === null || typeof v !== "object")
94
+ return;
95
+ const n = v;
96
+ if (n.kind === "havoc" && n.type?.kind === "user")
97
+ out.add(n.type.name);
98
+ for (const x of Object.values(v))
99
+ collectHavocedTypeNames(x, out);
100
+ }
83
101
  // ── Dafny name allocation ──────────────────────────────────
84
102
  //
85
103
  // freshName (names.ts) freshens in the *raw TS* namespace — but that is not the
@@ -145,6 +163,13 @@ function escapeName(name) {
145
163
  return user;
146
164
  return escapeGeneratedName(name);
147
165
  }
166
+ function isEmittedUserName(name) {
167
+ for (const emitted of _userDafnyNames.values()) {
168
+ if (emitted === name)
169
+ return true;
170
+ }
171
+ return false;
172
+ }
148
173
  /** Allocate a toolchain-generated name (an ANF temp, a comprehension binder, a
149
174
  * companion `_ensures` lemma). Escapes to a base, then freshens in the Dafny
150
175
  * namespace so it can't collapse onto an escaped user name. Bypasses the user
@@ -447,6 +472,10 @@ function emitExpr(e) {
447
472
  return `(|${obj}| >= |${args[0]}| && ${obj}[..|${args[0]}|] == ${args[0]})`;
448
473
  if (e.method === "charCodeAt")
449
474
  return `(${obj}[${args[0]}] as int)`;
475
+ if (e.method === "repeat") {
476
+ needPreamble("StringRepeat");
477
+ return `StringRepeat(${obj}, ${args[0]})`;
478
+ }
450
479
  }
451
480
  // Map methods
452
481
  if (ty === "map") {
@@ -515,7 +544,7 @@ function emitExpr(e) {
515
544
  // Discriminant check: x == .Ctor → x.Ctor?
516
545
  const op = mapOp(e.op);
517
546
  if ((op === "==" || op === "!=") && e.right.kind === "constructor") {
518
- const ctorName = escapeName(e.right.name.replace(/^\./, ""));
547
+ const ctorName = dafnyCtorName(e.right.name.replace(/^\./, ""));
519
548
  const pred = `${emitExpr(e.left)}.${ctorName}?`;
520
549
  return op === "!=" ? `(!${pred})` : pred;
521
550
  }
@@ -582,6 +611,8 @@ function emitExpr(e) {
582
611
  needPreamble("CeilReal");
583
612
  if (e.fn === "FloorReal")
584
613
  needPreamble("FloorReal");
614
+ if (e.fn === "StringFromCharCode")
615
+ needPreamble("StringFromCharCode");
585
616
  if (e.fn === "NatToString")
586
617
  needPreamble("NatToString");
587
618
  if (e.fn === "IntToString") {
@@ -611,7 +642,12 @@ function emitExpr(e) {
611
642
  // tags come from source strings ("spec-pure") that escapeName leaves alone.
612
643
  if (e.ctorOf) {
613
644
  const ctor = dafnyCtorName(e.fn);
614
- return _ambiguousCtors.has(e.fn)
645
+ // A source local may have the same name as a discriminated-union
646
+ // variant (`const error = ...; return { kind: "error", error }`).
647
+ // The bare `error(error)` is then parsed as a call through the local.
648
+ // Qualify whenever the emitted constructor spelling is already claimed
649
+ // in the user namespace, as well as when two datatypes share it.
650
+ return _ambiguousCtors.has(e.fn) || isEmittedUserName(ctor)
615
651
  ? `${e.ctorOf}.${ctor}(${args.join(", ")})`
616
652
  : `${ctor}(${args.join(", ")})`;
617
653
  }
@@ -875,7 +911,10 @@ function emitDecl(d) {
875
911
  case "opaque-type": {
876
912
  // Abstract type — no definition. `(==)` so it can sit inside datatypes
877
913
  // that derive structural equality. Never constructed or destructured.
878
- return `type ${escapeName(d.name)}(==)`;
914
+ // `0` (auto-init) only when a havoc of this type needs a witness to
915
+ // satisfy definite assignment — `var x: T := *` requires it.
916
+ const autoInit = _havocedTypeNames.has(d.name) ? ", 0" : "";
917
+ return `type ${escapeName(d.name)}(==${autoInit})`;
879
918
  }
880
919
  case "def": {
881
920
  const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
@@ -1269,6 +1308,27 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
1269
1308
  var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
1270
1309
  [upper] + StringToUpper(s[1..])
1271
1310
  }`;
1311
+ // `String.fromCharCode(n)` — the inverse of `s.charCodeAt(i)`'s `(s[i] as int)`.
1312
+ // Dafny's `char` is a Unicode scalar value, so the argument must miss the
1313
+ // surrogate range; that is the `requires`, discharged at each call site. The two
1314
+ // `ensures` give callers the round-trip law without unfolding the body.
1315
+ const STRING_FROM_CHAR_CODE = `function StringFromCharCode(n: int): string
1316
+ requires 0 <= n < 0xD800 || 0xE000 <= n < 0x110000
1317
+ ensures |StringFromCharCode(n)| == 1
1318
+ ensures (StringFromCharCode(n)[0] as int) == n
1319
+ {
1320
+ [n as char]
1321
+ }`;
1322
+ // `s.repeat(n)` — n copies of s, concatenated. The per-index ensures is stated
1323
+ // for the single-character receiver (the common case: padding with one digit).
1324
+ const STRING_REPEAT = `function StringRepeat(s: string, n: int): string
1325
+ requires n >= 0
1326
+ ensures |StringRepeat(s, n)| == |s| * n
1327
+ ensures |s| == 1 ==> forall i :: 0 <= i < n ==> StringRepeat(s, n)[i] == s[0]
1328
+ decreases n
1329
+ {
1330
+ if n == 0 then "" else s + StringRepeat(s, n - 1)
1331
+ }`;
1272
1332
  const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
1273
1333
  const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
1274
1334
  const MAX_OF_SEQ = `function MaxOfSeq(s: seq<int>): int
@@ -1386,6 +1446,8 @@ const PREAMBLE_CODE = [
1386
1446
  ["StringTrim", STRING_TRIM],
1387
1447
  ["StringToLower", STRING_TO_LOWER],
1388
1448
  ["StringToUpper", STRING_TO_UPPER],
1449
+ ["StringFromCharCode", STRING_FROM_CHAR_CODE],
1450
+ ["StringRepeat", STRING_REPEAT],
1389
1451
  ["NatToString", NAT_TO_STRING],
1390
1452
  ["IntToString", INT_TO_STRING],
1391
1453
  ["MathAbs", MATH_ABS],
@@ -1493,15 +1555,18 @@ function qualifyCtor(name, type) {
1493
1555
  return `${type}.${mapped}`;
1494
1556
  return mapped;
1495
1557
  }
1496
- /** Translate a Lean match pattern to Dafny syntax.
1558
+ /** Translate a backend-neutral match pattern to Dafny syntax.
1497
1559
  * ".ctorName field1 field2" → "ctorName(field1, field2)"
1498
1560
  * ".ctorName" → "ctorName"
1561
+ * literal value → quoted Dafny string
1499
1562
  * "_" → "_"
1500
1563
  */
1501
1564
  const CTOR_MAP = { "some": "Some", "none": "None" };
1502
1565
  function translatePattern(p) {
1503
1566
  if (p.kind === "wild")
1504
1567
  return "_";
1568
+ if (p.kind === "literal")
1569
+ return emitExpr({ kind: "str", value: p.value });
1505
1570
  const ctorName = (Object.hasOwn(CTOR_MAP, p.ctor) ? CTOR_MAP[p.ctor] : undefined) ?? dafnyCtorName(p.ctor);
1506
1571
  if (p.binders.length === 0)
1507
1572
  return ctorName;
@@ -1512,6 +1577,8 @@ export function emitDafnyFile(file, tsFileName, opts) {
1512
1577
  resetDafnyNameCache();
1513
1578
  buildRecordCtorMap(file.decls);
1514
1579
  _neededPreambles.clear();
1580
+ _havocedTypeNames.clear();
1581
+ collectHavocedTypeNames(file.decls, _havocedTypeNames);
1515
1582
  // Track successfully emitted pure defs — method wrappers are only
1516
1583
  // skipped when the corresponding pure def was actually emitted.
1517
1584
  const emittedPureDefs = new Set();
@@ -11,6 +11,25 @@ import { setUserNames, freshName } from "./names.js";
11
11
  // ── Expression extraction ────────────────────────────────────
12
12
  /** When set, calls whose function/method name matches this key are replaced with havoc. */
13
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
+ }
14
33
  /** Auto-detected cross-file calls. Populated by `extractExpr` whenever it sees
15
34
  * a call `Obj.method(...)` or `foo(...)` whose ts-morph symbol resolves to a
16
35
  * different `.ts` source file. Emitted in Dafny as `function {:axiom} <flat>`.
@@ -326,14 +345,18 @@ function _eraseGenerics(tsType) {
326
345
  return tsType;
327
346
  }
328
347
  function extractExpr(node) {
329
- // Havoc key matching: replace matching calls with havoc expression
330
- 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))) {
331
350
  const fnExpr = node.getExpression();
332
351
  const name = Node.isPropertyAccessExpression(fnExpr) ? fnExpr.getName()
333
352
  : Node.isIdentifier(fnExpr) ? fnExpr.getText()
334
353
  : null;
335
354
  if (name === _havocKey) {
336
- 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) };
337
360
  }
338
361
  }
339
362
  // Numeric literal
@@ -492,19 +515,78 @@ function extractExpr(node) {
492
515
  }
493
516
  // Arrow function: (x) => expr or (x) => { stmts }
494
517
  if (Node.isArrowFunction(node)) {
495
- const params = node.getParameters().map(p => {
518
+ // A binding pattern is not an identifier in either backend. Keep the
519
+ // callback arity unchanged by replacing a flat object pattern with one
520
+ // synthetic parameter and binding its fields at the start of the body:
521
+ // ({ x, y: z }) => e
522
+ // becomes, in Raw IR, the equivalent of
523
+ // (_lambdaParam0) => { const x = _lambdaParam0.x;
524
+ // const z = _lambdaParam0.y; return e; }
525
+ // transform's lambda flattener later turns these immutable lets back into
526
+ // the expression form required by Dafny lambdas. More involved binding
527
+ // semantics stay explicit errors rather than leaking pattern text into a
528
+ // generated backend identifier.
529
+ const destructureBindings = [];
530
+ const params = node.getParameters().map((p, paramIndex) => {
496
531
  const typeNode = p.getTypeNode();
497
- return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
532
+ const tsType = typeNode ? typeNode.getText() : undefined;
533
+ const nameNode = p.getNameNode();
534
+ if (Node.isIdentifier(nameNode))
535
+ return { name: nameNode.getText(), tsType };
536
+ if (Node.isArrayBindingPattern(nameNode)) {
537
+ throw new Error(`array binding pattern in arrow parameter not yet supported: ${nameNode.getText()}`);
538
+ }
539
+ if (!Node.isObjectBindingPattern(nameNode)) {
540
+ throw new Error(`unsupported arrow parameter binding: ${p.getName()}`);
541
+ }
542
+ if (p.getInitializer()) {
543
+ throw new Error(`defaulted object binding pattern in arrow parameter not yet supported: ${p.getText()}`);
544
+ }
545
+ const paramName = freshName(`_lambdaParam${paramIndex}`);
546
+ for (const el of nameNode.getElements()) {
547
+ if (el.getDotDotDotToken()) {
548
+ throw new Error(`rest property in arrow parameter destructuring not yet supported: ${el.getText()}`);
549
+ }
550
+ if (el.getInitializer()) {
551
+ throw new Error(`default value in arrow parameter destructuring not yet supported: ${el.getText()}`);
552
+ }
553
+ const localNode = el.getNameNode();
554
+ if (!Node.isIdentifier(localNode)) {
555
+ throw new Error(`nested binding pattern in arrow parameter destructuring not yet supported: ${el.getText()}`);
556
+ }
557
+ const localName = localNode.getText();
558
+ const propNode = el.getPropertyNameNode();
559
+ if (propNode && !Node.isIdentifier(propNode)) {
560
+ throw new Error(`computed or literal property in arrow parameter destructuring not yet supported: ${el.getText()}`);
561
+ }
562
+ destructureBindings.push({
563
+ kind: "let",
564
+ name: localName,
565
+ mutable: false,
566
+ tsType: null,
567
+ init: {
568
+ kind: "field",
569
+ obj: { kind: "var", name: paramName },
570
+ field: propNode ? propNode.getText() : localName,
571
+ },
572
+ line: p.getStartLineNumber(),
573
+ });
574
+ }
575
+ return { name: paramName, tsType };
498
576
  });
499
577
  // Return type from the checker — inferred when unannotated — so resolve can
500
578
  // type return-position record literals and give the lambda a real fn type.
501
579
  const returnTsType = typeToString(node.getReturnType());
502
580
  const body = node.getBody();
503
581
  if (Node.isExpression(body)) {
504
- return { kind: "lambda", params, body: extractExpr(body), returnTsType };
582
+ const expr = extractExpr(body);
583
+ const loweredBody = destructureBindings.length === 0
584
+ ? expr
585
+ : [...destructureBindings, { kind: "return", value: expr, line: body.getStartLineNumber() }];
586
+ return { kind: "lambda", params, body: loweredBody, returnTsType };
505
587
  }
506
588
  if (Node.isBlock(body)) {
507
- return { kind: "lambda", params, body: extractStmts(body.getStatements()), returnTsType };
589
+ return { kind: "lambda", params, body: [...destructureBindings, ...extractStmts(body.getStatements())], returnTsType };
508
590
  }
509
591
  throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
510
592
  }
@@ -1242,12 +1324,10 @@ function extractStmts(stmts) {
1242
1324
  continue;
1243
1325
  }
1244
1326
  if (Node.isVariableStatement(s)) {
1245
- const havocMatch = s.getLeadingCommentRanges()
1246
- .map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+)|(?:\s+(\S+)))?$/))
1247
- .find(m => m !== null);
1248
- const havocType = havocMatch?.[1]?.trim() ?? null; // //@ havoc : Type
1249
- const havocKey = havocMatch?.[2] ?? null; // //@ havoc key
1250
- const isHavoc = !!havocMatch;
1327
+ const havoc = havocDirective(s);
1328
+ const havocType = havoc?.type ?? null; // //@ havoc : Type
1329
+ const havocKey = havoc?.key ?? null; // //@ havoc key
1330
+ const isHavoc = !!havoc;
1251
1331
  for (const d of s.getDeclarations()) {
1252
1332
  // Havoc on destructuring: emit each named binding as a separate havoced variable
1253
1333
  const nameNode = d.getNameNode();
@@ -1654,10 +1734,16 @@ function extractStmts(stmts) {
1654
1734
  // B: ...`) — the stripped breaks are the switch exits.
1655
1735
  const clauseInfos = s.getClauses().map(clause => {
1656
1736
  const stmts = extractStmts(clause.getStatements());
1737
+ let label = null;
1738
+ if (Node.isCaseClause(clause)) {
1739
+ const labelExpr = extractExpr(clause.getExpression());
1740
+ if (labelExpr.kind !== "str") {
1741
+ throw new Error(`Unsupported switch case at line ${clause.getStartLineNumber()}: expected a string literal`);
1742
+ }
1743
+ label = labelExpr.value;
1744
+ }
1657
1745
  return {
1658
- label: Node.isCaseClause(clause)
1659
- ? clause.getExpression().getText().replace(/^["']|["']$/g, "")
1660
- : null,
1746
+ label,
1661
1747
  stmts,
1662
1748
  exits: isExit(stmts[stmts.length - 1]),
1663
1749
  };
@@ -1688,7 +1774,11 @@ function extractStmts(stmts) {
1688
1774
  // functions this would emit the wrong shape, but lsc has no current
1689
1775
  // examples of explicit bare return in void functions; revisit if one
1690
1776
  // appears.
1691
- result.push({ kind: "return", value: expr ? extractExpr(expr) : { kind: "var", name: "undefined" }, line });
1777
+ // `//@ havoc <key>` on a return abstracts the matching calls or new
1778
+ // expressions inside the returned expression — there is no variable to
1779
+ // hang a whole-value havoc on, so only the key form applies here.
1780
+ const value = withHavocKey(havocDirective(s)?.key ?? null, () => expr ? extractExpr(expr) : { kind: "var", name: "undefined" });
1781
+ result.push({ kind: "return", value, line });
1692
1782
  continue;
1693
1783
  }
1694
1784
  if (Node.isBreakStatement(s)) {
@@ -1704,14 +1794,12 @@ function extractStmts(stmts) {
1704
1794
  // //@ havoc before `x = e` — discard the RHS, assign a nondeterministic
1705
1795
  // value of x's type. Only applies to plain `=` with an identifier LHS;
1706
1796
  // compound assigns, `arr[i] = v`, and `x++` fall through to desugaring.
1707
- const havocMatch = s.getLeadingCommentRanges()
1708
- .map(r => r.getText().trim().match(/^\/\/@ havoc(?:\s*:\s*(.+))?$/))
1709
- .find(m => m !== null);
1710
- if (havocMatch && Node.isBinaryExpression(expr)
1797
+ const havoc = havocDirective(s);
1798
+ if (havoc && !havoc.key && Node.isBinaryExpression(expr)
1711
1799
  && expr.getOperatorToken().getText() === "="
1712
1800
  && Node.isIdentifier(expr.getLeft())) {
1713
1801
  const target = expr.getLeft().getText();
1714
- const tsType = havocMatch[1]?.trim() ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
1802
+ const tsType = havoc.type ?? _eraseGenerics(typeToString(expr.getLeft().getType()));
1715
1803
  result.push({ kind: "assign", target, value: { kind: "havoc", tsType }, line });
1716
1804
  continue;
1717
1805
  }
@@ -1901,6 +1989,12 @@ function extractFunctionInner(fn, parentAnnotations) {
1901
1989
  return "void"; // Promise<void>
1902
1990
  }
1903
1991
  const node = fn.getReturnTypeNode();
1992
+ // A type predicate (`x is T` / `asserts x is T`) is a `boolean` at
1993
+ // runtime; the narrowing it performs is a TS-only refinement with no
1994
+ // counterpart in the model. Without this, `getText()` yields "x is T"
1995
+ // and the type mapper reads the subject name as an opaque type.
1996
+ if (node && node.getKind() === SyntaxKind.TypePredicate)
1997
+ return "boolean";
1904
1998
  if (node && Node.isUnionTypeNode(node))
1905
1999
  return _eraseGenerics(_tsTypeFromUnionNode(node));
1906
2000
  if (node)
@@ -2461,6 +2555,28 @@ export function extractModule(sourceFile) {
2461
2555
  }
2462
2556
  }
2463
2557
  }
2558
+ // A constant's initializer can reference other constants
2559
+ // (`const ZERO_NINE = ZERO + nthDigit(-1)`). Close over those initializers
2560
+ // before filtering — mirroring the transitive type filter below — or a
2561
+ // constant reachable only from another constant is dropped and the backend
2562
+ // sees an undefined name.
2563
+ // Snapshot first: what the closure adds are VALUE references, and a value
2564
+ // and a type can share a name (`const Action = Schema.Literals(…)` next to
2565
+ // `type Action = Schema.Schema.Type<typeof Action>`). Keeping the constant
2566
+ // alive must not also drag in the unrelated type alias, so the type filter
2567
+ // below runs off the pre-closure set.
2568
+ const typeReferencedNames = new Set(referencedNames);
2569
+ for (let grew = true; grew;) {
2570
+ grew = false;
2571
+ for (const c of constants) {
2572
+ if (!referencedNames.has(c.name))
2573
+ continue;
2574
+ const before = referencedNames.size;
2575
+ collectNamesExpr(c.value);
2576
+ if (referencedNames.size !== before)
2577
+ grew = true;
2578
+ }
2579
+ }
2464
2580
  constants.splice(0, constants.length, ...constants.filter(c => referencedNames.has(c.name)));
2465
2581
  // Filter types to only those referenced by verified functions (transitive)
2466
2582
  const neededTypes = new Set();
@@ -2479,7 +2595,7 @@ export function extractModule(sourceFile) {
2479
2595
  for (const m of f.tsType.matchAll(/\b([A-Z]\w*)\b/g))
2480
2596
  markType(m[1]);
2481
2597
  }
2482
- for (const name of referencedNames)
2598
+ for (const name of typeReferencedNames)
2483
2599
  markType(name);
2484
2600
  // Signature types also mark their base after stripping array/optional
2485
2601
  // WRAPPERS (`Out[]`/`Msg | undefined` → `Out`/`Msg`), so a function returning
package/tools/dist/ir.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  export const pWild = () => ({ kind: "wild" });
8
8
  export const pCtor = (c, ...binders) => ({ kind: "ctor", ctor: c, binders });
9
+ export const pLiteral = (value) => ({ kind: "literal", value });
9
10
  /** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
10
11
  export function patternBinders(p) {
11
12
  return p.kind === "ctor" ? p.binders : [];
@@ -196,9 +196,13 @@ let _boolCtx = false;
196
196
  function leanCtorName(name) {
197
197
  return /^[A-Za-z_][A-Za-z0-9_'!?]*$/.test(name) ? name : `«${name}»`;
198
198
  }
199
- /** Render a match pattern to Lean syntax: `_`, `.none`, `.some x`, `.syn seq`. */
199
+ /** Render a match pattern to Lean syntax: `_`, a quoted literal, or `.ctor args`. */
200
200
  function renderLeanPattern(p) {
201
- return p.kind === "wild" ? "_" : "." + [leanCtorName(p.ctor), ...p.binders].join(" ");
201
+ if (p.kind === "wild")
202
+ return "_";
203
+ if (p.kind === "literal")
204
+ return emitExpr({ kind: "str", value: p.value });
205
+ return "." + [leanCtorName(p.ctor), ...p.binders].join(" ");
202
206
  }
203
207
  // A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
204
208
  // (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
@@ -354,7 +358,30 @@ function emitExpr(e, parentPrec) {
354
358
  const obj = emitExpr(e.obj);
355
359
  const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall" || e.obj.kind === "if" || e.obj.kind === "let";
356
360
  const receiver = wrap ? `(${obj})` : obj;
357
- const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall") ? `(${emitExpr(a)})` : emitExpr(a));
361
+ // Predicate-taking array methods require a Bool-valued callback even when
362
+ // the surrounding expression returns an array/option rather than Bool.
363
+ // Usually Lean can coerce a decidable Prop-valued predicate. A predicate
364
+ // containing a raw Bool match cannot be coerced through ∧/∨/¬, though, so
365
+ // switch just that callback to computational connectives. This is local:
366
+ // setting the enclosing definition's Bool context would incorrectly
367
+ // affect unrelated arguments and would still miss predicates inside an
368
+ // Array-returning definition such as `xs.filter(...)`.
369
+ const predicateMethod = e.objTy.kind === "array" &&
370
+ ["filter", "every", "some", "find"].includes(e.method);
371
+ const args = e.args.map((a, i) => {
372
+ const prevBoolCtx = _boolCtx;
373
+ if (predicateMethod && i === 0)
374
+ _boolCtx = prevBoolCtx || needsBoolConnectives(a);
375
+ let rendered;
376
+ try {
377
+ rendered = emitExpr(a);
378
+ }
379
+ finally {
380
+ _boolCtx = prevBoolCtx;
381
+ }
382
+ return (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall")
383
+ ? `(${rendered})` : rendered;
384
+ });
358
385
  return emitMethodCall(e.objTy.kind, e.method, e.monadic, receiver, args);
359
386
  }
360
387
  case "lambda": {
@@ -385,7 +412,7 @@ function emitExpr(e, parentPrec) {
385
412
  const ctor = rhs.type ? _unionCtors.get(rhs.type)?.find(c => c.name === rhs.name) : undefined;
386
413
  if (ctor && ctor.fields.length > 0) {
387
414
  const [yes, no] = e.op === "=" ? ["true", "false"] : ["false", "true"];
388
- return `(match ${emitExpr(e.left)} with | .${escapeName(rhs.name)} .. => ${yes} | _ => ${no})`;
415
+ return `(match ${emitExpr(e.left)} with | .${leanCtorName(rhs.name)} .. => ${yes} | _ => ${no})`;
389
416
  }
390
417
  }
391
418
  // `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
@@ -471,7 +498,7 @@ function emitExpr(e, parentPrec) {
471
498
  const idx = owner.fields.findIndex(f => f.name === e.field);
472
499
  const pats = owner.fields.map((_, i) => (i === idx ? "_v" : "_")).join(" ");
473
500
  const fty = tyToLean(owner.fields[idx].type);
474
- return `(match ${emitExpr(e.obj)} with | .${escapeName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
501
+ return `(match ${emitExpr(e.obj)} with | .${leanCtorName(owner.name)} ${pats} => _v | _ => (default : ${fty}))`;
475
502
  }
476
503
  }
477
504
  const obj = emitExpr(e.obj);
@@ -55,10 +55,36 @@ function resolveTsType(tsType, overrides, varName) {
55
55
  }
56
56
  return parseTsType(tsType);
57
57
  }
58
- /** If expr is a string literal and targetTy is a user type, coerce the literal's type. */
58
+ /** Infer a conditional's result type after its branches have been resolved or
59
+ * contextually coerced. A void branch is the source-level null/undefined arm. */
60
+ function conditionalResultTy(thenTy, elseTy) {
61
+ let ty = thenTy.kind !== "unknown" ? thenTy : elseTy;
62
+ if (thenTy.kind === "void" && elseTy.kind !== "void" && elseTy.kind !== "unknown") {
63
+ ty = { kind: "optional", inner: elseTy };
64
+ }
65
+ else if (elseTy.kind === "void" && thenTy.kind !== "void" && thenTy.kind !== "unknown") {
66
+ ty = { kind: "optional", inner: thenTy };
67
+ }
68
+ else if (thenTy.kind === "optional" && elseTy.kind !== "optional" && elseTy.kind !== "unknown") {
69
+ ty = thenTy;
70
+ }
71
+ else if (elseTy.kind === "optional" && thenTy.kind !== "optional" && thenTy.kind !== "unknown") {
72
+ ty = elseTy;
73
+ }
74
+ return ty;
75
+ }
76
+ /** Contextually type string literals as constructors. Ternary branches inherit
77
+ * the ternary's target, and an optional target contributes its payload type —
78
+ * the caller adds the Some wrapper only after the payload has been coerced. */
59
79
  function coerceStr(expr, targetTy) {
60
- if (expr.kind === "str" && targetTy.kind === "user")
61
- return { ...expr, ty: targetTy };
80
+ const payloadTy = targetTy.kind === "optional" ? targetTy.inner : targetTy;
81
+ if (expr.kind === "str" && payloadTy.kind === "user")
82
+ return { ...expr, ty: payloadTy };
83
+ if (expr.kind === "conditional") {
84
+ const then_ = coerceStr(expr.then, payloadTy);
85
+ const else_ = coerceStr(expr.else, payloadTy);
86
+ return { ...expr, then: then_, else: else_, ty: conditionalResultTy(then_.ty, else_.ty) };
87
+ }
62
88
  return expr;
63
89
  }
64
90
  // ── Helpers ──────────────────────────────────────────────────
@@ -84,6 +110,7 @@ function findSynthArrayUnion(name, typeDecls) {
84
110
  * Returns `value` unchanged if no coercion applies (types already match,
85
111
  * source is unknown, or no rule matches). */
86
112
  function coerceToTargetTy(value, targetTy, typeDecls) {
113
+ value = coerceStr(value, targetTy);
87
114
  if (value.ty.kind === "unknown" || value.ty.kind === "void")
88
115
  return value;
89
116
  if (targetTy.kind === "optional" && value.ty.kind !== "optional") {
@@ -328,10 +355,33 @@ function isUnmodeledTy(ty, typeDecls) {
328
355
  function isStringUnionTy(ty, typeDecls) {
329
356
  return declOfTy(typeDecls, ty)?.kind === "string-union";
330
357
  }
358
+ /** Whether ts-morph widened a string-union initializer to the declared string
359
+ * shape. Inferred optional locals need the same rescue as bare locals:
360
+ * `Option<string>` from TS must not erase an `Option<Color>` initializer. */
361
+ function isWidenedStringUnionTy(declTy, initTy, typeDecls) {
362
+ if (declTy.kind === "string" && isStringUnionTy(initTy, typeDecls))
363
+ return true;
364
+ if (declTy.kind === "optional" && initTy.kind === "optional") {
365
+ return isWidenedStringUnionTy(declTy.inner, initTy.inner, typeDecls);
366
+ }
367
+ return false;
368
+ }
331
369
  /** Infer quantifier variable type from usage in body.
332
370
  * If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
333
371
  * return the collection's key type. Otherwise return null (default to int). */
334
372
  function inferQuantVarType(varName, body, ctx) {
373
+ // Spec membership uses a binary node (`x in collection`), unlike the
374
+ // executable `.has(x)` / `.includes(x)` calls handled below. Infer the
375
+ // quantified variable from the RHS collection so documented forms such as
376
+ // `forall(x, x in \result ==> ...)` do not silently fall back to `int`.
377
+ if (body.kind === "binop" && body.op === "in" &&
378
+ body.left.kind === "var" && body.left.name === varName) {
379
+ const collectionTy = resolveExpr(body.right, ctx).ty;
380
+ if (collectionTy.kind === "map")
381
+ return collectionTy.key;
382
+ if (collectionTy.kind === "set" || collectionTy.kind === "array")
383
+ return collectionTy.elem;
384
+ }
335
385
  // Look for membership/lookup builtins (map.has(k), map.get(k),
336
386
  // array.includes(k) — registry `argIsKey`) where k is our variable
337
387
  if (body.kind === "call" && body.fn.kind === "field" &&
@@ -399,6 +449,8 @@ function classifyCall(fn, ctx) {
399
449
  return "pure";
400
450
  if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray")
401
451
  return "pure";
452
+ if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode")
453
+ return "pure";
402
454
  if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
403
455
  return "spec-pure";
404
456
  // Bare-name `//@ extern` declarations are emitted as `function {:axiom}` —
@@ -515,7 +567,7 @@ function isDefinedCheckRawLambda(raw) {
515
567
  const isUndef = (x) => x.kind === "var" && x.name === "undefined";
516
568
  return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
517
569
  }
518
- /** Coerce call arguments: string literals user types, non-optional → Some, pad missing optional args. */
570
+ /** Coerce call arguments to their declared parameter slots and pad missing optional args. */
519
571
  function coerceCallArgs(args, fn, ctx) {
520
572
  if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
521
573
  return args;
@@ -523,11 +575,7 @@ function coerceCallArgs(args, fn, ctx) {
523
575
  args = args.map((a, i) => {
524
576
  if (i >= paramTys.length)
525
577
  return a;
526
- a = coerceStr(a, paramTys[i]);
527
- if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
528
- return wrapSome(a, paramTys[i]);
529
- }
530
- return a;
578
+ return coerceToTargetTy(a, paramTys[i], ctx.typeDecls);
531
579
  });
532
580
  // Pad missing optional args with None
533
581
  for (let i = args.length; i < paramTys.length; i++) {
@@ -546,6 +594,11 @@ function inferMethodReturnTy(fn, args, ctx) {
546
594
  if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
547
595
  return { kind: "bool" };
548
596
  }
597
+ // `String.fromCharCode(n)` is the inverse of `s.charCodeAt(i)`: an int code
598
+ // point in, a one-character string out.
599
+ if (fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode") {
600
+ return { kind: "string" };
601
+ }
549
602
  // Math.* numeric builtins: abs/min/max preserve the operand's numeric type
550
603
  // (real if any operand is real); floor/ceil/round/trunc return an integer.
551
604
  if (fn.obj.kind === "var" && fn.obj.name === "Math") {
@@ -803,7 +856,7 @@ function resolveExpr(e, ctx) {
803
856
  if (ext) {
804
857
  const args = e.args.map(a => resolveExpr(a, ctx));
805
858
  const fn = { kind: "var", name: ext.flat, ty: { kind: "unknown" } };
806
- return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure" };
859
+ return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure", paramTys: ext.params };
807
860
  }
808
861
  }
809
862
  const fn = resolveExpr(e.fn, ctx);
@@ -819,7 +872,8 @@ function resolveExpr(e, ctx) {
819
872
  const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
820
873
  let args = coerceCallArgs(rawArgs.map((a, i) => {
821
874
  let aCtx = argCtx;
822
- if (paramTypes && i < paramTypes.length && paramTypes[i].kind === "user") {
875
+ if (paramTypes && i < paramTypes.length &&
876
+ (paramTypes[i].kind === "user" || paramTypes[i].kind === "array" || paramTypes[i].kind === "optional")) {
823
877
  aCtx = { ...aCtx, returnTy: paramTypes[i] };
824
878
  }
825
879
  return resolveExpr(a, aCtx);
@@ -854,7 +908,8 @@ function resolveExpr(e, ctx) {
854
908
  }
855
909
  const builtinId = fn.kind === "field" ? recognizeBuiltin(fn.obj.ty, fn.field) : null;
856
910
  return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx),
857
- ...(builtinId ? { builtinId } : {}) };
911
+ ...(builtinId ? { builtinId } : {}),
912
+ ...(paramTypes ? { paramTys: paramTypes } : {}) };
858
913
  }
859
914
  case "index": {
860
915
  const obj = resolveExpr(e.obj, ctx);
@@ -935,7 +990,7 @@ function resolveExpr(e, ctx) {
935
990
  const inner = left.ty.kind === "optional" ? left.ty.inner : left.ty;
936
991
  // The default shares the result type, so coerce a string literal to a
937
992
  // string-union enum (e.g. `availableLevels[0] ?? "off"`).
938
- const right = coerceStr(resolveExpr(e.right, ctx), inner);
993
+ const right = coerceToTargetTy(resolveExpr(e.right, ctx), inner, ctx.typeDecls);
939
994
  // `??` is only total when its default is: with a nullable right operand
940
995
  // (rule-chain style `ruleA(e) ?? ruleB(e) ?? null`), the result stays
941
996
  // optional — otherwise the enclosing chain level loses its optionality
@@ -1047,7 +1102,6 @@ function resolveExpr(e, ctx) {
1047
1102
  let value = resolveExpr(f.value, valueCtx);
1048
1103
  if (fieldDecl) {
1049
1104
  const declTy = fieldDecl.type;
1050
- value = coerceStr(value, declTy);
1051
1105
  // Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
1052
1106
  if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
1053
1107
  value = { kind: "arrayLiteral", elems: [], ty: declTy };
@@ -1088,7 +1142,7 @@ function resolveExpr(e, ctx) {
1088
1142
  const elems = e.elems.map((el, i) => {
1089
1143
  const slot = slots[i];
1090
1144
  const r = resolveExpr(el, slot ? { ...ctx, returnTy: slot } : ctx);
1091
- return slot ? coerceStr(r, slot) : r;
1145
+ return slot ? coerceToTargetTy(r, slot, ctx.typeDecls) : r;
1092
1146
  });
1093
1147
  return { kind: "arrayLiteral", elems, ty: { kind: "tuple", elems: elems.map(x => x.ty) } };
1094
1148
  }
@@ -1105,7 +1159,7 @@ function resolveExpr(e, ctx) {
1105
1159
  const r = resolveExpr(el, elemCtx);
1106
1160
  // Coerce a bare string-literal element to a string-union enum (e.g.
1107
1161
  // `["off", …]: ModelThinkingLevel[]`), like return/arg positions.
1108
- return expectedElem ? coerceStr(r, expectedElem) : r;
1162
+ return expectedElem ? coerceToTargetTy(r, expectedElem, ctx.typeDecls) : r;
1109
1163
  });
1110
1164
  const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
1111
1165
  // No expected collection type: infer array vs tuple from the elements —
@@ -1173,21 +1227,7 @@ function resolveExpr(e, ctx) {
1173
1227
  let else_ = resolveExpr(e.else, elseCtx);
1174
1228
  then_ = coerceStr(then_, else_.ty);
1175
1229
  else_ = coerceStr(else_, then_.ty);
1176
- let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
1177
- if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
1178
- ty = { kind: "optional", inner: else_.ty };
1179
- }
1180
- else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
1181
- ty = { kind: "optional", inner: then_.ty };
1182
- }
1183
- else if (then_.ty.kind === "optional" && else_.ty.kind !== "optional" && else_.ty.kind !== "unknown") {
1184
- // Asymmetric optional: one branch returns Option<T>, the other returns T.
1185
- // Widen to Option<T> so callers/return-coercion see the wider type.
1186
- ty = then_.ty;
1187
- }
1188
- else if (else_.ty.kind === "optional" && then_.ty.kind !== "optional" && then_.ty.kind !== "unknown") {
1189
- ty = else_.ty;
1190
- }
1230
+ const ty = conditionalResultTy(then_.ty, else_.ty);
1191
1231
  return { kind: "conditional", cond, then: then_, else: else_, ty };
1192
1232
  }
1193
1233
  case "emptyCollection": {
@@ -1284,7 +1324,16 @@ function resolveStmt(s, ctx) {
1284
1324
  // one optional level when consulting returnTy.
1285
1325
  const initCtx = (declTy.kind === "user" || declTy.kind === "array" || declTy.kind === "optional")
1286
1326
  ? { ...ctx, returnTy: declTy } : ctx;
1287
- const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
1327
+ let init = coerceStr(resolveExpr(s.init, initCtx), declTy);
1328
+ // Under noUncheckedIndexedAccess, TS gives `const e = arr[i]` type T | undefined
1329
+ // while the index expression itself resolves to T. Leave that mismatch intact:
1330
+ // narrow.ts's ruleOptionalIndexBinding adds the runtime bounds guard and the
1331
+ // corresponding Some/None branches. Wrapping here would turn it into an
1332
+ // unconditional Some(arr[i]) and prevent that JS-semantics rewrite from firing.
1333
+ const deferOptionalIndex = declTy.kind === "optional" && init.kind === "index" &&
1334
+ init.obj.ty.kind === "array" && init.ty.kind !== "optional";
1335
+ if (!deferOptionalIndex)
1336
+ init = coerceToTargetTy(init, declTy, ctx.typeDecls);
1288
1337
  let ty;
1289
1338
  if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
1290
1339
  // ts-morph's declared type is opaque to us (an expanded union it made
@@ -1295,7 +1344,7 @@ function resolveStmt(s, ctx) {
1295
1344
  ? { kind: "optional", inner: init.ty }
1296
1345
  : init.ty;
1297
1346
  }
1298
- else if (declTy.kind === "string" && isStringUnionTy(init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
1347
+ else if (isWidenedStringUnionTy(declTy, init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
1299
1348
  // ts-morph widened a string-union to `string`; keep the initializer's
1300
1349
  // datatype so `local === "lit"` lowers to a discriminant test.
1301
1350
  ty = init.ty;
@@ -1321,22 +1370,11 @@ function resolveStmt(s, ctx) {
1321
1370
  // to their named datatypes.
1322
1371
  const valueCtx = (targetTy.kind === "user" || targetTy.kind === "array" || targetTy.kind === "optional")
1323
1372
  ? { ...ctx, returnTy: targetTy } : ctx;
1324
- let value = coerceStr(resolveExpr(s.value, valueCtx), targetTy);
1325
- // Auto-wrap non-optional value in Some when target is optional
1326
- const isUndef = value.kind === "var" && value.name === "undefined";
1327
- if (targetTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "unknown" && !isUndef) {
1328
- value = wrapSome(value, targetTy);
1329
- }
1373
+ const value = coerceToTargetTy(resolveExpr(s.value, valueCtx), targetTy, ctx.typeDecls);
1330
1374
  return [{ kind: "assign", target: s.target, value }, ctx.env];
1331
1375
  }
1332
1376
  case "return": {
1333
- let value = coerceStr(resolveExpr(s.value, ctx), ctx.returnTy);
1334
- // Wrap non-optional return value in Some when function returns optional
1335
- // Skip if already optional, void, or undefined (which maps to None)
1336
- const isUndef = value.kind === "var" && value.name === "undefined";
1337
- if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
1338
- value = wrapSome(value, ctx.returnTy);
1339
- }
1377
+ const value = coerceToTargetTy(resolveExpr(s.value, ctx), ctx.returnTy, ctx.typeDecls);
1340
1378
  return [{ kind: "return", value }, ctx.env];
1341
1379
  }
1342
1380
  case "break":
@@ -1452,14 +1490,27 @@ function resolveStmt(s, ctx) {
1452
1490
  }
1453
1491
  }
1454
1492
  // ── Pure / return-in-loop detection ──────────────────────────
1455
- /** Syntactic purity: no while, no for-of, no mutable let. */
1493
+ /** Whether a raw statement or expression tree contains a havoc anywhere. */
1494
+ function containsHavoc(v) {
1495
+ if (Array.isArray(v))
1496
+ return v.some(containsHavoc);
1497
+ if (v === null || typeof v !== "object")
1498
+ return false;
1499
+ if (v.kind === "havoc")
1500
+ return true;
1501
+ return Object.values(v).some(containsHavoc);
1502
+ }
1503
+ /** Syntactic purity: no while, no for-of, no mutable let, no havoc. */
1456
1504
  function isSyntacticallyPure(stmts) {
1457
1505
  for (const s of stmts) {
1506
+ // Havoc lowers to Dafny's `*`, which is only valid in a method.
1507
+ if (containsHavoc(s))
1508
+ return false;
1458
1509
  switch (s.kind) {
1459
1510
  case "while":
1460
1511
  case "forof": return false;
1461
1512
  case "let":
1462
- if (s.mutable || s.init.kind === "havoc")
1513
+ if (s.mutable)
1463
1514
  return false;
1464
1515
  break;
1465
1516
  case "if":
@@ -1779,7 +1830,8 @@ export function resolveModule(raw) {
1779
1830
  // record literals on map-typed constants (e.g. `Record<string, number>`)
1780
1831
  // get their `ty` set to `map<...>` rather than `user("...")`.
1781
1832
  const valueCtx = { ...emptyCtx, returnTy: ty };
1782
- return { name: c.name, ty, value: resolveExpr(c.value, valueCtx) };
1833
+ const value = coerceToTargetTy(resolveExpr(c.value, valueCtx), ty, raw.typeDecls);
1834
+ return { name: c.name, ty, value };
1783
1835
  });
1784
1836
  const moduleConstants = new Map(constants.map(c => [c.name, c.ty]));
1785
1837
  return {
@@ -5,7 +5,7 @@
5
5
  * No type lookups, no string parsing, no re-inference.
6
6
  */
7
7
  import { tyEqual } from "./typedir.js";
8
- import { anyExprInStmts, pWild, pCtor, patternBinders, patternBinds, patternCtor } from "./ir.js";
8
+ import { anyExprInStmts, pWild, pCtor, pLiteral, patternBinders, patternBinds, patternCtor } from "./ir.js";
9
9
  import { parseTsType } from "./types.js";
10
10
  import { freshName } from "./names.js";
11
11
  import { builtinSpec } from "./builtins.js";
@@ -234,7 +234,10 @@ function kindHelperDecl(decl) {
234
234
  * arm body would still be captured, so prime on any module-wide collision.
235
235
  * Deterministic, so the pattern binder and its body substitutions agree. */
236
236
  function matchBinder(fieldName, prefix) {
237
- return freshName(prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`);
237
+ const safePrefix = prefix === "\\result"
238
+ ? "result"
239
+ : prefix?.replace(/[^A-Za-z0-9_]/g, "_");
240
+ return freshName(safePrefix ? `_${safePrefix}_${fieldName}` : `_${fieldName}`);
238
241
  }
239
242
  /** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
240
243
  function buildMatchPattern(variantName, fields, scopePrefix) {
@@ -358,20 +361,33 @@ function flattenLambdaBody(stmts) {
358
361
  * field, index, record, forall, or exists sub-expressions.
359
362
  */
360
363
  /** JS truthiness coercion for `if`/`while`/`?:` conditions.
361
- * Dafny requires bool; coerce number→`≠0`, string→non-empty, array→`true`
364
+ * Dafny requires bool; coerce number→`!== 0`, string→non-empty, array→`true`
362
365
  * (every array, even `[]`, is truthy in JS).
363
- * Optional conds are handled separately by narrow.ts (rewritten to someMatch). */
364
- function coerceCondToBool(cond, ty) {
365
- if (ty.kind === "bool")
366
- return cond;
367
- if (ty.kind === "int" || ty.kind === "nat")
368
- return { kind: "binop", op: "≠", left: cond, right: { kind: "num", value: 0 } };
369
- if (ty.kind === "string")
370
- return { kind: "binop", op: ">", left: { kind: "field", obj: cond, field: "length" }, right: { kind: "num", value: 0 } };
366
+ *
367
+ * Rewrites the typed tree, before lowering, and distributes over `&&`/`||`:
368
+ * each operand of a logical connective is itself in condition position, and the
369
+ * operands need not share a type — `i >= 0 && carry`, with `carry` an int, is a
370
+ * bool conjoined with a number. A conjunction takes its type from its right
371
+ * operand (resolve), so coercing the whole expression by its type would emit
372
+ * `((i >= 0) && carry) != 0`, which is not well-typed.
373
+ *
374
+ * Optional conds never arrive here — narrow.ts rewrites them to someMatch. */
375
+ function asCondition(e) {
376
+ const bool = { kind: "bool" };
377
+ if (e.ty.kind === "bool")
378
+ return e;
379
+ if (e.kind === "binop" && (e.op === "&&" || e.op === "||"))
380
+ return { ...e, left: asCondition(e.left), right: asCondition(e.right), ty: bool };
381
+ if (e.ty.kind === "int" || e.ty.kind === "nat")
382
+ return { kind: "binop", op: "!==", left: e, right: { kind: "num", value: 0, ty: e.ty }, ty: bool };
383
+ if (e.ty.kind === "string")
384
+ return { kind: "binop", op: ">",
385
+ left: { kind: "field", obj: e, field: "length", ty: { kind: "nat" } },
386
+ right: { kind: "num", value: 0, ty: { kind: "nat" } }, ty: bool };
371
387
  // Arrays, objects, maps, sets, tuples are always truthy in JS (even `[]`/`{}`).
372
- if (["array", "user", "map", "set", "tuple"].includes(ty.kind))
373
- return { kind: "bool", value: true };
374
- return cond;
388
+ if (["array", "user", "map", "set", "tuple"].includes(e.ty.kind))
389
+ return { kind: "bool", value: true, ty: bool };
390
+ return e;
375
391
  }
376
392
  /** Wrap an expression in Some/None for optional-typed conditionals.
377
393
  * If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
@@ -802,6 +818,12 @@ function lowerExpr(e, binds) {
802
818
  }
803
819
  }
804
820
  }
821
+ // String.fromCharCode(n) → preamble function (inverse of charCodeAt,
822
+ // which lowers to `(s[i] as int)`).
823
+ if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "String" &&
824
+ e.fn.field === "fromCharCode" && e.args.length === 1) {
825
+ return { kind: "app", fn: "StringFromCharCode", args: [lowerExpr(e.args[0], binds)] };
826
+ }
805
827
  // Math.abs/min/max → preamble functions
806
828
  if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
807
829
  if (e.fn.field === "abs" && e.args.length === 1)
@@ -1032,7 +1054,7 @@ function lowerExpr(e, binds) {
1032
1054
  // JS truthiness coercion (string/array/int → ... > 0). Matches SPEC §3.1
1033
1055
  // negation forms (`!s` → `s == ""`). Optional conds are already
1034
1056
  // rewritten to someMatch by narrow.ts.
1035
- const cond = coerceCondToBool(lowerExpr(e.cond, binds), e.cond.ty);
1057
+ const cond = lowerExpr(asCondition(e.cond), binds);
1036
1058
  let thenExpr = lowerExpr(e.then, binds);
1037
1059
  let elseExpr = lowerExpr(e.else, binds);
1038
1060
  if (e.ty.kind === "optional") {
@@ -1365,13 +1387,23 @@ function matchToIfChains(stmts) {
1365
1387
  if (s.kind !== "match")
1366
1388
  return [s];
1367
1389
  const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
1368
- const ctorArms = arms.filter(a => a.pattern.kind !== "wild");
1390
+ // Literal-value matches are not user-inductive matches and need no
1391
+ // discriminator rewrite. In particular, don't let a later ctor arm make us
1392
+ // accidentally drop an earlier literal arm from a malformed mixed match.
1393
+ if (arms.some(a => a.pattern.kind === "literal"))
1394
+ return [{ ...s, arms }];
1395
+ const ctorArms = arms.filter(a => a.pattern.kind === "ctor");
1369
1396
  const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
1370
1397
  const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
1371
1398
  if (!decl)
1372
1399
  return [{ ...s, arms }]; // not a user union (e.g. Option) — leave as match
1373
1400
  const scrutExpr = s.scrutinee;
1374
1401
  const defaultArm = arms.find(a => a.pattern.kind === "wild");
1402
+ const declaredCtors = decl.kind === "discriminated-union"
1403
+ ? decl.variants?.map(v => v.name)
1404
+ : decl.kind === "string-union" ? decl.values : undefined;
1405
+ const coveredCtors = new Set(ctorArms.map(a => patternCtor(a.pattern)).filter((c) => !!c));
1406
+ const exhaustiveWithoutDefault = !defaultArm && !!declaredCtors && declaredCtors.every(c => coveredCtors.has(c));
1375
1407
  let elseBranch = defaultArm ? defaultArm.body : [];
1376
1408
  for (let k = ctorArms.length - 1; k >= 0; k--) {
1377
1409
  const armBody = ctorArms[k].body;
@@ -1401,6 +1433,13 @@ function matchToIfChains(stmts) {
1401
1433
  value: { kind: "field", obj: scrutExpr, field: f.name, fromUnion: decl.name, ctor },
1402
1434
  });
1403
1435
  });
1436
+ // An exhaustive source match has no fallthrough. Use its final arm as
1437
+ // the unconditional else branch; emitting `else pure ()` would force a
1438
+ // Unit result even when every arm returns the method's result type.
1439
+ if (exhaustiveWithoutDefault && k === ctorArms.length - 1) {
1440
+ elseBranch = [...lets, ...armBody];
1441
+ continue;
1442
+ }
1404
1443
  elseBranch = [{ kind: "if", cond, then: [...lets, ...armBody], else: elseBranch }];
1405
1444
  }
1406
1445
  return elseBranch;
@@ -1754,13 +1793,13 @@ function transformStmt(s, typeDecls) {
1754
1793
  }
1755
1794
  case "if": {
1756
1795
  // Lift from condition only (Lean rule: don't lift from branches).
1757
- const { binds, expr: cond } = liftMethodCalls(s.cond);
1758
- return [...binds, { kind: "if", cond: coerceCondToBool(cond, s.cond.ty), then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
1796
+ const { binds, expr: cond } = liftMethodCalls(asCondition(s.cond));
1797
+ return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
1759
1798
  }
1760
1799
  case "while":
1761
1800
  return [{
1762
1801
  kind: "while",
1763
- cond: coerceCondToBool(transformExpr(s.cond), s.cond.ty),
1802
+ cond: transformExpr(asCondition(s.cond)),
1764
1803
  invariants: s.invariants.map(transformExpr),
1765
1804
  decreasing: s.decreases ? transformExpr(s.decreases) : null,
1766
1805
  doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
@@ -1808,10 +1847,18 @@ function mapStmtExprs(s, r) {
1808
1847
  * and delegates body transformation to the caller-provided function.
1809
1848
  * Returns null if any body transformation returns null (pure path abort). */
1810
1849
  function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1811
- const decl = typeName ? declOf(typeDecls, typeName) : undefined;
1850
+ const decl = declOf(typeDecls, typeName);
1851
+ if (!decl || (decl.kind !== "string-union" && decl.kind !== "discriminated-union")) {
1852
+ throw new Error(`match type ${typeName} is not a declared union`);
1853
+ }
1812
1854
  const arms = [];
1813
1855
  for (const c of cases) {
1814
1856
  const variant = decl?.variants?.find(v => v.name === c.name);
1857
+ const declared = decl.kind === "string-union"
1858
+ ? decl.values?.includes(c.name)
1859
+ : !!variant;
1860
+ if (!declared)
1861
+ throw new Error(`case ${JSON.stringify(c.name)} is not a variant of ${typeName}`);
1815
1862
  const fields = variant?.fields ?? [];
1816
1863
  const pattern = buildMatchPattern(c.name, fields, varName);
1817
1864
  const body = transformBody(c.body, varName, fields, c.name);
@@ -1821,6 +1868,19 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1821
1868
  }
1822
1869
  return arms;
1823
1870
  }
1871
+ /** Build value-pattern arms for a switch on a plain string. Case labels have
1872
+ * already been validated and decoded by extraction; unlike datatype cases,
1873
+ * these are literal values and introduce no constructor-field binders. */
1874
+ function buildLiteralMatchArms(cases, transformBody) {
1875
+ const arms = [];
1876
+ for (const c of cases) {
1877
+ const body = transformBody(c.body);
1878
+ if (body === null)
1879
+ return null;
1880
+ arms.push({ pattern: pLiteral(c.name), body });
1881
+ }
1882
+ return arms;
1883
+ }
1824
1884
  function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
1825
1885
  const decl = declOf(typeDecls, typeName);
1826
1886
  // Synth array-unions (discriminant "__isArray__") have single-field variants
@@ -1903,8 +1963,8 @@ function remainingVariant(typeName, cases, typeDecls) {
1903
1963
  /** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
1904
1964
  * `field`, assuming `obj` is a discriminated union with `field` as its
1905
1965
  * discriminant. When that's NOT so — e.g. `obj` is a plain record with an
1906
- * enum-typed `field` — the switch is really on the enum VALUE. This returns the
1907
- * enum scrutinee `obj.field` (+ the field's enum type) to match directly; null
1966
+ * enum- or string-typed `field` — the switch is really on the field VALUE. This
1967
+ * returns the scrutinee `obj.field` (+ its resolved type) to match directly; null
1908
1968
  * for a genuine discriminant switch or `switch(localVar)`, which callers handle
1909
1969
  * their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
1910
1970
  function enumFieldSwitch(s, typeDecls) {
@@ -1916,9 +1976,18 @@ function enumFieldSwitch(s, typeDecls) {
1916
1976
  const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
1917
1977
  return {
1918
1978
  scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
1919
- enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
1979
+ fieldTy,
1920
1980
  };
1921
1981
  }
1982
+ /** Declared datatype matched by a switch, excluding records/aliases/opaque
1983
+ * types. A plain `string` intentionally has no declaration and uses literal
1984
+ * patterns instead. */
1985
+ function switchCtorDecl(typeDecls, ty) {
1986
+ if (!ty)
1987
+ return undefined;
1988
+ const decl = declOfTy(typeDecls, ty);
1989
+ return decl?.kind === "string-union" || decl?.kind === "discriminated-union" ? decl : undefined;
1990
+ }
1922
1991
  /** Stamp variant ctor info onto datatype updates of the match scrutinee in
1923
1992
  * lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
1924
1993
  * `replaceFieldAccess`'s stamping. Emitters need the pin to use
@@ -1937,15 +2006,24 @@ function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
1937
2006
  function emitSwitchStmt(s, typeDecls) {
1938
2007
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1939
2008
  const ef = enumFieldSwitch(s, typeDecls);
2009
+ const matchTy = ef ? ef.fieldTy : s.expr.ty;
2010
+ const literalCases = matchTy?.kind === "string";
2011
+ const ctorDecl = switchCtorDecl(typeDecls, matchTy);
2012
+ if (!literalCases && !ctorDecl) {
2013
+ const typeName = !matchTy ? "unknown" : matchTy.kind === "user" ? matchTy.name : matchTy.kind;
2014
+ throw new Error(`switch scrutinee must be string-typed or a declared union, got ${typeName}`);
2015
+ }
1940
2016
  const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
1941
- const arms = ef
1942
- ? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
1943
- : buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields, ctorName) => {
1944
- let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
1945
- if (ctorName && vn && baseName)
1946
- out = stampScrutineeUpdates(out, vn, ctorName, baseName);
1947
- return out;
1948
- });
2017
+ const arms = literalCases
2018
+ ? buildLiteralMatchArms(cases, (body) => transformStmts(body, typeDecls))
2019
+ : ef
2020
+ ? buildMatchArms(cases, undefined, ctorDecl.name, typeDecls, (body) => transformStmts(body, typeDecls))
2021
+ : buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", ctorDecl.name, typeDecls, (body, vn, fields, ctorName) => {
2022
+ let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
2023
+ if (ctorName && vn && baseName)
2024
+ out = stampScrutineeUpdates(out, vn, ctorName, baseName);
2025
+ return out;
2026
+ });
1949
2027
  if (s.defaultBody.length > 0)
1950
2028
  arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
1951
2029
  return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
@@ -1967,14 +2045,67 @@ function replaceFieldsInTStmts(stmts, objName, replacements) {
1967
2045
  }));
1968
2046
  }
1969
2047
  /** Replace all variant fields of obj → match binder vars in typed IR.
1970
- * Thin wrapper around replaceFieldsInTStmts for discriminant match/switch. */
2048
+ * Before replacing field reads, realize TypeScript's structural argument
2049
+ * conversion for calls such as `helper(outcome)` inside a narrowed union arm.
2050
+ * The source value is still the enclosing union in typed IR, while Dafny and
2051
+ * Lean expect the helper's nominal record. Rebuild that record solely from
2052
+ * the fields bound by this arm's match pattern. */
1971
2053
  function replaceFieldAccessInTStmts(stmts, varName, fields) {
1972
- return replaceFieldsInTStmts(stmts, varName, fields.map(f => ({
2054
+ const projected = projectStructuralCallArgsInTStmts(stmts, varName, fields);
2055
+ return replaceFieldsInTStmts(projected, varName, fields.map(f => ({
1973
2056
  fieldName: f.name,
1974
2057
  newName: matchBinder(f.name, varName),
1975
2058
  fallbackTy: f.type ?? parseTsType(f.tsType),
1976
2059
  })));
1977
2060
  }
2061
+ /** Project a narrowed union scrutinee into a named structural record expected
2062
+ * by a same-module/extern call. Resolution stamps named calls with paramTys;
2063
+ * this pass fires only when every target record field has an identically typed
2064
+ * match-bound source field. That deliberately avoids inventing a broad cast:
2065
+ * it is the nominal-backend witness for the structural call TS already accepts. */
2066
+ function projectStructuralCallArgsInTStmts(stmts, varName, fields) {
2067
+ const sourceFields = fields.map(f => ({
2068
+ ...f,
2069
+ resolvedTy: f.type ?? parseTsType(f.tsType),
2070
+ }));
2071
+ return stmts.map(s => mapTStmt(s, e => {
2072
+ if (e.kind !== "call" || !e.paramTys)
2073
+ return null;
2074
+ const paramTys = e.paramTys;
2075
+ let changed = false;
2076
+ const args = e.args.map((arg, i) => {
2077
+ if (arg.kind !== "var" || arg.name !== varName || i >= paramTys.length)
2078
+ return arg;
2079
+ const targetTy = paramTys[i];
2080
+ const targetDecl = declOfTy(_typeDecls, targetTy);
2081
+ if (targetTy.kind !== "user" || targetDecl?.kind !== "record" || !targetDecl.fields)
2082
+ return arg;
2083
+ const matched = [];
2084
+ for (const targetField of targetDecl.fields) {
2085
+ const sourceField = sourceFields.find(f => f.name === targetField.name);
2086
+ const targetFieldTy = targetField.type ?? parseTsType(targetField.tsType);
2087
+ if (!sourceField || !tyEqual(sourceField.resolvedTy, targetFieldTy))
2088
+ return arg;
2089
+ matched.push({ targetField, sourceField });
2090
+ }
2091
+ changed = true;
2092
+ return {
2093
+ kind: "record",
2094
+ spread: null,
2095
+ fields: matched.map(m => ({
2096
+ name: m.targetField.name,
2097
+ value: {
2098
+ kind: "var",
2099
+ name: matchBinder(m.sourceField.name, varName),
2100
+ ty: m.sourceField.resolvedTy,
2101
+ },
2102
+ })),
2103
+ ty: targetTy,
2104
+ };
2105
+ });
2106
+ return changed ? { ...e, args } : null;
2107
+ }));
2108
+ }
1978
2109
  /** Replace obj.field → replacement var in typed IR expressions (before lowering).
1979
2110
  * Mirrors replaceFieldsInTStmts but operates on a single TExpr tree. */
1980
2111
  function replaceFieldInTExpr(expr, objName, replacements) {
@@ -2062,7 +2193,7 @@ function transformPureBody(stmts, typeDecls) {
2062
2193
  const elseExpr = transformPureBody(elseStmts, typeDecls);
2063
2194
  if (!elseExpr)
2064
2195
  return null;
2065
- return { kind: "if", cond: coerceCondToBool(transformExpr(s.cond), s.cond.ty), then: thenExpr, else: elseExpr };
2196
+ return { kind: "if", cond: transformExpr(asCondition(s.cond)), then: thenExpr, else: elseExpr };
2066
2197
  }
2067
2198
  case "switch": return transformPureSwitch(s, typeDecls);
2068
2199
  case "someMatch": {
@@ -2095,7 +2226,13 @@ function transformPureSwitch(s, typeDecls) {
2095
2226
  const ef = enumFieldSwitch(s, typeDecls);
2096
2227
  if (ef) {
2097
2228
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
2098
- const arms = buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformPureBody(body, typeDecls));
2229
+ const literalCases = ef.fieldTy?.kind === "string";
2230
+ const ctorDecl = switchCtorDecl(typeDecls, ef.fieldTy);
2231
+ if (!literalCases && !ctorDecl)
2232
+ return null;
2233
+ const arms = literalCases
2234
+ ? buildLiteralMatchArms(cases, (body) => transformPureBody(body, typeDecls))
2235
+ : buildMatchArms(cases, undefined, ctorDecl.name, typeDecls, (body) => transformPureBody(body, typeDecls));
2099
2236
  if (!arms)
2100
2237
  return null;
2101
2238
  if (s.defaultBody.length > 0) {
@@ -2106,13 +2243,15 @@ function transformPureSwitch(s, typeDecls) {
2106
2243
  }
2107
2244
  return { kind: "match", scrutinee: ef.scrutinee, arms };
2108
2245
  }
2109
- const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
2110
- if (!declOf(typeDecls, typeName))
2246
+ const ctorDecl = switchCtorDecl(typeDecls, s.expr.ty);
2247
+ if (!ctorDecl)
2111
2248
  return null;
2249
+ const typeName = ctorDecl.name;
2112
2250
  const varName = s.expr.kind === "var" ? s.expr.name : undefined;
2113
2251
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
2114
2252
  const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {
2115
- let result = transformPureBody(body, typeDecls);
2253
+ const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
2254
+ let result = transformPureBody(projected, typeDecls);
2116
2255
  if (!result)
2117
2256
  return null;
2118
2257
  if (fields.length > 0 && vn)
@@ -2139,7 +2278,8 @@ function transformPureMatch(chain, typeDecls) {
2139
2278
  // the statement-level counterpart of this substitution.
2140
2279
  const isSynthArrayUnion = decl?.discriminant === "__isArray__";
2141
2280
  const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields, ctorName) => {
2142
- let result = transformPureBody(body, typeDecls);
2281
+ const projected = vn ? projectStructuralCallArgsInTStmts(body, vn, fields) : body;
2282
+ let result = transformPureBody(projected, typeDecls);
2143
2283
  if (!result)
2144
2284
  return null;
2145
2285
  if (fields.length > 0 && vn)
@@ -2159,7 +2299,8 @@ function transformPureMatch(chain, typeDecls) {
2159
2299
  const remaining = remainingVariant(chain.typeName, chain.cases, typeDecls);
2160
2300
  if (remaining) {
2161
2301
  // Exactly one variant left — destructure for variant-specific field access.
2162
- let body = transformPureBody(chain.fallthrough, typeDecls);
2302
+ const projected = projectStructuralCallArgsInTStmts(chain.fallthrough, chain.varName, remaining.fields);
2303
+ let body = transformPureBody(projected, typeDecls);
2163
2304
  if (!body)
2164
2305
  return null;
2165
2306
  if (remaining.fields.length > 0)