lemmascript 0.3.0 → 0.3.2

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,101 @@ 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 === "indexOf")
297
+ return { kind: "int" };
298
+ if (fn.field === "shift")
299
+ return objTy.elem;
300
+ if (fn.field === "push" || fn.field === "concat")
301
+ return objTy;
302
+ if (fn.field === "filter")
303
+ return objTy;
304
+ if (fn.field === "every" || fn.field === "some")
305
+ return { kind: "bool" };
306
+ if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
307
+ const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
308
+ ? args[0].body[0].value.ty : { kind: "unknown" };
309
+ return { kind: "array", elem: retTy };
310
+ }
311
+ }
312
+ else if (objTy.kind === "string") {
313
+ if (fn.field === "trim" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
314
+ return { kind: "string" };
315
+ if (fn.field === "includes")
316
+ return { kind: "bool" };
317
+ }
318
+ return { kind: "unknown" };
319
+ }
206
320
  // ── Resolve expressions ──────────────────────────────────────
207
321
  function resolveExpr(e, ctx) {
208
322
  switch (e.kind) {
209
323
  case "var":
324
+ if (e.name === "undefined")
325
+ return { kind: "var", name: "undefined", ty: { kind: "void" } };
210
326
  return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
211
327
  case "num":
212
328
  if (!Number.isInteger(e.value))
@@ -228,15 +344,18 @@ function resolveExpr(e, ctx) {
228
344
  }
229
345
  case "binop": {
230
346
  let left = resolveExpr(e.left, ctx);
231
- // && narrowing: if left is "x !== undefined", narrow x for right side
347
+ // && narrowing: if left is "x !== undefined", narrow x for right side.
348
+ // Field chains are excluded — resolve can't substitute in sub-expressions;
349
+ // transform's emitOptionalMatch handles field chains in statement contexts.
232
350
  let rightCtx = ctx;
351
+ let rawRight = e.right;
233
352
  if (e.op === "&&") {
234
- const narrowed = narrowOptional(e.left, ctx.env);
235
- if (narrowed && narrowed.inThen) {
353
+ const narrowed = detectOptionalCheck(e.left, ctx);
354
+ if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
236
355
  rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
237
356
  }
238
357
  }
239
- let right = resolveExpr(e.right, rightCtx);
358
+ let right = resolveExpr(rawRight, rightCtx);
240
359
  if (e.op === "===" || e.op === "!==") {
241
360
  left = coerceStr(left, right.ty);
242
361
  right = coerceStr(right, left.ty);
@@ -263,114 +382,22 @@ function resolveExpr(e, ctx) {
263
382
  }
264
383
  case "call": {
265
384
  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
385
+ const rawArgs = inferLambdaParamTypes(fn, e.args);
386
+ // For .push() on a typed array, resolve args with element type context
285
387
  let argCtx = ctx;
286
388
  if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
287
389
  fn.obj.ty.elem.kind === "user") {
288
390
  argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
289
391
  }
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
- }
392
+ const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
393
+ const ty = inferMethodReturnTy(fn, args, ctx);
367
394
  return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
368
395
  }
369
396
  case "index": {
370
397
  const obj = resolveExpr(e.obj, ctx);
371
398
  const idx = resolveExpr(e.idx, ctx);
372
399
  const idxTy = obj.ty.kind === "array" ? obj.ty.elem
373
- : obj.ty.kind === "map" ? obj.ty.value
400
+ : obj.ty.kind === "map" ? { kind: "optional", inner: obj.ty.value }
374
401
  : { kind: "unknown" };
375
402
  return { kind: "index", obj, idx, ty: idxTy };
376
403
  }
@@ -391,7 +418,17 @@ function resolveExpr(e, ctx) {
391
418
  if (decl?.kind === "record") {
392
419
  const f = decl.fields?.find(f => f.name === e.field);
393
420
  if (f)
394
- ty = resolveTsType(f.tsType, ctx.overrides);
421
+ ty = f.type;
422
+ }
423
+ // Also resolve fields from discriminated-union variants
424
+ if (ty.kind === "unknown" && decl?.kind === "discriminated-union" && decl.variants) {
425
+ for (const variant of decl.variants) {
426
+ const f = variant.fields.find(f => f.name === e.field);
427
+ if (f) {
428
+ ty = f.type;
429
+ break;
430
+ }
431
+ }
395
432
  }
396
433
  }
397
434
  return { kind: "field", obj, field: e.field, ty, isDiscriminant };
@@ -408,15 +445,15 @@ function resolveExpr(e, ctx) {
408
445
  let value = resolveExpr(f.value, fieldCtx);
409
446
  const fieldDecl = decl?.fields?.find(df => df.name === f.name);
410
447
  if (fieldDecl) {
411
- const declTy = parseTsType(fieldDecl.tsType);
448
+ const declTy = fieldDecl.type;
412
449
  value = coerceStr(value, declTy);
450
+ // Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
451
+ if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
452
+ value = { kind: "arrayLiteral", elems: [], ty: declTy };
453
+ }
413
454
  // Coerce non-optional to optional: wrap in Some (only when value type is concrete)
414
455
  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
- };
456
+ value = wrapSome(value, declTy);
420
457
  }
421
458
  }
