lemmascript 0.3.0 → 0.3.1

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.
@@ -99,22 +99,47 @@ function coerceStr(expr, targetTy) {
99
99
  return expr;
100
100
  }
101
101
  // ── Helpers ──────────────────────────────────────────────────
102
- /** Detect `v !== undefined` or `undefined !== v` where v: optional<T>. */
103
- function narrowOptional(cond, env) {
102
+ /** Wrap a resolved expression in Some() for optional coercion. */
103
+ function wrapSome(value, optionalTy) {
104
+ return {
105
+ kind: "call", fn: { kind: "var", name: "Some", ty: optionalTy },
106
+ args: [value], ty: optionalTy, callKind: "pure",
107
+ };
108
+ }
109
+ /** Detect `v !== undefined` or `undefined !== v` where v: optional<T>.
110
+ * Handles simple variables, field access chains, and arbitrary expressions.
111
+ * When `fieldExpr` is returned, callers must use `substituteRawExpr` to narrow.
112
+ *
113
+ * Does NOT recurse into `&&` — callers that need to detect optional checks
114
+ * inside `&&` conditions should check `cond.left` explicitly. */
115
+ function detectOptionalCheck(cond, ctx) {
104
116
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
105
117
  return null;
106
- // v !== undefined OR undefined !== v
107
- let varName = null;
108
- if (cond.left.kind === "var" && cond.right.kind === "var" && cond.right.name === "undefined")
109
- varName = cond.left.name;
110
- if (cond.right.kind === "var" && cond.left.kind === "var" && cond.left.name === "undefined")
111
- varName = cond.right.name;
112
- if (!varName)
118
+ // Identify the expression being checked against undefined
119
+ let optExpr = null;
120
+ if (cond.right.kind === "var" && cond.right.name === "undefined")
121
+ optExpr = cond.left;
122
+ if (cond.left.kind === "var" && cond.left.name === "undefined")
123
+ optExpr = cond.right;
124
+ if (!optExpr)
113
125
  return null;
114
- const ty = lookup(env, varName);
115
- if (!ty || ty.kind !== "optional")
116
- return null;
117
- return { varName, innerTy: ty.inner, inThen: cond.op === "!==" };
126
+ // Simple variable — env lookup, no substitution needed
127
+ if (optExpr.kind === "var") {
128
+ const ty = lookup(ctx.env, optExpr.name);
129
+ if (!ty || ty.kind !== "optional")
130
+ return null;
131
+ return { varName: optExpr.name, innerTy: ty.inner, inThen: cond.op === "!==" };
132
+ }
133
+ // Field access chain or arbitrary expression — resolve to check type, needs substitution
134
+ const resolved = resolveExpr(optExpr, ctx);
135
+ if (resolved.ty.kind === "optional") {
136
+ const synVar = optExpr.kind === "field" ? `_narr${_synVarCounter++}` : `_opt${_synVarCounter++}`;
137
+ return {
138
+ varName: synVar, innerTy: resolved.ty.inner, inThen: cond.op === "!==",
139
+ fieldExpr: optExpr, narrowedExpr: resolved,
140
+ };
141
+ }
142
+ return null;
118
143
  }
119
144
  /** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
120
145
  function isRefMutableInTS(ty) {
@@ -203,10 +228,99 @@ function classifyCall(fn, ctx) {
203
228
  return "method";
204
229
  return "unknown";
205
230
  }
231
+ // ── Call resolution helpers ─────────────────────────────────
232
+ /** Infer lambda param types from array method context (map, filter, etc.).
233
+ * Returns updated rawArgs with inferred tsType on the first lambda param. */
234
+ function inferLambdaParamTypes(fn, rawArgs) {
235
+ if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
236
+ ["map", "filter", "every", "some", "find"].includes(fn.field) &&
237
+ rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
238
+ rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
239
+ const elemTy = fn.obj.ty.elem;
240
+ const tsType = elemTy.kind === "user" ? elemTy.name
241
+ : elemTy.kind === "string" ? "string"
242
+ : elemTy.kind === "int" || elemTy.kind === "nat" ? "number"
243
+ : elemTy.kind === "bool" ? "boolean" : undefined;
244
+ if (tsType) {
245
+ const lam = rawArgs[0];
246
+ const updatedParams = [{ ...lam.params[0], tsType }, ...lam.params.slice(1)];
247
+ return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
248
+ }
249
+ }
250
+ return rawArgs;
251
+ }
252
+ /** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
253
+ function coerceCallArgs(args, fn, ctx) {
254
+ if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
255
+ return args;
256
+ const paramTys = ctx.fnParams.get(fn.name);
257
+ args = args.map((a, i) => {
258
+ if (i >= paramTys.length)
259
+ return a;
260
+ a = coerceStr(a, paramTys[i]);
261
+ if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
262
+ return wrapSome(a, paramTys[i]);
263
+ }
264
+ return a;
265
+ });
266
+ // Pad missing optional args with None
267
+ for (let i = args.length; i < paramTys.length; i++) {
268
+ if (paramTys[i].kind === "optional") {
269
+ args.push({ kind: "var", name: "undefined", ty: paramTys[i] });
270
+ }
271
+ }
272
+ return args;
273
+ }
274
+ /** Infer return type for collection/string method calls. */
275
+ function inferMethodReturnTy(fn, args, ctx) {
276
+ if (fn.kind !== "field")
277
+ return { kind: "unknown" };
278
+ const objTy = fn.obj.ty;
279
+ if (objTy.kind === "map") {
280
+ if (fn.field === "get")
281
+ return ctx.inSpec ? objTy.value : { kind: "optional", inner: objTy.value };
282
+ if (fn.field === "has")
283
+ return { kind: "bool" };
284
+ if (fn.field === "set" || fn.field === "delete")
285
+ return objTy;
286
+ }
287
+ else if (objTy.kind === "set") {
288
+ if (fn.field === "has")
289
+ return { kind: "bool" };
290
+ if (fn.field === "add" || fn.field === "delete")
291
+ return objTy;
292
+ }
293
+ else if (objTy.kind === "array") {
294
+ if (fn.field === "includes")
295
+ return { kind: "bool" };
296
+ if (fn.field === "shift")
297
+ return objTy.elem;
298
+ if (fn.field === "push" || fn.field === "concat")
299
+ return objTy;
300
+ if (fn.field === "filter")
301
+ return objTy;
302
+ if (fn.field === "every" || fn.field === "some")
303
+ return { kind: "bool" };
304
+ if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
305
+ const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
306
+ ? args[0].body[0].value.ty : { kind: "unknown" };
307
+ return { kind: "array", elem: retTy };
308
+ }
309
+ }
310
+ else if (objTy.kind === "string") {
311
+ if (fn.field === "trim" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
312
+ return { kind: "string" };
313
+ if (fn.field === "includes")
314
+ return { kind: "bool" };
315
+ }
316
+ return { kind: "unknown" };
317
+ }
206
318
  // ── Resolve expressions ──────────────────────────────────────
