lemmascript 0.1.0 → 0.3.0

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,12 +99,96 @@ function coerceStr(expr, targetTy) {
33
99
  return expr;
34
100
  }
35
101
  // ── Helpers ──────────────────────────────────────────────────
102
+ /** Detect `v !== undefined` or `undefined !== v` where v: optional<T>. */
103
+ function narrowOptional(cond, env) {
104
+ if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
105
+ 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)
113
+ 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 === "!==" };
118
+ }
119
+ /** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
120
+ function isRefMutableInTS(ty) {
121
+ return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
122
+ }
36
123
  function findDecl(ctx, name) {
37
124
  return ctx.typeDecls.find(d => d.name === name);
38
125
  }
39
126
  function getDiscriminant(ctx, typeName) {
40
127
  return findDecl(ctx, typeName)?.discriminant;
41
128
  }
129
+ /** Infer quantifier variable type from usage in body.
130
+ * If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
131
+ * return the collection's key type. Otherwise return null (default to int). */
132
+ function inferQuantVarType(varName, body, ctx) {
133
+ // Look for calls like map.has(k), map.get(k), or array.includes(k) where k is our variable
134
+ if (body.kind === "call" && body.fn.kind === "field" &&
135
+ (body.fn.field === "has" || body.fn.field === "get" || body.fn.field === "includes") &&
136
+ body.args.length === 1 && body.args[0].kind === "var" && body.args[0].name === varName) {
137
+ const objTy = lookup(ctx.env, body.fn.obj.kind === "var" ? body.fn.obj.name : "");
138
+ if (objTy?.kind === "map")
139
+ return objTy.key;
140
+ if (objTy?.kind === "set")
141
+ return objTy.elem;
142
+ if (objTy?.kind === "array")
143
+ return objTy.elem;
144
+ }
145
+ // Recurse into subexpressions
146
+ if (body.kind === "binop") {
147
+ return inferQuantVarType(varName, body.left, ctx) ?? inferQuantVarType(varName, body.right, ctx);
148
+ }
149
+ if (body.kind === "unop")
150
+ return inferQuantVarType(varName, body.expr, ctx);
151
+ if (body.kind === "call") {
152
+ for (const a of body.args) {
153
+ const r = inferQuantVarType(varName, a, ctx);
154
+ if (r)
155
+ return r;
156
+ }
157
+ return inferQuantVarType(varName, body.fn, ctx);
158
+ }
159
+ if (body.kind === "field")
160
+ return inferQuantVarType(varName, body.obj, ctx);
161
+ if (body.kind === "index") {
162
+ return inferQuantVarType(varName, body.obj, ctx) ?? inferQuantVarType(varName, body.idx, ctx);
163
+ }
164
+ if (body.kind === "conditional") {
165
+ return inferQuantVarType(varName, body.cond, ctx) ??
166
+ inferQuantVarType(varName, body.then, ctx) ?? inferQuantVarType(varName, body.else, ctx);
167
+ }
168
+ if ((body.kind === "forall" || body.kind === "exists") && body.var !== varName) {
169
+ return inferQuantVarType(varName, body.body, ctx);
170
+ }
171
+ if (body.kind === "arrayLiteral") {
172
+ for (const el of body.elems) {
173
+ const r = inferQuantVarType(varName, el, ctx);
174
+ if (r)
175
+ return r;
176
+ }
177
+ }
178
+ if (body.kind === "record") {
179
+ if (body.spread) {
180
+ const r = inferQuantVarType(varName, body.spread, ctx);
181
+ if (r)
182
+ return r;
183
+ }
184
+ for (const f of body.fields) {
185
+ const r = inferQuantVarType(varName, f.value, ctx);
186
+ if (r)
187
+ return r;
188
+ }
189
+ }
190
+ return null;
191
+ }
42
192
  function classifyCall(fn, ctx) {
43
193
  if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Math")
44
194
  return "pure";
@@ -59,35 +209,170 @@ function resolveExpr(e, ctx) {
59
209
  case "var":
60
210
  return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
61
211
  case "num":
212
+ if (!Number.isInteger(e.value))
213
+ return { kind: "num", value: e.value, ty: { kind: "real" } };
62
214
  return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
63
215
  case "str":
64
216
  return { kind: "str", value: e.value, ty: { kind: "string" } };
65
217
  case "bool":
66
218
  return { kind: "bool", value: e.value, ty: { kind: "bool" } };
219
+ case "nonNull": {
220
+ const expr = resolveExpr(e.expr, ctx);
221
+ // Unwrap optional type; for map.get()!, force to direct access type
222
+ if (expr.kind === "call" && expr.fn.kind === "field" &&
223
+ expr.fn.obj.ty.kind === "map" && expr.fn.field === "get") {
224
+ return { ...expr, ty: expr.fn.obj.ty.value };
225
+ }
226
+ const ty = expr.ty.kind === "optional" ? expr.ty.inner : expr.ty;
227
+ return { ...expr, ty };
228
+ }
67
229
  case "binop": {
68
230
  let left = resolveExpr(e.left, ctx);
69
- let right = resolveExpr(e.right, ctx);
231
+ // && narrowing: if left is "x !== undefined", narrow x for right side
232
+ let rightCtx = ctx;
233
+ if (e.op === "&&") {
234
+ const narrowed = narrowOptional(e.left, ctx.env);
235
+ if (narrowed && narrowed.inThen) {
236
+ rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
237
+ }
238
+ }
239
+ let right = resolveExpr(e.right, rightCtx);
70
240
  if (e.op === "===" || e.op === "!==") {
71
241
  left = coerceStr(left, right.ty);
72
242
  right = coerceStr(right, left.ty);
73
243
  }
74
244
  let ty = { kind: "unknown" };
75
- if (["===", "!==", ">=", "<=", ">", "<", "&&", "||"].includes(e.op))
245
+ if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
76
246
  ty = { kind: "bool" };
77
- else if (["+", "-", "*", "/", "%"].includes(e.op))
78
- ty = left.ty;
247
+ else if (e.op === "&&")
248
+ ty = right.ty;
249
+ else if (e.op === "||" && left.ty.kind === "optional") {
250
+ // || undefined is identity for optionals — keep the optional type
251
+ ty = (e.right.kind === "var" && e.right.name === "undefined") ? left.ty : left.ty.inner;
252
+ }
253
+ else if (e.op === "||")
254
+ ty = right.ty;
255
+ else if (["+", "-", "*", "/", "%"].includes(e.op)) {
256
+ ty = (left.ty.kind === "real" || right.ty.kind === "real") ? { kind: "real" } : left.ty;
257
+ }
79
258
  return { kind: "binop", op: e.op, left, right, ty };
80
259
  }
81
260
  case "unop": {
82
261
  const expr = resolveExpr(e.expr, ctx);
83
262
  return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
84
263
  }
85
- case "call":
86
- return { kind: "call", fn: resolveExpr(e.fn, ctx), args: e.args.map(a => resolveExpr(a, ctx)), ty: { kind: "unknown" }, callKind: classifyCall(e.fn, ctx) };
264
+ case "call": {
265
+ 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
285
+ let argCtx = ctx;
286
+ if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
287
+ fn.obj.ty.elem.kind === "user") {
288
+ argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
289
+ }
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
+ }
367
+ return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
368
+ }
87
369
  case "index": {
88
370
  const obj = resolveExpr(e.obj, ctx);
89
371
  const idx = resolveExpr(e.idx, ctx);
90
- return { kind: "index", obj, idx, ty: obj.ty.kind === "array" ? obj.ty.elem : { kind: "unknown" } };
372
+ const idxTy = obj.ty.kind === "array" ? obj.ty.elem
373
+ : obj.ty.kind === "map" ? obj.ty.value
374
+ : { kind: "unknown" };
375
+ return { kind: "index", obj, idx, ty: idxTy };
91
376
  }
