lemmascript 0.3.1 → 0.3.3

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.
@@ -108,7 +108,9 @@ function wrapSome(value, optionalTy) {
108
108
  }
109
109
  /** Detect `v !== undefined` or `undefined !== v` where v: optional<T>.
110
110
  * Handles simple variables, field access chains, and arbitrary expressions.
111
- * When `fieldExpr` is returned, callers must use `substituteRawExpr` to narrow.
111
+ * When `fieldExpr` is returned for a simple field chain (obj.field), callers
112
+ * can narrow via `narrowedFields` context. For complex expressions (calls, etc.),
113
+ * callers should fall back to `substituteRawExpr`.
112
114
  *
113
115
  * Does NOT recurse into `&&` — callers that need to detect optional checks
114
116
  * inside `&&` conditions should check `cond.left` explicitly. */
@@ -141,6 +143,19 @@ function detectOptionalCheck(cond, ctx) {
141
143
  }
142
144
  return null;
143
145
  }
146
+ /** Collect all optional narrowings from an early-return condition.
147
+ * Handles single checks (x === undefined) and compound || chains
148
+ * (x === undefined || y === undefined). */
149
+ function collectEarlyReturnNarrowings(cond, ctx) {
150
+ if (cond.kind === "binop" && cond.op === "||") {
151
+ return [...collectEarlyReturnNarrowings(cond.left, ctx), ...collectEarlyReturnNarrowings(cond.right, ctx)];
152
+ }
153
+ const narrowed = detectOptionalCheck(cond, ctx);
154
+ if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
155
+ return [{ varName: narrowed.varName, innerTy: narrowed.innerTy }];
156
+ }
157
+ return [];
158
+ }
144
159
  /** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
145
160
  function isRefMutableInTS(ty) {
146
161
  return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
@@ -293,6 +308,8 @@ function inferMethodReturnTy(fn, args, ctx) {
293
308
  else if (objTy.kind === "array") {
294
309
  if (fn.field === "includes")
295
310
  return { kind: "bool" };
311
+ if (fn.field === "indexOf")
312
+ return { kind: "int" };
296
313
  if (fn.field === "shift")
297
314
  return objTy.elem;
298
315
  if (fn.field === "push" || fn.field === "concat")
@@ -387,15 +404,28 @@ function resolveExpr(e, ctx) {
387
404
  fn.obj.ty.elem.kind === "user") {
388
405
  argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
389
406
  }
390
- const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
391
- const ty = inferMethodReturnTy(fn, args, ctx);
407
+ // Propagate parameter types to arguments for record literal resolution
408
+ // (enables inline discriminated union construction in function arguments)
409
+ const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
410
+ const args = coerceCallArgs(rawArgs.map((a, i) => {
411
+ let aCtx = argCtx;
412
+ if (paramTypes && i < paramTypes.length && paramTypes[i].kind === "user") {
413
+ aCtx = { ...aCtx, returnTy: paramTypes[i] };
414
+ }
415
+ return resolveExpr(a, aCtx);
416
+ }), fn, ctx);
417
+ let ty = inferMethodReturnTy(fn, args, ctx);
418
+ // For same-file function calls, use the known return type
419
+ if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
420
+ ty = ctx.fnReturns.get(fn.name);
421
+ }
392
422
  return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
393
423
  }
394
424
  case "index": {
395
425
  const obj = resolveExpr(e.obj, ctx);
396
426
  const idx = resolveExpr(e.idx, ctx);
397
427
  const idxTy = obj.ty.kind === "array" ? obj.ty.elem
398
- : obj.ty.kind === "map" ? obj.ty.value
428
+ : obj.ty.kind === "map" ? { kind: "optional", inner: obj.ty.value }
399
429
  : { kind: "unknown" };
400
430
  return { kind: "index", obj, idx, ty: idxTy };
401
431
  }
@@ -403,16 +433,24 @@ function resolveExpr(e, ctx) {
403
433
  const obj = resolveExpr(e.obj, ctx);
404
434
  let isDiscriminant = false;
405
435
  let ty = { kind: "unknown" };
406
- if (e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
436
+ // Check narrowed field context (from conditional optional checks on field chains)
437
+ if (obj.kind === "var" && ctx.narrowedFields.length > 0) {
438
+ const nf = ctx.narrowedFields.find(n => n.objName === obj.name && n.fieldName === e.field);
439
+ if (nf)
440
+ ty = nf.narrowedTy;
441
+ }
442
+ if (ty.kind === "unknown" && e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
407
443
  ty = { kind: "nat" };
408
444
  }
409
- else if (e.field === "size" && (obj.ty.kind === "map" || obj.ty.kind === "set")) {
445
+ else if (ty.kind === "unknown" && e.field === "size" && (obj.ty.kind === "map" || obj.ty.kind === "set")) {
410
446
  ty = { kind: "nat" };
411
447
  }
412
- else if (obj.ty.kind === "user") {
413
- if (getDiscriminant(ctx, obj.ty.name) === e.field)
448
+ else if (ty.kind === "unknown" && obj.ty.kind === "user") {
449
+ // Strip generic args for type lookup: "Result<Model, Err>" "Result"
450
+ const baseTyName = obj.ty.name.includes("<") ? obj.ty.name.slice(0, obj.ty.name.indexOf("<")) : obj.ty.name;
451
+ if (getDiscriminant(ctx, baseTyName) === e.field)
414
452
  isDiscriminant = true;
415
- const decl = findDecl(ctx, obj.ty.name);
453
+ const decl = findDecl(ctx, baseTyName);
416
454
  if (decl?.kind === "record") {
417
455
  const f = decl.fields?.find(f => f.name === e.field);
418
456
  if (f)
@@ -440,11 +478,20 @@ function resolveExpr(e, ctx) {
440
478
  // Clear returnTy for field values — it applies to THIS record, not nested ones
441
479
  const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
442
480
  const fields = e.fields.map(f => {
443
- let value = resolveExpr(f.value, fieldCtx);
444
481
  const fieldDecl = decl?.fields?.find(df => df.name === f.name);
482
+ // Propagate declared field type into context so nested records resolve
483
+ // their union variant correctly (e.g., { kind: 'Idle' } → EffectMode.Idle)
484
+ const valueCtx = (fieldDecl?.type?.kind === "user")
485
+ ? { ...fieldCtx, returnTy: fieldDecl.type }
486
+ : fieldCtx;
487
+ let value = resolveExpr(f.value, valueCtx);
445
488
  if (fieldDecl) {
446
489
  const declTy = fieldDecl.type;
447
490
  value = coerceStr(value, declTy);
491
+ // Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
492
+ if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
493
+ value = { kind: "arrayLiteral", elems: [], ty: declTy };
494
+ }
448
495
  // Coerce non-optional to optional: wrap in Some (only when value type is concrete)
449
496
  if (declTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "void" && value.ty.kind !== "unknown") {
450
497
  value = wrapSome(value, declTy);
@@ -493,7 +540,6 @@ function resolveExpr(e, ctx) {
493
540
  case "conditional": {
494
541
  const cond = resolveExpr(e.cond, ctx);
495
542
  let narrowedVar;
496
- let narrowedExprResolved;
497
543
  let thenCtx = ctx;
498
544
  let rawThen = e.then;
499
545
  // Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
@@ -509,23 +555,56 @@ function resolveExpr(e, ctx) {
509
555
  thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
510
556
  }
511
557
  }
512
- // Phase 2: Explicit check — v !== undefined (simple vars, field chains,
513
- // complex expressions all handled uniformly by detectOptionalCheck)
558
+ // Phase 2/3: Explicit check — v !== undefined, or && with optional check.
559
+ // Resolve only narrows the type environment; transform handles all structural
560
+ // narrowing (match generation, variable binding, && splitting).
561
+ let narrowedExprResolved;
562
+ let elseCtx = ctx;
514
563
  if (!narrowedVar) {
515
564
  const narrowed = detectOptionalCheck(e.cond, ctx)
516
- // Phase 3: && with optional check — (v !== undefined && ...) ? ... : ...
517
565
  ?? (e.cond.kind === "binop" && e.cond.op === "&&" ? detectOptionalCheck(e.cond.left, ctx) : null);
518
566
  if (narrowed && narrowed.inThen) {
519
- narrowedVar = narrowed.varName;
520
- thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
521
- if (narrowed.fieldExpr) {
567
+ if (!narrowed.fieldExpr) {
568
+ // Simple var: extend env only — transform will detect and generate match
569
+ thenCtx = withEnv(thenCtx, extend(thenCtx.env, narrowed.varName, narrowed.innerTy));
570
+ }
571
+ else if (narrowed.fieldExpr.kind === "field" && narrowed.fieldExpr.obj.kind === "var") {
572
+ // Simple field chain (obj.field): narrow via field context — no substitution
573
+ thenCtx = {
574
+ ...thenCtx,
575
+ narrowedFields: [...thenCtx.narrowedFields, {
576
+ objName: narrowed.fieldExpr.obj.name,
577
+ fieldName: narrowed.fieldExpr.field,
578
+ narrowedTy: narrowed.innerTy,
579
+ }],
580
+ };
581
+ }
582
+ else {
583
+ // Complex expression (call result, deep chain, etc.): transform can't detect these,
584
+ // so keep old behavior — substitute + narrowedVar + narrowedExpr
585
+ narrowedVar = narrowed.varName;
522
586
  narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
523
587
  rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
588
+ thenCtx = withEnv(thenCtx, extend(thenCtx.env, narrowed.varName, narrowed.innerTy));
589
+ }
590
+ }
591
+ else if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
592
+ // v === undefined: narrow v in the else branch
593
+ elseCtx = withEnv(elseCtx, extend(elseCtx.env, narrowed.varName, narrowed.innerTy));
594
+ }
595
+ // Compound || with === undefined: narrow all checked vars in else branch
596
+ // e.g. if (a === undefined || b === undefined) then X else Y → narrow a,b in Y
597
+ // TODO: resolve-time narrowing works but transform doesn't emit match unwrap
598
+ // for || conditions yet — decompose || into nested matches in transform.
599
+ // Workaround: split || into separate if guards in user code.
600
+ if (!narrowed && e.cond.kind === "binop" && e.cond.op === "||") {
601
+ for (const n of collectEarlyReturnNarrowings(e.cond, ctx)) {
602
+ elseCtx = withEnv(elseCtx, extend(elseCtx.env, n.varName, n.innerTy));
524
603
  }
525
604
  }
526
605
  }
527
606
  let then_ = resolveExpr(rawThen, thenCtx);
528
- let else_ = resolveExpr(e.else, ctx);
607
+ let else_ = resolveExpr(e.else, elseCtx);
529
608
  then_ = coerceStr(then_, else_.ty);
530
609
  else_ = coerceStr(else_, then_.ty);
531
610
  let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
@@ -537,8 +616,7 @@ function resolveExpr(e, ctx) {
537
616
  ty = { kind: "optional", inner: then_.ty };
538
617
  }
539
618
  // When narrowedExpr is set AND a branch is void, the match produces Optional
540
- const hasVoidBranch = then_.ty.kind === "void" || else_.ty.kind === "void";
541
- if (narrowedExprResolved && hasVoidBranch && ty.kind !== "optional") {
619
+ if (narrowedExprResolved && (then_.ty.kind === "void" || else_.ty.kind === "void") && ty.kind !== "optional") {
542
620
  ty = { kind: "optional", inner: ty };
543
621
  }
544
622
  return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
@@ -579,12 +657,13 @@ function resolveBlock(stmts, ctx) {
579
657
  result.push(typed);
580
658
  env = nextEnv;
581
659
  // Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
660
+ // Also handles compound: if (x === undefined || y === undefined) { return }
582
661
  // Field chains are excluded — resolve can't substitute in statement lists;
583
662
  // transform's emitOptionalMatch handles field chains in statement contexts.
584
663
  if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) {
585
- const narrowed = detectOptionalCheck(s.cond, withEnv(ctx, env));
586
- if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
587
- env = extend(env, narrowed.varName, narrowed.innerTy);
664
+ const narrowings = collectEarlyReturnNarrowings(s.cond, withEnv(ctx, env));
665
+ for (const n of narrowings) {
666
+ env = extend(env, n.varName, n.innerTy);
588
667
  }
589
668
  }
590
669
  }
@@ -593,8 +672,13 @@ function resolveBlock(stmts, ctx) {
593
672
  function resolveStmt(s, ctx) {
594
673
  switch (s.kind) {
595
674
  case "let": {
596
- const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
597
- const init = coerceStr(resolveExpr(s.init, ctx), ty);
675
+ const declTy = resolveTsType(s.tsType, ctx.overrides, s.name);
676
+ // Propagate declared type as returnTy so nested record expressions
677
+ // resolve union variants correctly (e.g., EffectState → mode: EffectMode → { kind: 'Idle' })
678
+ const initCtx = declTy.kind === "user" ? { ...ctx, returnTy: declTy } : ctx;
679
+ const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
680
+ // Map indexing: TS says T, but access can fail → use Optional<T> from init
681
+ const ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
598
682
  // const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
599
683
  const mutable = s.mutable || isRefMutableInTS(ty);
600
684
  return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
@@ -825,6 +909,8 @@ function collectCallsStmts(stmts, fns, out) {
825
909
  }
826
910
  function computePureFns(functions) {
827
911
  const allFnNames = new Set(functions.map(fn => fn.name));
912
+ // //@ pure functions are always considered pure — never taint callers
913
+ const forcePure = new Set(functions.filter(fn => fn.pure).map(fn => fn.name));
828
914
  // Build call graph: fn → set of same-file functions it calls
829
915
  const callGraph = new Map();
830
916
  for (const fn of functions) {
@@ -832,8 +918,8 @@ function computePureFns(functions) {
832
918
  collectCallsStmts(fn.body, allFnNames, calls);
833
919
  callGraph.set(fn.name, calls);
834
920
  }
835
- // Seed: syntactically non-pure functions
836
- const nonPure = new Set(functions.filter(fn => !isSyntacticallyPure(fn.body)).map(fn => fn.name));
921
+ // Seed: syntactically non-pure functions (skip //@ pure)
922
+ const nonPure = new Set(functions.filter(fn => !forcePure.has(fn.name) && !isSyntacticallyPure(fn.body)).map(fn => fn.name));
837
923
  // Build reverse graph: fn → set of functions that call it
838
924
  const callers = new Map();
839
925
  for (const name of allFnNames)
@@ -842,12 +928,12 @@ function computePureFns(functions) {
842
928
  for (const callee of callees)
843
929
  callers.get(callee).add(caller);
844
930
  }
845
- // Propagate impurity through reverse call graph
931
+ // Propagate impurity through reverse call graph (skip //@ pure)
846
932
  const worklist = [...nonPure];
847
933
  while (worklist.length > 0) {
848
934
  const fn = worklist.pop();
849
935
  for (const caller of callers.get(fn) ?? []) {
850
- if (!nonPure.has(caller)) {
936
+ if (!nonPure.has(caller) && !forcePure.has(caller)) {
851
937
  nonPure.add(caller);
852
938
  worklist.push(caller);
853
939
  }
@@ -880,7 +966,7 @@ function containsReturn(stmts) {
880
966
  return false;
881
967
  }
882
968
  // ── Resolve function / module ────────────────────────────────
883
- function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
969
+ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map(), opts) {
884
970
  const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
885
971
  const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
886
972
  const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
@@ -889,7 +975,7 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
889
975
  env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
890
976
  for (const p of params)
891
977
  env = extend(env, p.name, p.ty);
892
- const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
978
+ const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false, narrowedFields: [] };
893
979
  const requiresCtx = { ...baseCtx, inSpec: true };
894
980
  const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
895
981
  // Apply type parameter constraints from //@ type T (==) annotations
@@ -901,17 +987,19 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
901
987
  name: fn.name, typeParams, params, returnTy,
902
988
  requires: resolveSpecs(fn.requires, requiresCtx),
903
989
  ensures: resolveSpecs(fn.ensures, ensuresCtx),
990
+ decreases: fn.decreases ? resolveSpec(fn.decreases, requiresCtx) : null,
904
991
  isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
992
+ forcePure: fn.pure,
905
993
  body: resolveBlock(fn.body, baseCtx),
906
994
  };
907
995
  }
908
- function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
996
+ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map(), fnReturns = new Map()) {
909
997
  const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
910
998
  // Create a synthetic record type for 'this' so field access resolves
911
999
  const thisType = { kind: "user", name: cls.name };
912
1000
  const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
913
1001
  const allTypeDecls = [...typeDecls, thisDecl];
914
- const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
1002
+ const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, fnReturns, {
915
1003
  thisBinding: { name: "this", ty: thisType },
916
1004
  forcePure: false, // class methods are never pure (they access this)
917
1005
  }));
@@ -933,15 +1021,18 @@ function precomputeFieldTypes(typeDecls) {
933
1021
  }
934
1022
  }
935
1023
  export function resolveModule(raw) {
1024
+ _synVarCounter = 0;
936
1025
  precomputeFieldTypes(raw.typeDecls);
937
1026
  const pureFns = computePureFns(raw.functions);
938
- // Pre-compute function parameter types for optional coercion
1027
+ // Pre-compute function parameter and return types
939
1028
  const fnParams = new Map();
1029
+ const fnReturns = new Map();
940
1030
  for (const fn of raw.functions) {
941
1031
  const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
942
1032
  fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
1033
+ fnReturns.set(fn.name, resolveTsType(fn.returnType, overrides, "\\result"));
943
1034
  }
944
- const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, inSpec: false, inLambda: false };
1035
+ const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, fnReturns, inSpec: false, inLambda: false, narrowedFields: [] };
945
1036
  const constants = (raw.constants ?? []).map(c => ({
946
1037
  name: c.name,
947
1038
  ty: parseTsType(c.tsType),
@@ -951,7 +1042,7 @@ export function resolveModule(raw) {
951
1042
  file: raw.file,
952
1043
  typeDecls: raw.typeDecls,
953
1044
  constants,
954
- functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams)),
955
- classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams)),
1045
+ functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams, fnReturns)),
1046
+ classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams, fnReturns)),
956
1047
  };
957
1048
  }