422
459
  return { name: f.name, value };
@@ -461,13 +498,11 @@ function resolveExpr(e, ctx) {
461
498
  }
462
499
  case "conditional": {
463
500
  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
501
  let narrowedVar;
468
502
  let narrowedExprResolved;
469
503
  let thenCtx = ctx;
470
504
  let rawThen = e.then;
505
+ // Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
471
506
  if (cond.ty.kind === "optional") {
472
507
  const innerTy = cond.ty.inner;
473
508
  if (e.cond.kind === "var") {
@@ -480,24 +515,18 @@ function resolveExpr(e, ctx) {
480
515
  thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
481
516
  }
482
517
  }
483
- // Explicit optional check: x !== undefined ? expr(x) : undefined
484
- // Narrow x to its inner type in the then-branch.
518
+ // Phase 2: Explicit check v !== undefined (simple vars, field chains,
519
+ // complex expressions all handled uniformly by detectOptionalCheck)
485
520
  if (!narrowedVar) {
486
- const narrowed = narrowOptional(e.cond, ctx.env);
521
+ const narrowed = detectOptionalCheck(e.cond, ctx)
522
+ // Phase 3: && with optional check — (v !== undefined && ...) ? ... : ...
523
+ ?? (e.cond.kind === "binop" && e.cond.op === "&&" ? detectOptionalCheck(e.cond.left, ctx) : null);
487
524
  if (narrowed && narrowed.inThen) {
488
525
  narrowedVar = narrowed.varName;
489
526
  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));
527
+ if (narrowed.fieldExpr) {
528
+ narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
529
+ rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
501
530
  }
502
531
  }
503
532
  }
@@ -506,8 +535,16 @@ function resolveExpr(e, ctx) {
506
535
  then_ = coerceStr(then_, else_.ty);
507
536
  else_ = coerceStr(else_, then_.ty);
508
537
  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") {
538
+ // When one branch is undefined, result is optional
539
+ if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
540
+ ty = { kind: "optional", inner: else_.ty };
541
+ }
542
+ else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
543
+ ty = { kind: "optional", inner: then_.ty };
544
+ }
545
+ // When narrowedExpr is set AND a branch is void, the match produces Optional
546
+ const hasVoidBranch = then_.ty.kind === "void" || else_.ty.kind === "void";
547
+ if (narrowedExprResolved && hasVoidBranch && ty.kind !== "optional") {
511
548
  ty = { kind: "optional", inner: ty };
512
549
  }
513
550
  return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
@@ -547,14 +584,25 @@ function resolveBlock(stmts, ctx) {
547
584
  const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
548
585
  result.push(typed);
549
586
  env = nextEnv;
587
+ // Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
588
+ // Field chains are excluded — resolve can't substitute in statement lists;
589
+ // transform's emitOptionalMatch handles field chains in statement contexts.
590
+ if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) {
591
+ const narrowed = detectOptionalCheck(s.cond, withEnv(ctx, env));
592
+ if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
593
+ env = extend(env, narrowed.varName, narrowed.innerTy);
594
+ }
595
+ }
550
596
  }
551
597
  return result;
552
598
  }