92
377
  case "field": {
93
378
  const obj = resolveExpr(e.obj, ctx);
@@ -96,6 +381,9 @@ function resolveExpr(e, ctx) {
96
381
  if (e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
97
382
  ty = { kind: "nat" };
98
383
  }
384
+ else if (e.field === "size" && (obj.ty.kind === "map" || obj.ty.kind === "set")) {
385
+ ty = { kind: "nat" };
386
+ }
99
387
  else if (obj.ty.kind === "user") {
100
388
  if (getDiscriminant(ctx, obj.ty.name) === e.field)
101
389
  isDiscriminant = true;
@@ -114,11 +402,23 @@ function resolveExpr(e, ctx) {
114
402
  // Infer record type: from spread, or from return type context
115
403
  const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
116
404
  const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
405
+ // Clear returnTy for field values — it applies to THIS record, not nested ones
406
+ const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
117
407
  const fields = e.fields.map(f => {
118
- let value = resolveExpr(f.value, ctx);
408
+ let value = resolveExpr(f.value, fieldCtx);
119
409
  const fieldDecl = decl?.fields?.find(df => df.name === f.name);
120
- if (fieldDecl)
121
- value = coerceStr(value, parseTsType(fieldDecl.tsType));
410
+ if (fieldDecl) {
411
+ const declTy = parseTsType(fieldDecl.tsType);
412
+ value = coerceStr(value, declTy);
413
+ // Coerce non-optional to optional: wrap in Some (only when value type is concrete)
414
+ 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
+ };
420
+ }
421
+ }
122
422
  return { name: f.name, value };
123
423
  });
124
424
  return { kind: "record", spread, fields, ty: recordTy ?? ty };
@@ -128,11 +428,13 @@ function resolveExpr(e, ctx) {
128
428
  throw new Error("\\result is only valid in ensures");
129
429
  return { kind: "result", ty: ctx.returnTy };
130
430
  case "forall": {
131
- const varTy = e.varType === "nat" ? { kind: "nat" } : { kind: "int" };
431
+ const varTy = e.varType !== "int" ? parseTsType(e.varType)
432
+ : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
132
433
  return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
133
434
  }
134
435
  case "exists": {
135
- const varTy = e.varType === "nat" ? { kind: "nat" } : { kind: "int" };
436
+ const varTy = e.varType !== "int" ? parseTsType(e.varType)
437
+ : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
136
438
  return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
137
439
  }
138
440
  case "arrayLiteral": {
@@ -159,13 +461,64 @@ function resolveExpr(e, ctx) {
159
461
  }
160
462
  case "conditional": {
161
463
  const cond = resolveExpr(e.cond, ctx);
162
- let then_ = resolveExpr(e.then, 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
+ let narrowedVar;
468
+ let narrowedExprResolved;
469
+ let thenCtx = ctx;
470
+ let rawThen = e.then;
471
+ if (cond.ty.kind === "optional") {
472
+ const innerTy = cond.ty.inner;
473
+ if (e.cond.kind === "var") {
474
+ narrowedVar = e.cond.name;
475
+ thenCtx = withEnv(ctx, extend(ctx.env, e.cond.name, innerTy));
476
+ }
477
+ else {
478
+ narrowedVar = `_opt${_synVarCounter++}`;
479
+ rawThen = substituteRawExpr(e.then, e.cond, { kind: "var", name: narrowedVar });
480
+ thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
481
+ }
482
+ }
483
+ // Explicit optional check: x !== undefined ? expr(x) : undefined
484
+ // Narrow x to its inner type in the then-branch.
485
+ if (!narrowedVar) {
486
+ const narrowed = narrowOptional(e.cond, ctx.env);
487
+ if (narrowed && narrowed.inThen) {
488
+ narrowedVar = narrowed.varName;
489
+ 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));
501
+ }
502
+ }
503
+ }
504
+ let then_ = resolveExpr(rawThen, thenCtx);
163
505
  let else_ = resolveExpr(e.else, ctx);
164
506
  then_ = coerceStr(then_, else_.ty);
165
507
  else_ = coerceStr(else_, then_.ty);
166
- const ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
167
- return { kind: "conditional", cond, then: then_, else: else_, ty };
508
+ 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") {
511
+ ty = { kind: "optional", inner: ty };
512
+ }
513
+ return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
168
514
  }
515
+ case "emptyCollection": {
516
+ const ty = parseTsType(e.tsType);
517
+ const elems = e.initElems ? e.initElems.map(el => resolveExpr(el, ctx)) : [];
518
+ return { kind: "arrayLiteral", elems, ty };
519
+ }
520
+ case "havoc":
521
+ return { kind: "havoc", ty: resolveTsType(e.tsType, ctx.overrides) };
169
522
  }