207
319
  function resolveExpr(e, ctx) {
208
320
  switch (e.kind) {
209
321
  case "var":
322
+ if (e.name === "undefined")
323
+ return { kind: "var", name: "undefined", ty: { kind: "void" } };
210
324
  return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
211
325
  case "num":
212
326
  if (!Number.isInteger(e.value))
@@ -228,15 +342,18 @@ function resolveExpr(e, ctx) {
228
342
  }
229
343
  case "binop": {
230
344
  let left = resolveExpr(e.left, ctx);
231
- // && narrowing: if left is "x !== undefined", narrow x for right side
345
+ // && narrowing: if left is "x !== undefined", narrow x for right side.
346
+ // Field chains are excluded — resolve can't substitute in sub-expressions;
347
+ // transform's emitOptionalMatch handles field chains in statement contexts.
232
348
  let rightCtx = ctx;
349
+ let rawRight = e.right;
233
350
  if (e.op === "&&") {
234
- const narrowed = narrowOptional(e.left, ctx.env);
235
- if (narrowed && narrowed.inThen) {
351
+ const narrowed = detectOptionalCheck(e.left, ctx);
352
+ if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
236
353
  rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
237
354
  }
238
355
  }
239
- let right = resolveExpr(e.right, rightCtx);
356
+ let right = resolveExpr(rawRight, rightCtx);
240
357
  if (e.op === "===" || e.op === "!==") {
241
358
  left = coerceStr(left, right.ty);
242
359
  right = coerceStr(right, left.ty);
@@ -263,107 +380,15 @@ function resolveExpr(e, ctx) {
263
380
  }
264
381
  case "call": {
265
382
  const fn = resolveExpr(e.fn, ctx);
266
- // Infer lambda param types from array method context (map, filter, etc.)
267
- let rawArgs = e.args;
268
- if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
269
- ["map", "filter", "every", "some", "find"].includes(fn.field) &&
270
- rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
271
- rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
272
- const elemTy = fn.obj.ty.elem;
273
- const tsType = elemTy.kind === "user" ? elemTy.name
274
- : elemTy.kind === "string" ? "string"
275
- : elemTy.kind === "int" || elemTy.kind === "nat" ? "number"
276
- : elemTy.kind === "bool" ? "boolean" : undefined;
277
- if (tsType) {
278
- const lam = rawArgs[0];
279
- const updatedParams = [{ ...lam.params[0], tsType }, ...lam.params.slice(1)];
280
- rawArgs = [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
281
- }
282
- }
283
- // For .push() on a typed array, resolve the argument with element type context
284
- // so record expressions can match fields and coerce types
383
+ const rawArgs = inferLambdaParamTypes(fn, e.args);
384
+ // For .push() on a typed array, resolve args with element type context
285
385
  let argCtx = ctx;
286
386
  if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
287
387
  fn.obj.ty.elem.kind === "user") {
288
388
  argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
289
389
  }
290
- let args = rawArgs.map(a => resolveExpr(a, argCtx));
291
- // Coerce args: string literals to user types, non-optional to Option, pad missing optional args
292
- if (fn.kind === "var" && ctx.fnParams.has(fn.name)) {
293
- const paramTys = ctx.fnParams.get(fn.name);
294
- args = args.map((a, i) => {
295
- if (i >= paramTys.length)
296
- return a;
297
- // Coerce string literal to user type (e.g., 'MissingList' → Err constructor)
298
- a = coerceStr(a, paramTys[i]);
299
- // Wrap non-optional in Some when callee expects optional param
300
- if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
301
- return {
302
- kind: "call",
303
- fn: { kind: "var", name: "Some", ty: paramTys[i] },
304
- args: [a],
305
- ty: paramTys[i],
306
- callKind: "pure",
307
- };
308
- }
309
- return a;
310
- });
311
- // Pad missing optional args with None
312
- for (let i = args.length; i < paramTys.length; i++) {
313
- if (paramTys[i].kind === "optional") {
314
- args.push({ kind: "var", name: "undefined", ty: paramTys[i] });
315
- }
316
- }
317
- }
318
- let ty = { kind: "unknown" };
319
- // Infer return types for collection methods
320
- if (fn.kind === "field" && fn.obj.ty.kind === "map") {
321
- if (fn.field === "get")
322
- ty = ctx.inSpec ? fn.obj.ty.value : { kind: "optional", inner: fn.obj.ty.value };
323
- else if (fn.field === "has")
324
- ty = { kind: "bool" };
325
- else if (fn.field === "set")
326
- ty = fn.obj.ty;
327
- else if (fn.field === "delete")
328
- ty = fn.obj.ty;
329
- }
330
- else if (fn.kind === "field" && fn.obj.ty.kind === "set") {
331
- if (fn.field === "has")
332
- ty = { kind: "bool" };
333
- else if (fn.field === "add")
334
- ty = fn.obj.ty;
335
- else if (fn.field === "delete")
336
- ty = fn.obj.ty;
337
- }
338
- else if (fn.kind === "field" && fn.obj.ty.kind === "array") {
339
- if (fn.field === "includes")
340
- ty = { kind: "bool" };
341
- else if (fn.field === "shift")
342
- ty = fn.obj.ty.elem;
343
- else if (fn.field === "push")
344
- ty = fn.obj.ty;
345
- else if (fn.field === "concat")
346
- ty = fn.obj.ty;
347
- else if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
348
- const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
349
- ? args[0].body[0].value.ty : { kind: "unknown" };
350
- ty = { kind: "array", elem: retTy };
351
- }
352
- else if (fn.field === "filter")
353
- ty = fn.obj.ty;
354
- else if (fn.field === "every" || fn.field === "some")
355
- ty = { kind: "bool" };
356
- }
357
- else if (fn.kind === "field" && fn.obj.ty.kind === "string") {
358
- if (fn.field === "trim")
359
- ty = { kind: "string" };
360
- else if (fn.field === "toLowerCase")
361
- ty = { kind: "string" };
362
- else if (fn.field === "toUpperCase")
363
- ty = { kind: "string" };
364
- else if (fn.field === "includes")
365
- ty = { kind: "bool" };
366
- }
390
+ const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
391
+ const ty = inferMethodReturnTy(fn, args, ctx);
367
392
  return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
368
393
  }
369
394
  case "index": {
@@ -391,7 +416,17 @@ function resolveExpr(e, ctx) {
391
416
  if (decl?.kind === "record") {
392
417
  const f = decl.fields?.find(f => f.name === e.field);
393
418
  if (f)
394
- ty = resolveTsType(f.tsType, ctx.overrides);
419
+ ty = f.type;
420
+ }
421
+ // Also resolve fields from discriminated-union variants
422
+ if (ty.kind === "unknown" && decl?.kind === "discriminated-union" && decl.variants) {
423
+ for (const variant of decl.variants) {
424
+ const f = variant.fields.find(f => f.name === e.field);
425
+ if (f) {
426
+ ty = f.type;
427
+ break;
428
+ }
429
+ }
395
430
  }
396
431
  }
397
432
  return { kind: "field", obj, field: e.field, ty, isDiscriminant };
@@ -408,15 +443,11 @@ function resolveExpr(e, ctx) {
408
443
  let value = resolveExpr(f.value, fieldCtx);
409
444
  const fieldDecl = decl?.fields?.find(df => df.name === f.name);
410
445
  if (fieldDecl) {
411
- const declTy = parseTsType(fieldDecl.tsType);
446
+ const declTy = fieldDecl.type;
412
447
  value = coerceStr(value, declTy);
413
448
  // Coerce non-optional to optional: wrap in Some (only when value type is concrete)
414
449
  if (declTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "void" && value.ty.kind !== "unknown") {
415
- value = {
416
- kind: "call",
417
- fn: { kind: "var", name: "Some", ty: declTy },
418
- args: [value], ty: declTy, callKind: "pure",
419
- };
450
+ value = wrapSome(value, declTy);
420
451
  }
421
452
  }
422
453
  return { name: f.name, value };
@@ -461,13 +492,11 @@ function resolveExpr(e, ctx) {
461
492
  }
462
493
  case "conditional": {
463
494
  const cond = resolveExpr(e.cond, ctx);
464
- // Optional truthiness: opt ? X : Y
465
- // Narrow the optional to its inner type in the then-branch so that
466
- // field accesses resolve correctly (e.g. entry.decision.field).
467
495
  let narrowedVar;
468
496
  let narrowedExprResolved;
469
497
  let thenCtx = ctx;
470
498
  let rawThen = e.then;
499
+ // Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
471
500
  if (cond.ty.kind === "optional") {
472
501
  const innerTy = cond.ty.inner;
473
502
  if (e.cond.kind === "var") {
@@ -480,24 +509,18 @@ function resolveExpr(e, ctx) {
480
509
  thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
481
510
  }
482
511
  }
483
- // Explicit optional check: x !== undefined ? expr(x) : undefined
484
- // Narrow x to its inner type in the then-branch.
512
+ // Phase 2: Explicit check v !== undefined (simple vars, field chains,
513
+ // complex expressions all handled uniformly by detectOptionalCheck)
485
514
  if (!narrowedVar) {
486
- const narrowed = narrowOptional(e.cond, ctx.env);
515
+ const narrowed = detectOptionalCheck(e.cond, ctx)
516
+ // Phase 3: && with optional check — (v !== undefined && ...) ? ... : ...
517
+ ?? (e.cond.kind === "binop" && e.cond.op === "&&" ? detectOptionalCheck(e.cond.left, ctx) : null);
487
518
  if (narrowed && narrowed.inThen) {
488
519
  narrowedVar = narrowed.varName;
489
520
  thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
490
- }
491
- // Handle complex optional expressions: f() !== undefined ? f().field : undefined
492
- if (!narrowedVar && e.cond.kind === "binop" && e.cond.op === "!==" &&
493
- e.cond.right.kind === "var" && e.cond.right.name === "undefined") {
494
- const optExpr = e.cond.left;
495
- const resolvedOpt = resolveExpr(optExpr, ctx);
496
- if (resolvedOpt.ty.kind === "optional") {
497
- narrowedVar = `_opt${_synVarCounter++}`;
498
- narrowedExprResolved = resolvedOpt;
499
- rawThen = substituteRawExpr(e.then, optExpr, { kind: "var", name: narrowedVar });
500
- thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, resolvedOpt.ty.inner));
521
+ if (narrowed.fieldExpr) {
522
+ narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
523
+ rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
501
524
  }
502
525
  }
503
526
  }
@@ -506,8 +529,16 @@ function resolveExpr(e, ctx) {
506
529
  then_ = coerceStr(then_, else_.ty);
507
530
  else_ = coerceStr(else_, then_.ty);
508
531
  let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
509
- // When narrowedExpr is set, the transform will emit a match producing Optional
510
- if (narrowedExprResolved && ty.kind !== "optional") {
532
+ // When one branch is undefined, result is optional
533
+ if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
534
+ ty = { kind: "optional", inner: else_.ty };
535
+ }
536
+ else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
537
+ ty = { kind: "optional", inner: then_.ty };
538
+ }
539
+ // 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") {
511
542
  ty = { kind: "optional", inner: ty };
512
543
  }
513
544
  return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
@@ -547,6 +578,15 @@ function resolveBlock(stmts, ctx) {
547
578
  const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
548
579
  result.push(typed);
549
580
  env = nextEnv;
581
+ // Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
582
+ // Field chains are excluded — resolve can't substitute in statement lists;
583
+ // transform's emitOptionalMatch handles field chains in statement contexts.
584
+ 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);
588
+ }
589
+ }
550
590
  }
