lemmascript 0.2.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.
@@ -6,6 +6,72 @@
6
6
  */
7
7
  import { parseTsType } from "./types.js";
8
8
  import { parseExpr } from "./specparser.js";
9
+ // ── Raw expression substitution ─────────────────────────────
10
+ let _synVarCounter = 0;
11
+ /**
12
+ * Structural equality for raw field-access chains (var and field nodes only).
13
+ * Exact within a single expression scope — raw IR has no bindings that could
14
+ * cause name collisions (those are introduced by resolve, which runs after).
15
+ */
16
+ function rawExprEquals(a, b) {
17
+ if (a.kind === "var" && b.kind === "var")
18
+ return a.name === b.name;
19
+ if (a.kind === "field" && b.kind === "field")
20
+ return a.field === b.field && rawExprEquals(a.obj, b.obj);
21
+ if (a.kind === "call" && b.kind === "call")
22
+ return rawExprEquals(a.fn, b.fn) && a.args.length === b.args.length && a.args.every((arg, i) => rawExprEquals(arg, b.args[i]));
23
+ if (a.kind === "index" && b.kind === "index")
24
+ return rawExprEquals(a.obj, b.obj) && rawExprEquals(a.idx, b.idx);
25
+ return false;
26
+ }
27
+ /** Return the root variable name of a field-access chain, or null. */
28
+ function rawChainRoot(e) {
29
+ if (e.kind === "var")
30
+ return e.name;
31
+ if (e.kind === "field")
32
+ return rawChainRoot(e.obj);
33
+ return null;
34
+ }
35
+ /**
36
+ * Replace all occurrences of `target` in `expr` with `replacement`.
37
+ * Only matches field-access chains (see rawExprEquals). Stops at lambda
38
+ * boundaries that shadow the chain's root variable.
39
+ */
40
+ function substituteRawExpr(expr, target, replacement) {
41
+ if (rawExprEquals(expr, target))
42
+ return replacement;
43
+ const root = rawChainRoot(target);
44
+ const sub = (e) => substituteRawExpr(e, target, replacement);
45
+ switch (expr.kind) {
46
+ case "var":
47
+ case "num":
48
+ case "str":
49
+ case "bool":
50
+ case "result":
51
+ case "havoc":
52
+ case "emptyCollection":
53
+ return expr;
54
+ case "binop": return { ...expr, left: sub(expr.left), right: sub(expr.right) };
55
+ case "unop": return { ...expr, expr: sub(expr.expr) };
56
+ case "call": return { ...expr, fn: sub(expr.fn), args: expr.args.map(sub) };
57
+ case "field": return { ...expr, obj: sub(expr.obj) };
58
+ case "index": return { ...expr, obj: sub(expr.obj), idx: sub(expr.idx) };
59
+ case "record":
60
+ return { ...expr, spread: expr.spread ? sub(expr.spread) : null,
61
+ fields: expr.fields.map(f => ({ ...f, value: sub(f.value) })) };
62
+ case "arrayLiteral": return { ...expr, elems: expr.elems.map(sub) };
63
+ case "conditional": return { ...expr, cond: sub(expr.cond), then: sub(expr.then), else: sub(expr.else) };
64
+ case "nonNull": return { ...expr, expr: sub(expr.expr) };
65
+ case "forall":
66
+ case "exists":
67
+ return { ...expr, body: sub(expr.body) };
68
+ case "lambda":
69
+ // Don't cross lambda boundaries that shadow the chain's root variable
70
+ if (root && expr.params.some(p => p.name === root))
71
+ return expr;
72
+ return { ...expr, body: Array.isArray(expr.body) ? expr.body : sub(expr.body) };
73
+ }
74
+ }
9
75
  function lookup(env, name) {
10
76
  if (!env)
11
77
  return undefined;
@@ -33,22 +99,47 @@ function coerceStr(expr, targetTy) {
33
99
  return expr;
34
100
  }
35
101
  // ── Helpers ──────────────────────────────────────────────────
36
- /** Detect `v !== undefined` or `undefined !== v` where v: optional<T>. */
37
- 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) {
38
116
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
39
117
  return null;
40
- // v !== undefined OR undefined !== v
41
- let varName = null;
42
- if (cond.left.kind === "var" && cond.right.kind === "var" && cond.right.name === "undefined")
43
- varName = cond.left.name;
44
- if (cond.right.kind === "var" && cond.left.kind === "var" && cond.left.name === "undefined")
45
- varName = cond.right.name;
46
- if (!varName)
47
- return null;
48
- const ty = lookup(env, varName);
49
- if (!ty || ty.kind !== "optional")
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)
50
125
  return null;