170
523
  }
171
524
  // ── Resolve specs ────────────────────────────────────────────
@@ -202,22 +555,54 @@ function resolveStmt(s, ctx) {
202
555
  case "let": {
203
556
  const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
204
557
  const init = coerceStr(resolveExpr(s.init, ctx), ty);
205
- return [{ kind: "let", name: s.name, ty, mutable: s.mutable, init }, extend(ctx.env, s.name, ty)];
558
+ // const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
559
+ const mutable = s.mutable || isRefMutableInTS(ty);
560
+ return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
206
561
  }
207
562
  case "assign": {
208
563
  const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
209
564
  return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
210
565
  }
211
- case "return":
212
- return [{ kind: "return", value: coerceStr(resolveExpr(s.value, ctx), ctx.returnTy) }, ctx.env];
566
+ case "return": {
567
+ let value = coerceStr(resolveExpr(s.value, ctx), ctx.returnTy);
568
+ // Wrap non-optional return value in Some when function returns optional
569
+ // Skip if already optional, void, or undefined (which maps to None)
570
+ const isUndef = value.kind === "var" && value.name === "undefined";
571
+ 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
+ };
577
+ }
578
+ return [{ kind: "return", value }, ctx.env];
579
+ }
213
580
  case "break":
214
581
  return [{ kind: "break" }, ctx.env];
