lemmascript 0.5.20 → 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.20",
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": {
@@ -1555,15 +1555,18 @@ function qualifyCtor(name, type) {
1555
1555
  return `${type}.${mapped}`;
1556
1556
  return mapped;
1557
1557
  }
1558
- /** Translate a Lean match pattern to Dafny syntax.
1558
+ /** Translate a backend-neutral match pattern to Dafny syntax.
1559
1559
  * ".ctorName field1 field2" → "ctorName(field1, field2)"
1560
1560
  * ".ctorName" → "ctorName"
1561
+ * literal value → quoted Dafny string
1561
1562
  * "_" → "_"
1562
1563
  */
1563
1564
  const CTOR_MAP = { "some": "Some", "none": "None" };
1564
1565
  function translatePattern(p) {
1565
1566
  if (p.kind === "wild")
1566
1567
  return "_";
1568
+ if (p.kind === "literal")
1569
+ return emitExpr({ kind: "str", value: p.value });
1567
1570
  const ctorName = (Object.hasOwn(CTOR_MAP, p.ctor) ? CTOR_MAP[p.ctor] : undefined) ?? dafnyCtorName(p.ctor);
1568
1571
  if (p.binders.length === 0)
1569
1572
  return ctorName;
@@ -515,19 +515,78 @@ function extractExpr(node) {
515
515
  }
516
516
  // Arrow function: (x) => expr or (x) => { stmts }
517
517
  if (Node.isArrowFunction(node)) {
518
- 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) => {
519
531
  const typeNode = p.getTypeNode();
520
- 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 };
521
576
  });
522
577
  // Return type from the checker — inferred when unannotated — so resolve can
523
578
  // type return-position record literals and give the lambda a real fn type.
524
579
  const returnTsType = typeToString(node.getReturnType());
525
580
  const body = node.getBody();
526
581
  if (Node.isExpression(body)) {
527
- 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 };
528
587
  }
529
588
  if (Node.isBlock(body)) {
530
- return { kind: "lambda", params, body: extractStmts(body.getStatements()), returnTsType };
589
+ return { kind: "lambda", params, body: [...destructureBindings, ...extractStmts(body.getStatements())], returnTsType };
531
590
  }
532
591
  throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
533
592
  }