553
599
  function resolveStmt(s, ctx) {
554
600
  switch (s.kind) {
555
601
  case "let": {
556
- const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
557
- const init = coerceStr(resolveExpr(s.init, ctx), ty);
602
+ const declTy = resolveTsType(s.tsType, ctx.overrides, s.name);
603
+ const init = coerceStr(resolveExpr(s.init, ctx), declTy);
604
+ // Map indexing: TS says T, but access can fail → use Optional<T> from init
605
+ const ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
558
606
  // const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
559
607
  const mutable = s.mutable || isRefMutableInTS(ty);
560
608
  return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
@@ -569,11 +617,7 @@ function resolveStmt(s, ctx) {
569
617
  // Skip if already optional, void, or undefined (which maps to None)
570
618
  const isUndef = value.kind === "var" && value.name === "undefined";
571
619
  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
- };
620
+ value = wrapSome(value, ctx.returnTy);
577
621
  }
578
622
  return [{ kind: "return", value }, ctx.env];
579
623
  }
@@ -584,23 +628,20 @@ function resolveStmt(s, ctx) {
584
628
  case "expr":
585
629
  return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
586
630
  case "if": {
587
- // Narrow optional<T> → T when checking !== undefined or undefined !==
631
+ // Narrow optional<T> → T when checking !== undefined or undefined !==.
632
+ // Also checks left side of && conditions: if (x !== undefined && ...) { ... }
633
+ // Field chains are excluded — resolve can't substitute in statement bodies;
634
+ // transform's emitOptionalMatch handles field chains in statement contexts.
588
635
  let thenCtx = ctx, elseCtx = ctx;
589
- const narrowed = narrowOptional(s.cond, ctx.env);
590
- if (narrowed) {
636
+ const narrowed = detectOptionalCheck(s.cond, ctx)
637
+ ?? (s.cond.kind === "binop" && s.cond.op === "&&" ? detectOptionalCheck(s.cond.left, ctx) : null);
638
+ if (narrowed && !narrowed.fieldExpr) {
591
639
  const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
592
640
  if (narrowed.inThen)
593
641
  thenCtx = withEnv(ctx, env);
594
642
  else
595
643
  elseCtx = withEnv(ctx, env);
596
644
  }
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
645
  return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
605
646
  }
606
647
  case "while": {
@@ -847,21 +888,28 @@ function containsReturn(stmts) {
847
888
  return false;
848
889
  }
849
890
  // ── Resolve function / module ────────────────────────────────
850
- function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
891
+ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
851
892
  const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
852
893
  const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
853
894
  const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
854
895
  let env = null;
896
+ if (opts?.thisBinding)
897
+ env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
855
898
  for (const p of params)
856
899
  env = extend(env, p.name, p.ty);
857
900
  const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
858
901
  const requiresCtx = { ...baseCtx, inSpec: true };
859
902
  const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
903
+ // Apply type parameter constraints from //@ type T (==) annotations
904
+ const typeParams = fn.typeParams.map(tp => {
905
+ const constraint = overrides.get(tp);
906
+ return constraint ? `${tp}${constraint}` : tp;
907
+ });
860
908
  return {
861
- name: fn.name, typeParams: fn.typeParams, params, returnTy,
909
+ name: fn.name, typeParams, params, returnTy,
862
910
  requires: resolveSpecs(fn.requires, requiresCtx),
863
911
  ensures: resolveSpecs(fn.ensures, ensuresCtx),
864
- isPure: pureFns.has(fn.name),
912
+ isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
865
913
  body: resolveBlock(fn.body, baseCtx),
866
914
  };
867
915
  }
@@ -869,31 +917,31 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
869
917
  const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
870
918
  // Create a synthetic record type for 'this' so field access resolves
871
919
  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 })) };
920
+ const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
873
921
  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
- });
922
+ const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
923
+ thisBinding: { name: "this", ty: thisType },
924
+ forcePure: false, // class methods are never pure (they access this)
925
+ }));
894
926
  return { name: cls.name, fields, methods };
895
927
  }
928
+ /** Pre-compute Ty on all TypeDeclInfo fields/variants/aliases.
929
+ * Called once per module so consumers can read field.type instead of re-parsing tsType. */
930
+ function precomputeFieldTypes(typeDecls) {
931
+ for (const d of typeDecls) {
932
+ if (d.fields)
933
+ for (const f of d.fields)
934
+ f.type = parseTsType(f.tsType);
935
+ if (d.variants)
936
+ for (const v of d.variants)
937
+ for (const f of v.fields)
938
+ f.type = parseTsType(f.tsType);
939
+ if (d.aliasOf && !d.aliasOfTy)
940
+ d.aliasOfTy = parseTsType(d.aliasOf);
941
+ }
942
+ }
896
943
  export function resolveModule(raw) {
944
+ precomputeFieldTypes(raw.typeDecls);
897
945
  const pureFns = computePureFns(raw.functions);
898
946
  // Pre-compute function parameter types for optional coercion
899
947
  const fnParams = new Map();