551
591
  return result;
552
592
  }
@@ -569,11 +609,7 @@ function resolveStmt(s, ctx) {
569
609
  // Skip if already optional, void, or undefined (which maps to None)
570
610
  const isUndef = value.kind === "var" && value.name === "undefined";
571
611
  if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
572
- value = {
573
- kind: "call",
574
- fn: { kind: "var", name: "Some", ty: ctx.returnTy },
575
- args: [value], ty: ctx.returnTy, callKind: "pure",
576
- };
612
+ value = wrapSome(value, ctx.returnTy);
577
613
  }
578
614
  return [{ kind: "return", value }, ctx.env];
579
615
  }
@@ -584,23 +620,20 @@ function resolveStmt(s, ctx) {
584
620
  case "expr":
585
621
  return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
586
622
  case "if": {
587
- // Narrow optional<T> → T when checking !== undefined or undefined !==
623
+ // Narrow optional<T> → T when checking !== undefined or undefined !==.
624
+ // Also checks left side of && conditions: if (x !== undefined && ...) { ... }
625
+ // Field chains are excluded — resolve can't substitute in statement bodies;
626
+ // transform's emitOptionalMatch handles field chains in statement contexts.
588
627
  let thenCtx = ctx, elseCtx = ctx;
589
- const narrowed = narrowOptional(s.cond, ctx.env);
590
- if (narrowed) {
628
+ const narrowed = detectOptionalCheck(s.cond, ctx)
629
+ ?? (s.cond.kind === "binop" && s.cond.op === "&&" ? detectOptionalCheck(s.cond.left, ctx) : null);
630
+ if (narrowed && !narrowed.fieldExpr) {
591
631
  const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
592
632
  if (narrowed.inThen)
593
633
  thenCtx = withEnv(ctx, env);
594
634
  else
595
635
  elseCtx = withEnv(ctx, env);
596
636
  }
597
- // Also narrow from left side of && condition: if (x !== undefined && ...) { ... }
598
- if (!narrowed && s.cond.kind === "binop" && s.cond.op === "&&") {
599
- const leftNarrowed = narrowOptional(s.cond.left, ctx.env);
600
- if (leftNarrowed && leftNarrowed.inThen) {
601
- thenCtx = withEnv(ctx, extend(ctx.env, leftNarrowed.varName, leftNarrowed.innerTy));
602
- }
603
- }
604
637
  return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
605
638
  }
606
639
  case "while": {
@@ -847,21 +880,28 @@ function containsReturn(stmts) {
847
880
  return false;
848
881
  }
849
882
  // ── Resolve function / module ────────────────────────────────
850
- function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
883
+ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
851
884
  const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
852
885
  const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
853
886
  const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
854
887
  let env = null;
888
+ if (opts?.thisBinding)
889
+ env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
855
890
  for (const p of params)
856
891
  env = extend(env, p.name, p.ty);
857
892
  const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
858
893
  const requiresCtx = { ...baseCtx, inSpec: true };
859
894
  const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
895
+ // Apply type parameter constraints from //@ type T (==) annotations
896
+ const typeParams = fn.typeParams.map(tp => {
897
+ const constraint = overrides.get(tp);
898
+ return constraint ? `${tp}${constraint}` : tp;
899
+ });
860
900
  return {
861
- name: fn.name, typeParams: fn.typeParams, params, returnTy,
901
+ name: fn.name, typeParams, params, returnTy,
862
902
  requires: resolveSpecs(fn.requires, requiresCtx),
863
903
  ensures: resolveSpecs(fn.ensures, ensuresCtx),
864
- isPure: pureFns.has(fn.name),
904
+ isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
865
905
  body: resolveBlock(fn.body, baseCtx),
866
906
  };
867
907
  }
@@ -869,31 +909,31 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
869
909
  const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
870
910
  // Create a synthetic record type for 'this' so field access resolves
871
911
  const thisType = { kind: "user", name: cls.name };
872
- const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType })) };
912
+ const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
873
913
  const allTypeDecls = [...typeDecls, thisDecl];