@@ -1675,10 +1734,16 @@ function extractStmts(stmts) {
1675
1734
  // B: ...`) — the stripped breaks are the switch exits.
1676
1735
  const clauseInfos = s.getClauses().map(clause => {
1677
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
+ }
1678
1745
  return {
1679
- label: Node.isCaseClause(clause)
1680
- ? clause.getExpression().getText().replace(/^["']|["']$/g, "")
1681
- : null,
1746
+ label,
1682
1747
  stmts,
1683
1748
  exits: isExit(stmts[stmts.length - 1]),
1684
1749
  };
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": {
@@ -370,6 +370,18 @@ function isWidenedStringUnionTy(declTy, initTy, typeDecls) {
370
370
  * If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
371
371
  * return the collection's key type. Otherwise return null (default to int). */
372
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
+ }
373
385
  // Look for membership/lookup builtins (map.has(k), map.get(k),
374
386
  // array.includes(k) — registry `argIsKey`) where k is our variable
375
387
  if (body.kind === "call" && body.fn.kind === "field" &&
@@ -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";
@@ -1387,7 +1387,12 @@ function matchToIfChains(stmts) {
1387
1387
  if (s.kind !== "match")
1388
1388
  return [s];
1389
1389
  const arms = s.arms.map(a => ({ ...a, body: matchToIfChains(a.body) }));
1390
- 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");
1391
1396
  const firstCtor = ctorArms[0] ? patternCtor(ctorArms[0].pattern) : undefined;
1392
1397
  const decl = firstCtor ? declWithVariant(_typeDecls, firstCtor) : undefined;
1393
1398
  if (!decl)
@@ -1842,10 +1847,18 @@ function mapStmtExprs(s, r) {
1842
1847
  * and delegates body transformation to the caller-provided function.
1843
1848
  * Returns null if any body transformation returns null (pure path abort). */
1844
1849
  function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1845
- 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
+ }
1846
1854
  const arms = [];
1847
1855
  for (const c of cases) {
1848
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}`);
1849
1862
  const fields = variant?.fields ?? [];
1850
1863
  const pattern = buildMatchPattern(c.name, fields, varName);
1851
1864
  const body = transformBody(c.body, varName, fields, c.name);
@@ -1855,6 +1868,19 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1855
1868
  }
1856
1869
  return arms;
1857
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
+ }
1858
1884
  function emitMatchStmt(scrutinee, typeName, cases, fallthrough, typeDecls) {
1859
1885
  const decl = declOf(typeDecls, typeName);
1860
1886
  // Synth array-unions (discriminant "__isArray__") have single-field variants
@@ -1937,8 +1963,8 @@ function remainingVariant(typeName, cases, typeDecls) {
1937
1963
  /** `switch(obj.field)` is stripped at extraction to scrutinee `obj` + discriminant
1938
1964
  * `field`, assuming `obj` is a discriminated union with `field` as its
1939
1965
  * discriminant. When that's NOT so — e.g. `obj` is a plain record with an
1940
- * enum-typed `field` — the switch is really on the enum VALUE. This returns the
1941
- * 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
1942
1968
  * for a genuine discriminant switch or `switch(localVar)`, which callers handle
1943
1969
  * their usual way. Shared by emitSwitchStmt and transformPureSwitch. */
1944
1970
  function enumFieldSwitch(s, typeDecls) {
@@ -1950,9 +1976,18 @@ function enumFieldSwitch(s, typeDecls) {
1950
1976
  const fieldTy = objDecl?.kind === "record" ? objDecl.fields?.find(f => f.name === s.discriminant)?.type : undefined;
1951
1977
  return {
1952
1978
  scrutinee: { kind: "field", obj: transformExpr(s.expr), field: s.discriminant },
1953
- enumTyName: fieldTy?.kind === "user" ? fieldTy.name : undefined,
1979
+ fieldTy,
1954
1980
  };
1955
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
+ }
1956
1991
  /** Stamp variant ctor info onto datatype updates of the match scrutinee in
1957
1992
  * lowered arm bodies (`{ ...vn, f: v }`) — the statement-path twin of
1958
1993
  * `replaceFieldAccess`'s stamping. Emitters need the pin to use
@@ -1971,15 +2006,24 @@ function stampScrutineeUpdates(body, varName, ctorName, ctorOf) {
1971
2006
  function emitSwitchStmt(s, typeDecls) {
1972
2007
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1973
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
+ }
1974
2016
  const baseName = s.expr.ty.kind === "user" ? tyBaseName(s.expr.ty.name) : undefined;
1975
- const arms = ef
1976
- ? buildMatchArms(cases, undefined, ef.enumTyName, typeDecls, (body) => transformStmts(body, typeDecls))
1977
- : buildMatchArms(cases, s.expr.kind === "var" ? s.expr.name : "?", s.expr.ty.kind === "user" ? s.expr.ty.name : undefined, typeDecls, (body, vn, fields, ctorName) => {
1978
- let out = transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls);
1979
- if (ctorName && vn && baseName)
1980
- out = stampScrutineeUpdates(out, vn, ctorName, baseName);
1981
- return out;
1982
- });
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
+ });
1983
2027
  if (s.defaultBody.length > 0)
1984
2028
  arms.push({ pattern: pWild(), body: transformStmts(s.defaultBody, typeDecls) });
1985
2029
  return { kind: "match", scrutinee: ef ? ef.scrutinee : varE(s.expr.kind === "var" ? s.expr.name : "?"), arms };
@@ -2182,7 +2226,13 @@ function transformPureSwitch(s, typeDecls) {
2182
2226
  const ef = enumFieldSwitch(s, typeDecls);
2183
2227
  if (ef) {
2184
2228
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
2185
- 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));
2186
2236
  if (!arms)
2187
2237
  return null;
2188
2238
  if (s.defaultBody.length > 0) {
@@ -2193,9 +2243,10 @@ function transformPureSwitch(s, typeDecls) {
2193
2243
  }
2194
2244
  return { kind: "match", scrutinee: ef.scrutinee, arms };
2195
2245
  }
2196
- const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
2197
- if (!declOf(typeDecls, typeName))
2246
+ const ctorDecl = switchCtorDecl(typeDecls, s.expr.ty);
2247
+ if (!ctorDecl)
2198
2248
  return null;
2249
+ const typeName = ctorDecl.name;
2199
2250
  const varName = s.expr.kind === "var" ? s.expr.name : undefined;
2200
2251
  const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
2201
2252
  const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields, ctorName) => {