51
- 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;
52
143
  }
53
144
  /** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
54
145
  function isRefMutableInTS(ty) {
@@ -137,10 +228,99 @@ function classifyCall(fn, ctx) {
137
228
  return "method";
138
229
  return "unknown";
139
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
+ }
140
318
  // ── Resolve expressions ──────────────────────────────────────
141
319
  function resolveExpr(e, ctx) {
142
320
  switch (e.kind) {
143
321
  case "var":
322
+ if (e.name === "undefined")
323
+ return { kind: "var", name: "undefined", ty: { kind: "void" } };
144
324
  return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
145
325
  case "num":
146
326
  if (!Number.isInteger(e.value))
@@ -162,18 +342,31 @@ function resolveExpr(e, ctx) {
162
342
  }
163
343
  case "binop": {
164
344
  let left = resolveExpr(e.left, ctx);
165
- let right = resolveExpr(e.right, ctx);
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.
348
+ let rightCtx = ctx;
349
+ let rawRight = e.right;
350
+ if (e.op === "&&") {
351
+ const narrowed = detectOptionalCheck(e.left, ctx);
352
+ if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
353
+ rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
354
+ }
355
+ }
356
+ let right = resolveExpr(rawRight, rightCtx);
166
357
  if (e.op === "===" || e.op === "!==") {
167
358
  left = coerceStr(left, right.ty);
168
359
  right = coerceStr(right, left.ty);
169
360
  }
170
361
  let ty = { kind: "unknown" };
171
- if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op))
362
+ if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
172
363
  ty = { kind: "bool" };
173
364
  else if (e.op === "&&")
174
365
  ty = right.ty;
175
- else if (e.op === "||" && left.ty.kind === "optional")
176
- ty = left.ty.inner;
366
+ else if (e.op === "||" && left.ty.kind === "optional") {
367
+ // || undefined is identity for optionals — keep the optional type
368
+ ty = (e.right.kind === "var" && e.right.name === "undefined") ? left.ty : left.ty.inner;
369
+ }
177
370
  else if (e.op === "||")
178
371
  ty = right.ty;
179
372
  else if (["+", "-", "*", "/", "%"].includes(e.op)) {
@@ -187,65 +380,24 @@ function resolveExpr(e, ctx) {
187
380
  }
188
381
  case "call": {
189
382
  const fn = resolveExpr(e.fn, ctx);
190
- let args = e.args.map(a => resolveExpr(a, ctx));
191
- // Coerce non-optional args to Option when callee expects optional param: wrap in Some
192
- if (fn.kind === "var" && ctx.fnParams.has(fn.name)) {
193
- const paramTys = ctx.fnParams.get(fn.name);
194
- args = args.map((a, i) => {
195
- if (i < paramTys.length && a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
196
- return {
197
- kind: "call",
198
- fn: { kind: "var", name: "Some", ty: paramTys[i] },
199
- args: [a],
200
- ty: paramTys[i],
201
- callKind: "pure",
202
- };
203
- }
204
- return a;
205
- });
206
- }
207
- let ty = { kind: "unknown" };
208
- // Infer return types for collection methods
209
- if (fn.kind === "field" && fn.obj.ty.kind === "map") {
210
- if (fn.field === "get")
211
- ty = ctx.inSpec ? fn.obj.ty.value : { kind: "optional", inner: fn.obj.ty.value };
212
- else if (fn.field === "has")
213
- ty = { kind: "bool" };
214
- else if (fn.field === "set")
215
- ty = fn.obj.ty;
216
- }
217
- else if (fn.kind === "field" && fn.obj.ty.kind === "set") {
218
- if (fn.field === "has")
219
- ty = { kind: "bool" };
220
- else if (fn.field === "add")
221
- ty = fn.obj.ty;
222
- else if (fn.field === "delete")
223
- ty = fn.obj.ty;
224
- }
225
- else if (fn.kind === "field" && fn.obj.ty.kind === "array") {
226
- if (fn.field === "includes")
227
- ty = { kind: "bool" };
228
- else if (fn.field === "shift")
229
- ty = fn.obj.ty.elem;
230
- else if (fn.field === "push")
231
- ty = fn.obj.ty;
232
- }
233
- else if (fn.kind === "field" && fn.obj.ty.kind === "string") {
234
- if (fn.field === "trim")
235
- ty = { kind: "string" };
236
- else if (fn.field === "toLowerCase")
237
- ty = { kind: "string" };
238
- else if (fn.field === "toUpperCase")
239
- ty = { kind: "string" };
240
- else if (fn.field === "includes")
241
- ty = { kind: "bool" };
383
+ const rawArgs = inferLambdaParamTypes(fn, e.args);
384
+ // For .push() on a typed array, resolve args with element type context
385
+ let argCtx = ctx;
386
+ if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
387
+ fn.obj.ty.elem.kind === "user") {
388
+ argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
242
389
  }
390
+ const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
391
+ const ty = inferMethodReturnTy(fn, args, ctx);
243
392
  return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
244
393
  }
245
394
  case "index": {
246
395
  const obj = resolveExpr(e.obj, ctx);
247
396
  const idx = resolveExpr(e.idx, ctx);
248
- return { kind: "index", obj, idx, ty: obj.ty.kind === "array" ? obj.ty.elem : { kind: "unknown" } };
397
+ const idxTy = obj.ty.kind === "array" ? obj.ty.elem
398
+ : obj.ty.kind === "map" ? obj.ty.value
399
+ : { kind: "unknown" };
400
+ return { kind: "index", obj, idx, ty: idxTy };
249
401
  }
250
402
  case "field": {
251
403
  const obj = resolveExpr(e.obj, ctx);
@@ -264,7 +416,17 @@ function resolveExpr(e, ctx) {
264
416
  if (decl?.kind === "record") {
265
417
  const f = decl.fields?.find(f => f.name === e.field);
266
418
  if (f)
267
- 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
+ }
268
430
  }
269
431
  }
270
432
  return { kind: "field", obj, field: e.field, ty, isDiscriminant };
@@ -275,11 +437,19 @@ function resolveExpr(e, ctx) {
275
437
  // Infer record type: from spread, or from return type context
276
438
  const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
277
439
  const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
440
+ // Clear returnTy for field values — it applies to THIS record, not nested ones
441
+ const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
278
442
  const fields = e.fields.map(f => {
279
- let value = resolveExpr(f.value, ctx);
443
+ let value = resolveExpr(f.value, fieldCtx);
280
444
  const fieldDecl = decl?.fields?.find(df => df.name === f.name);
281
- if (fieldDecl)
282
- value = coerceStr(value, parseTsType(fieldDecl.tsType));
445
+ if (fieldDecl) {
446
+ const declTy = fieldDecl.type;
447
+ value = coerceStr(value, declTy);
448
+ // Coerce non-optional to optional: wrap in Some (only when value type is concrete)
449
+ if (declTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "void" && value.ty.kind !== "unknown") {
450
+ value = wrapSome(value, declTy);
451
+ }
452
+ }
283
453
  return { name: f.name, value };
284
454
  });
285
455
  return { kind: "record", spread, fields, ty: recordTy ?? ty };
@@ -289,12 +459,12 @@ function resolveExpr(e, ctx) {
289
459
  throw new Error("\\result is only valid in ensures");
290
460
  return { kind: "result", ty: ctx.returnTy };
291
461
  case "forall": {
292
- const varTy = e.varType === "nat" ? { kind: "nat" }
462
+ const varTy = e.varType !== "int" ? parseTsType(e.varType)
293
463
  : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
294
464
  return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
295
465
  }
296
466
  case "exists": {
297
- const varTy = e.varType === "nat" ? { kind: "nat" }
467
+ const varTy = e.varType !== "int" ? parseTsType(e.varType)
298
468
  : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
299
469
  return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
300
470
  }
@@ -322,16 +492,61 @@ function resolveExpr(e, ctx) {
322
492
  }
323
493
  case "conditional": {
324
494
  const cond = resolveExpr(e.cond, ctx);
325
- let then_ = resolveExpr(e.then, ctx);
495
+ let narrowedVar;
496
+ let narrowedExprResolved;
497
+ let thenCtx = ctx;
498
+ let rawThen = e.then;
499
+ // Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
500
+ if (cond.ty.kind === "optional") {
501
+ const innerTy = cond.ty.inner;
502
+ if (e.cond.kind === "var") {
503
+ narrowedVar = e.cond.name;
504
+ thenCtx = withEnv(ctx, extend(ctx.env, e.cond.name, innerTy));
505
+ }
506
+ else {
507
+ narrowedVar = `_opt${_synVarCounter++}`;
508
+ rawThen = substituteRawExpr(e.then, e.cond, { kind: "var", name: narrowedVar });
509
+ thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
510
+ }
511
+ }
512
+ // Phase 2: Explicit check — v !== undefined (simple vars, field chains,
513
+ // complex expressions all handled uniformly by detectOptionalCheck)
514
+ if (!narrowedVar) {
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);
518
+ if (narrowed && narrowed.inThen) {
519
+ narrowedVar = narrowed.varName;
520
+ thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
521
+ if (narrowed.fieldExpr) {
522
+ narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
523
+ rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
524
+ }
525
+ }
526
+ }
527
+ let then_ = resolveExpr(rawThen, thenCtx);
326
528
  let else_ = resolveExpr(e.else, ctx);
327
529
  then_ = coerceStr(then_, else_.ty);
328
530
  else_ = coerceStr(else_, then_.ty);
329
- const ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
330
- return { kind: "conditional", cond, then: then_, else: else_, ty };
531
+ let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
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") {
542
+ ty = { kind: "optional", inner: ty };
543
+ }
544
+ return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
331
545
  }
332
546
  case "emptyCollection": {
333
547
  const ty = parseTsType(e.tsType);
334
- return { kind: "arrayLiteral", elems: [], ty };
548
+ const elems = e.initElems ? e.initElems.map(el => resolveExpr(el, ctx)) : [];
549
+ return { kind: "arrayLiteral", elems, ty };
335
550
  }
336
551
  case "havoc":
337
552
  return { kind: "havoc", ty: resolveTsType(e.tsType, ctx.overrides) };
@@ -363,6 +578,15 @@ function resolveBlock(stmts, ctx) {
363
578
  const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
364
579
  result.push(typed);
365
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
+ }
366
590
  }
367
591
  return result;
368
592
  }
@@ -379,8 +603,16 @@ function resolveStmt(s, ctx) {
379
603
  const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
380
604
  return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
381
605
  }
382
- case "return":
383
- return [{ kind: "return", value: coerceStr(resolveExpr(s.value, ctx), ctx.returnTy) }, ctx.env];
606
+ case "return": {
607
+ let value = coerceStr(resolveExpr(s.value, ctx), ctx.returnTy);
608
+ // Wrap non-optional return value in Some when function returns optional
609
+ // Skip if already optional, void, or undefined (which maps to None)
610
+ const isUndef = value.kind === "var" && value.name === "undefined";
611
+ if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
612
+ value = wrapSome(value, ctx.returnTy);
613
+ }
614
+ return [{ kind: "return", value }, ctx.env];
615
+ }
384
616
  case "break":
385
617
  return [{ kind: "break" }, ctx.env];
386
618
  case "continue":
@@ -388,10 +620,14 @@ function resolveStmt(s, ctx) {
388
620
  case "expr":
389
621
  return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
390
622
  case "if": {
391
- // 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.
392
627
  let thenCtx = ctx, elseCtx = ctx;
393
- const narrowed = narrowOptional(s.cond, ctx.env);
394
- 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) {
395
631
  const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
396
632
  if (narrowed.inThen)
397
633
  thenCtx = withEnv(ctx, env);
@@ -483,7 +719,7 @@ function isSyntacticallyPure(stmts) {
483
719
  case "while":
484
720
  case "forof": return false;
485
721
  case "let":
486
- if (s.mutable)
722
+ if (s.mutable || s.init.kind === "havoc")
487
723
  return false;
488
724
  break;
489
725
  case "if":
@@ -644,24 +880,28 @@ function containsReturn(stmts) {
644
880
  return false;
645
881
  }
646
882
  // ── Resolve function / module ────────────────────────────────
647
- function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
648
- if (hasReturnInLoop(fn.body)) {
649
- throw new Error(`${fn.name}: return inside a loop is not supported.`);
650
- }
883
+ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
651
884
  const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
652
885
  const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
653
886
  const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
654
887
  let env = null;
888
+ if (opts?.thisBinding)
889
+ env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
655
890
  for (const p of params)
656
891
  env = extend(env, p.name, p.ty);
657
892
  const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
658
893
  const requiresCtx = { ...baseCtx, inSpec: true };
659
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
+ });
660
900
  return {
661
- name: fn.name, params, returnTy,
901
+ name: fn.name, typeParams, params, returnTy,
662
902
  requires: resolveSpecs(fn.requires, requiresCtx),
663
903
  ensures: resolveSpecs(fn.ensures, ensuresCtx),
664
- isPure: pureFns.has(fn.name),
904
+ isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
665
905
  body: resolveBlock(fn.body, baseCtx),
666
906
  };
667
907
  }
@@ -669,31 +909,31 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
669
909
  const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
670
910
  // Create a synthetic record type for 'this' so field access resolves
671
911
  const thisType = { kind: "user", name: cls.name };
672
- 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) })) };
673
913
  const allTypeDecls = [...typeDecls, thisDecl];
674
- const methods = cls.methods.map(fn => {
675
- // Add 'this' to the environment
676
- const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
677
- const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
678
- const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
679
- let env = null;
680
- env = extend(env, "this", thisType);
681
- for (const p of params)
682
- env = extend(env, p.name, p.ty);
683
- const baseCtx = { env, typeDecls: allTypeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
684
- const requiresCtx = { ...baseCtx, inSpec: true };
685
- const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
686
- return {
687
- name: fn.name, params, returnTy,
688
- requires: resolveSpecs(fn.requires, requiresCtx),
689
- ensures: resolveSpecs(fn.ensures, ensuresCtx),
690
- isPure: false, // class methods are never pure (they access this)
691
- body: resolveBlock(fn.body, baseCtx),
692
- };
693
- });
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
+ }));
694
918
  return { name: cls.name, fields, methods };
695
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
+ }
696
935
  export function resolveModule(raw) {
936
+ precomputeFieldTypes(raw.typeDecls);
697
937
  const pureFns = computePureFns(raw.functions);
698
938
  // Pre-compute function parameter types for optional coercion
699
939
  const fnParams = new Map();
@@ -129,6 +129,11 @@ class Parser {
129
129
  parseCmp() {
130
130
  const left = this.parseAdd();
131
131
  const t = this.peek();
132
+ // 'in' as infix membership operator (set/seq/map): x in S
133
+ if (t?.type === "ident" && t.value === "in") {
134
+ this.advance();
135
+ return { kind: "binop", op: "in", left, right: this.parseAdd() };
136
+ }
132
137
  if (t?.type === "op" && ["===", "!==", "==", "!=", ">=", "<=", ">", "<"].includes(t.value)) {
133
138
  this.advance();
134
139
  // Normalize == to ===, != to !== so downstream sees one spelling
@@ -252,8 +257,6 @@ class Parser {
252
257
  let varType = "int";
253
258
  if (this.match("punc", ":")) {
254
259
  const ty = this.expect("ident").value;
255
- if (ty !== "nat" && ty !== "int")
256
- throw new Error(`Unknown type '${ty}'`);
257
260
  varType = ty;
258
261
  }
259
262
  this.expect("punc", ",");