874
- const methods = cls.methods.map(fn => {
875
- // Add 'this' to the environment
876
- const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
877
- const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
878
- const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
879
- let env = null;
880
- env = extend(env, "this", thisType);
881
- for (const p of params)
882
- env = extend(env, p.name, p.ty);
883
- const baseCtx = { env, typeDecls: allTypeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
884
- const requiresCtx = { ...baseCtx, inSpec: true };
885
- const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
886
- return {
887
- name: fn.name, typeParams: fn.typeParams, params, returnTy,
888
- requires: resolveSpecs(fn.requires, requiresCtx),
889
- ensures: resolveSpecs(fn.ensures, ensuresCtx),
890
- isPure: false, // class methods are never pure (they access this)
891
- body: resolveBlock(fn.body, baseCtx),
892
- };
893
- });
914
+ const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
915
+ thisBinding: { name: "this", ty: thisType },
916
+ forcePure: false, // class methods are never pure (they access this)
917
+ }));
894
918
  return { name: cls.name, fields, methods };
895
919
  }
920
+ /** Pre-compute Ty on all TypeDeclInfo fields/variants/aliases.
921
+ * Called once per module so consumers can read field.type instead of re-parsing tsType. */
922
+ function precomputeFieldTypes(typeDecls) {
923
+ for (const d of typeDecls) {
924
+ if (d.fields)
925
+ for (const f of d.fields)
926
+ f.type = parseTsType(f.tsType);
927
+ if (d.variants)
928
+ for (const v of d.variants)
929
+ for (const f of v.fields)
930
+ f.type = parseTsType(f.tsType);
931
+ if (d.aliasOf && !d.aliasOfTy)
932
+ d.aliasOfTy = parseTsType(d.aliasOf);
933
+ }
934
+ }
896
935
  export function resolveModule(raw) {
936
+ precomputeFieldTypes(raw.typeDecls);
897
937
  const pureFns = computePureFns(raw.functions);
898
938
  // Pre-compute function parameter types for optional coercion
899
939
  const fnParams = new Map();