215
582
  case "continue":
216
583
  return [{ kind: "continue" }, ctx.env];
217
584
  case "expr":
218
585
  return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
219
- case "if":
220
- return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, ctx), else: resolveBlock(s.else, ctx) }, ctx.env];
586
+ case "if": {
587
+ // Narrow optional<T> T when checking !== undefined or undefined !==
588
+ let thenCtx = ctx, elseCtx = ctx;
589
+ const narrowed = narrowOptional(s.cond, ctx.env);
590
+ if (narrowed) {
591
+ const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
592
+ if (narrowed.inThen)
593
+ thenCtx = withEnv(ctx, env);
594
+ else
595
+ elseCtx = withEnv(ctx, env);
596
+ }
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
+ return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
605
+ }
221
606
  case "while": {
222
607
  const whileSpecCtx = { ...ctx, inSpec: true };
223
608
  return [{
@@ -231,24 +616,66 @@ function resolveStmt(s, ctx) {
231
616
  }
232
617
  case "forof": {
233
618
  const iterable = resolveExpr(s.iterable, ctx);
234
- const elemTy = iterable.ty.kind === "array" ? iterable.ty.elem : { kind: "unknown" };
235
- const idxName = `_${s.varName}_idx`;
236
- const withIdx = extend(ctx.env, idxName, { kind: "nat" });
237
- const withElem = extend(withIdx, s.varName, elemTy);
238
- const bodyCtx = withEnv(ctx, withElem);
619
+ // Determine element types for each destructured name
620
+ const nameTypes = [];
621
+ let env = ctx.env;
622
+ if (s.names.length === 1) {
623
+ // Single name: element type from array/set
624
+ const elemTy = iterable.ty.kind === "array" ? iterable.ty.elem
625
+ : iterable.ty.kind === "set" ? iterable.ty.elem
626
+ : { kind: "unknown" };
627
+ nameTypes.push(elemTy);
628
+ }
629
+ else if (s.names.length >= 2 && iterable.ty.kind === "map") {
630
+ // Map destructuring: [key, value]
631
+ nameTypes.push(iterable.ty.key, iterable.ty.value);
632
+ }
633
+ else {
634
+ // General tuple destructuring: all unknown
635
+ for (const _ of s.names)
636
+ nameTypes.push({ kind: "unknown" });
637
+ }
638
+ const idxName = `_${s.names[0]}_idx`;
639
+ env = extend(env, idxName, { kind: "nat" });
640
+ for (let j = 0; j < s.names.length; j++) {
641
+ env = extend(env, s.names[j], nameTypes[j] ?? { kind: "unknown" });
642
+ }
643
+ const bodyCtx = withEnv(ctx, env);
239
644
  return [{
240
- kind: "forof", varName: s.varName, varTy: elemTy, iterable,
645
+ kind: "forof", names: s.names, nameTypes, iterable,
241
646
  invariants: resolveSpecs(s.invariants, { ...bodyCtx, inSpec: true }),
242
647
  doneWith: s.doneWith ? resolveSpec(s.doneWith, { ...bodyCtx, inSpec: true }) : null,
243
648
  body: resolveBlock(s.body, bodyCtx),
244
649
  }, ctx.env];
245
650
  }
651
+ case "throw":
652
+ return [{ kind: "throw" }, ctx.env];
246
653
  case "switch":
247
654
  return [{
248
655
  kind: "switch", expr: resolveExpr(s.expr, ctx), discriminant: s.discriminant,
249
656
  cases: s.cases.map(c => ({ label: c.label, body: resolveBlock(c.body, ctx) })),
250
657
  defaultBody: resolveBlock(s.defaultBody, ctx),
251
658
  }, ctx.env];
659
+ case "ghostLet": {
660
+ const specCtx = { ...ctx, inSpec: true };
661
+ // Handle new Set<T>() / new Map<K,V>() constructors
662
+ const collMatch = s.init.match(/^new\s+(Set|Map)<(.+)>\(\)$/);
663
+ const init = collMatch
664
+ ? resolveExpr({ kind: "emptyCollection", collectionType: collMatch[1], tsType: `${collMatch[1]}<${collMatch[2]}>` }, specCtx)
665
+ : resolveExpr(parseExpr(s.init), specCtx);
666
+ const ty = s.tsType ? parseTsType(s.tsType) : init.ty;
667
+ return [{ kind: "ghostLet", name: s.name, ty, init }, extend(ctx.env, s.name, ty)];
668
+ }
669
+ case "ghostAssign": {
670
+ const specCtx = { ...ctx, inSpec: true };
671
+ const value = resolveExpr(parseExpr(s.value), specCtx);
672
+ return [{ kind: "ghostAssign", target: s.target, value }, ctx.env];
673
+ }
674
+ case "assert": {
675
+ const specCtx = { ...ctx, inSpec: true };
676
+ const expr = resolveExpr(parseExpr(s.expr), specCtx);
677
+ return [{ kind: "assert", expr }, ctx.env];
678
+ }
252
679
  }
253
680
  }
254
681
  // ── Pure / return-in-loop detection ──────────────────────────
@@ -259,7 +686,7 @@ function isSyntacticallyPure(stmts) {
259
686
  case "while":
260
687
  case "forof": return false;
261
688
  case "let":
262
- if (s.mutable)
689
+ if (s.mutable || s.init.kind === "havoc")
263
690
  return false;
264
691
  break;
265
692
  case "if":
@@ -420,32 +847,71 @@ function containsReturn(stmts) {
420
847
  return false;
421
848
  }
422
849
  // ── Resolve function / module ────────────────────────────────
423
- function resolveFunction(fn, typeDecls, pureFns) {
424
- if (hasReturnInLoop(fn.body)) {
425
- throw new Error(`${fn.name}: return inside a loop is not supported.`);
426
- }
850
+ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
427
851
  const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
428
852
  const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
429
853
  const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
430
854
  let env = null;
431
855
  for (const p of params)
432
856
  env = extend(env, p.name, p.ty);
433
- const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, inSpec: false, inLambda: false };
857
+ const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
434
858
  const requiresCtx = { ...baseCtx, inSpec: true };
435
859
  const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
436
860
  return {
437
- name: fn.name, params, returnTy,
861
+ name: fn.name, typeParams: fn.typeParams, params, returnTy,
438
862
  requires: resolveSpecs(fn.requires, requiresCtx),
439
863
  ensures: resolveSpecs(fn.ensures, ensuresCtx),
440
864
  isPure: pureFns.has(fn.name),
441
865
  body: resolveBlock(fn.body, baseCtx),
442
866
  };
443
867
  }
868
+ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
869
+ const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
870
+ // Create a synthetic record type for 'this' so field access resolves
871
+ 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 })) };
873
+ 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
+ });
894
+ return { name: cls.name, fields, methods };
895
+ }
444
896
  export function resolveModule(raw) {
445
897
  const pureFns = computePureFns(raw.functions);
898
+ // Pre-compute function parameter types for optional coercion
899
+ const fnParams = new Map();
900
+ for (const fn of raw.functions) {
901
+ const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
902
+ fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
903
+ }
904
+ const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, inSpec: false, inLambda: false };
905
+ const constants = (raw.constants ?? []).map(c => ({
906
+ name: c.name,
907
+ ty: parseTsType(c.tsType),
908
+ value: resolveExpr(c.value, emptyCtx),
909
+ }));
446
910
  return {
447
911
  file: raw.file,
448
912
  typeDecls: raw.typeDecls,
449
- functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns)),
913
+ constants,
914
+ functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams)),
915
+ classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams)),
450
916
  };
451
917
  }