lemmascript 0.0.1 → 0.2.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.
@@ -0,0 +1,717 @@
1
+ /**
2
+ * Resolve — Raw IR → Typed IR.
3
+ *
4
+ * Uses linked environments (Scheme-style) for lexical scoping.
5
+ * No mutation — each let extends the chain, lookup walks it.
6
+ */
7
+ import { parseTsType } from "./types.js";
8
+ import { parseExpr } from "./specparser.js";
9
+ function lookup(env, name) {
10
+ if (!env)
11
+ return undefined;
12
+ return env.name === name ? env.ty : lookup(env.parent, name);
13
+ }
14
+ function extend(env, name, ty) {
15
+ return { name, ty, parent: env };
16
+ }
17
+ function withEnv(ctx, env) {
18
+ return { ...ctx, env };
19
+ }
20
+ // ── TS type → Ty ─────────────────────────────────────────────
21
+ function resolveTsType(tsType, overrides, varName) {
22
+ if (varName) {
23
+ const o = overrides.get(varName);
24
+ if (o)
25
+ return parseTsType(o);
26
+ }
27
+ return parseTsType(tsType);
28
+ }
29
+ /** If expr is a string literal and targetTy is a user type, coerce the literal's type. */
30
+ function coerceStr(expr, targetTy) {
31
+ if (expr.kind === "str" && targetTy.kind === "user")
32
+ return { ...expr, ty: targetTy };
33
+ return expr;
34
+ }
35
+ // ── Helpers ──────────────────────────────────────────────────
36
+ /** Detect `v !== undefined` or `undefined !== v` where v: optional<T>. */
37
+ function narrowOptional(cond, env) {
38
+ if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
39
+ 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")
50
+ return null;
51
+ return { varName, innerTy: ty.inner, inThen: cond.op === "!==" };
52
+ }
53
+ /** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
54
+ function isRefMutableInTS(ty) {
55
+ return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
56
+ }
57
+ function findDecl(ctx, name) {
58
+ return ctx.typeDecls.find(d => d.name === name);
59
+ }
60
+ function getDiscriminant(ctx, typeName) {
61
+ return findDecl(ctx, typeName)?.discriminant;
62
+ }
63
+ /** Infer quantifier variable type from usage in body.
64
+ * If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
65
+ * return the collection's key type. Otherwise return null (default to int). */
66
+ function inferQuantVarType(varName, body, ctx) {
67
+ // Look for calls like map.has(k), map.get(k), or array.includes(k) where k is our variable
68
+ if (body.kind === "call" && body.fn.kind === "field" &&
69
+ (body.fn.field === "has" || body.fn.field === "get" || body.fn.field === "includes") &&
70
+ body.args.length === 1 && body.args[0].kind === "var" && body.args[0].name === varName) {
71
+ const objTy = lookup(ctx.env, body.fn.obj.kind === "var" ? body.fn.obj.name : "");
72
+ if (objTy?.kind === "map")
73
+ return objTy.key;
74
+ if (objTy?.kind === "set")
75
+ return objTy.elem;
76
+ if (objTy?.kind === "array")
77
+ return objTy.elem;
78
+ }
79
+ // Recurse into subexpressions
80
+ if (body.kind === "binop") {
81
+ return inferQuantVarType(varName, body.left, ctx) ?? inferQuantVarType(varName, body.right, ctx);
82
+ }
83
+ if (body.kind === "unop")
84
+ return inferQuantVarType(varName, body.expr, ctx);
85
+ if (body.kind === "call") {
86
+ for (const a of body.args) {
87
+ const r = inferQuantVarType(varName, a, ctx);
88
+ if (r)
89
+ return r;
90
+ }
91
+ return inferQuantVarType(varName, body.fn, ctx);
92
+ }
93
+ if (body.kind === "field")
94
+ return inferQuantVarType(varName, body.obj, ctx);
95
+ if (body.kind === "index") {
96
+ return inferQuantVarType(varName, body.obj, ctx) ?? inferQuantVarType(varName, body.idx, ctx);
97
+ }
98
+ if (body.kind === "conditional") {
99
+ return inferQuantVarType(varName, body.cond, ctx) ??
100
+ inferQuantVarType(varName, body.then, ctx) ?? inferQuantVarType(varName, body.else, ctx);
101
+ }
102
+ if ((body.kind === "forall" || body.kind === "exists") && body.var !== varName) {
103
+ return inferQuantVarType(varName, body.body, ctx);
104
+ }
105
+ if (body.kind === "arrayLiteral") {
106
+ for (const el of body.elems) {
107
+ const r = inferQuantVarType(varName, el, ctx);
108
+ if (r)
109
+ return r;
110
+ }
111
+ }
112
+ if (body.kind === "record") {
113
+ if (body.spread) {
114
+ const r = inferQuantVarType(varName, body.spread, ctx);
115
+ if (r)
116
+ return r;
117
+ }
118
+ for (const f of body.fields) {
119
+ const r = inferQuantVarType(varName, f.value, ctx);
120
+ if (r)
121
+ return r;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+ function classifyCall(fn, ctx) {
127
+ if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Math")
128
+ return "pure";
129
+ if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
130
+ return "spec-pure";
131
+ if (fn.kind === "var" && ctx.inSpec) {
132
+ // Not a known pure function — could be external (Lean-defined spec helper).
133
+ // Pass through as "pure" and let Lean catch any errors.
134
+ return "pure";
135
+ }
136
+ if (fn.kind === "var")
137
+ return "method";
138
+ return "unknown";
139
+ }
140
+ // ── Resolve expressions ──────────────────────────────────────
141
+ function resolveExpr(e, ctx) {
142
+ switch (e.kind) {
143
+ case "var":
144
+ return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
145
+ case "num":
146
+ if (!Number.isInteger(e.value))
147
+ return { kind: "num", value: e.value, ty: { kind: "real" } };
148
+ return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
149
+ case "str":
150
+ return { kind: "str", value: e.value, ty: { kind: "string" } };
151
+ case "bool":
152
+ return { kind: "bool", value: e.value, ty: { kind: "bool" } };
153
+ case "nonNull": {
154
+ const expr = resolveExpr(e.expr, ctx);
155
+ // Unwrap optional type; for map.get()!, force to direct access type
156
+ if (expr.kind === "call" && expr.fn.kind === "field" &&
157
+ expr.fn.obj.ty.kind === "map" && expr.fn.field === "get") {
158
+ return { ...expr, ty: expr.fn.obj.ty.value };
159
+ }
160
+ const ty = expr.ty.kind === "optional" ? expr.ty.inner : expr.ty;
161
+ return { ...expr, ty };
162
+ }
163
+ case "binop": {
164
+ let left = resolveExpr(e.left, ctx);
165
+ let right = resolveExpr(e.right, ctx);
166
+ if (e.op === "===" || e.op === "!==") {
167
+ left = coerceStr(left, right.ty);
168
+ right = coerceStr(right, left.ty);
169
+ }
170
+ let ty = { kind: "unknown" };
171
+ if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op))
172
+ ty = { kind: "bool" };
173
+ else if (e.op === "&&")
174
+ ty = right.ty;
175
+ else if (e.op === "||" && left.ty.kind === "optional")
176
+ ty = left.ty.inner;
177
+ else if (e.op === "||")
178
+ ty = right.ty;
179
+ else if (["+", "-", "*", "/", "%"].includes(e.op)) {
180
+ ty = (left.ty.kind === "real" || right.ty.kind === "real") ? { kind: "real" } : left.ty;
181
+ }
182
+ return { kind: "binop", op: e.op, left, right, ty };
183
+ }
184
+ case "unop": {
185
+ const expr = resolveExpr(e.expr, ctx);
186
+ return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
187
+ }
188
+ case "call": {
189
+ 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" };
242
+ }
243
+ return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
244
+ }
245
+ case "index": {
246
+ const obj = resolveExpr(e.obj, ctx);
247
+ const idx = resolveExpr(e.idx, ctx);
248
+ return { kind: "index", obj, idx, ty: obj.ty.kind === "array" ? obj.ty.elem : { kind: "unknown" } };
249
+ }
250
+ case "field": {
251
+ const obj = resolveExpr(e.obj, ctx);
252
+ let isDiscriminant = false;
253
+ let ty = { kind: "unknown" };
254
+ if (e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
255
+ ty = { kind: "nat" };
256
+ }
257
+ else if (e.field === "size" && (obj.ty.kind === "map" || obj.ty.kind === "set")) {
258
+ ty = { kind: "nat" };
259
+ }
260
+ else if (obj.ty.kind === "user") {
261
+ if (getDiscriminant(ctx, obj.ty.name) === e.field)
262
+ isDiscriminant = true;
263
+ const decl = findDecl(ctx, obj.ty.name);
264
+ if (decl?.kind === "record") {
265
+ const f = decl.fields?.find(f => f.name === e.field);
266
+ if (f)
267
+ ty = resolveTsType(f.tsType, ctx.overrides);
268
+ }
269
+ }
270
+ return { kind: "field", obj, field: e.field, ty, isDiscriminant };
271
+ }
272
+ case "record": {
273
+ const spread = e.spread ? resolveExpr(e.spread, ctx) : null;
274
+ const ty = spread ? spread.ty : { kind: "unknown" };
275
+ // Infer record type: from spread, or from return type context
276
+ const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
277
+ const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
278
+ const fields = e.fields.map(f => {
279
+ let value = resolveExpr(f.value, ctx);
280
+ const fieldDecl = decl?.fields?.find(df => df.name === f.name);
281
+ if (fieldDecl)
282
+ value = coerceStr(value, parseTsType(fieldDecl.tsType));
283
+ return { name: f.name, value };
284
+ });
285
+ return { kind: "record", spread, fields, ty: recordTy ?? ty };
286
+ }
287
+ case "result":
288
+ if (!ctx.allowResult)
289
+ throw new Error("\\result is only valid in ensures");
290
+ return { kind: "result", ty: ctx.returnTy };
291
+ case "forall": {
292
+ const varTy = e.varType === "nat" ? { kind: "nat" }
293
+ : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
294
+ return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
295
+ }
296
+ case "exists": {
297
+ const varTy = e.varType === "nat" ? { kind: "nat" }
298
+ : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
299
+ return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
300
+ }
301
+ case "arrayLiteral": {
302
+ const elems = e.elems.map(el => resolveExpr(el, ctx));
303
+ const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
304
+ return { kind: "arrayLiteral", elems, ty: { kind: "array", elem: elemTy } };
305
+ }
306
+ case "lambda": {
307
+ // Resolve lambda params — types from explicit annotation or unknown
308
+ const params = e.params.map(p => ({
309
+ name: p.name,
310
+ ty: p.tsType ? parseTsType(p.tsType) : { kind: "unknown" },
311
+ }));
312
+ // Extend env with lambda params
313
+ let lambdaEnv = ctx.env;
314
+ for (const p of params)
315
+ lambdaEnv = extend(lambdaEnv, p.name, p.ty);
316
+ const lambdaCtx = { ...withEnv(ctx, lambdaEnv), inLambda: true };
317
+ // Body: expression (wrap in return stmt) or statement block
318
+ const body = Array.isArray(e.body)
319
+ ? resolveBlock(e.body, lambdaCtx)
320
+ : [{ kind: "return", value: resolveExpr(e.body, lambdaCtx) }];
321
+ return { kind: "lambda", params, body, ty: { kind: "unknown" } };
322
+ }
323
+ case "conditional": {
324
+ const cond = resolveExpr(e.cond, ctx);
325
+ let then_ = resolveExpr(e.then, ctx);
326
+ let else_ = resolveExpr(e.else, ctx);
327
+ then_ = coerceStr(then_, else_.ty);
328
+ 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 };
331
+ }
332
+ case "emptyCollection": {
333
+ const ty = parseTsType(e.tsType);
334
+ return { kind: "arrayLiteral", elems: [], ty };
335
+ }
336
+ case "havoc":
337
+ return { kind: "havoc", ty: resolveTsType(e.tsType, ctx.overrides) };
338
+ }
339
+ }
340
+ // ── Resolve specs ────────────────────────────────────────────
341
+ function resolveSpec(spec, ctx) {
342
+ return resolveExpr(parseExpr(spec), ctx);
343
+ }
344
+ function resolveSpecs(specs, ctx) {
345
+ const result = [];
346
+ for (const spec of specs) {
347
+ for (const clause of splitConj(parseExpr(spec))) {
348
+ result.push(resolveExpr(clause, ctx));
349
+ }
350
+ }
351
+ return result;
352
+ }
353
+ function splitConj(e) {
354
+ if (e.kind === "binop" && e.op === "&&")
355
+ return [...splitConj(e.left), ...splitConj(e.right)];
356
+ return [e];
357
+ }
358
+ // ── Resolve statements ───────────────────────────────────────
359
+ function resolveBlock(stmts, ctx) {
360
+ const result = [];
361
+ let env = ctx.env;
362
+ for (const s of stmts) {
363
+ const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
364
+ result.push(typed);
365
+ env = nextEnv;
366
+ }
367
+ return result;
368
+ }
369
+ function resolveStmt(s, ctx) {
370
+ switch (s.kind) {
371
+ case "let": {
372
+ const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
373
+ const init = coerceStr(resolveExpr(s.init, ctx), ty);
374
+ // const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
375
+ const mutable = s.mutable || isRefMutableInTS(ty);
376
+ return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
377
+ }
378
+ case "assign": {
379
+ const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
380
+ return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
381
+ }
382
+ case "return":
383
+ return [{ kind: "return", value: coerceStr(resolveExpr(s.value, ctx), ctx.returnTy) }, ctx.env];
384
+ case "break":
385
+ return [{ kind: "break" }, ctx.env];
386
+ case "continue":
387
+ return [{ kind: "continue" }, ctx.env];
388
+ case "expr":
389
+ return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
390
+ case "if": {
391
+ // Narrow optional<T> → T when checking !== undefined or undefined !==
392
+ let thenCtx = ctx, elseCtx = ctx;
393
+ const narrowed = narrowOptional(s.cond, ctx.env);
394
+ if (narrowed) {
395
+ const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
396
+ if (narrowed.inThen)
397
+ thenCtx = withEnv(ctx, env);
398
+ else
399
+ elseCtx = withEnv(ctx, env);
400
+ }
401
+ return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
402
+ }
403
+ case "while": {
404
+ const whileSpecCtx = { ...ctx, inSpec: true };
405
+ return [{
406
+ kind: "while",
407
+ cond: resolveExpr(s.cond, ctx),
408
+ invariants: resolveSpecs(s.invariants, whileSpecCtx),
409
+ decreases: s.decreases ? resolveSpec(s.decreases, whileSpecCtx) : null,
410
+ doneWith: s.doneWith ? resolveSpec(s.doneWith, whileSpecCtx) : null,
411
+ body: resolveBlock(s.body, ctx),
412
+ }, ctx.env];
413
+ }
414
+ case "forof": {
415
+ const iterable = resolveExpr(s.iterable, ctx);
416
+ // Determine element types for each destructured name
417
+ const nameTypes = [];
418
+ let env = ctx.env;
419
+ if (s.names.length === 1) {
420
+ // Single name: element type from array/set
421
+ const elemTy = iterable.ty.kind === "array" ? iterable.ty.elem
422
+ : iterable.ty.kind === "set" ? iterable.ty.elem
423
+ : { kind: "unknown" };
424
+ nameTypes.push(elemTy);
425
+ }
426
+ else if (s.names.length >= 2 && iterable.ty.kind === "map") {
427
+ // Map destructuring: [key, value]
428
+ nameTypes.push(iterable.ty.key, iterable.ty.value);
429
+ }
430
+ else {
431
+ // General tuple destructuring: all unknown
432
+ for (const _ of s.names)
433
+ nameTypes.push({ kind: "unknown" });
434
+ }
435
+ const idxName = `_${s.names[0]}_idx`;
436
+ env = extend(env, idxName, { kind: "nat" });
437
+ for (let j = 0; j < s.names.length; j++) {
438
+ env = extend(env, s.names[j], nameTypes[j] ?? { kind: "unknown" });
439
+ }
440
+ const bodyCtx = withEnv(ctx, env);
441
+ return [{
442
+ kind: "forof", names: s.names, nameTypes, iterable,
443
+ invariants: resolveSpecs(s.invariants, { ...bodyCtx, inSpec: true }),
444
+ doneWith: s.doneWith ? resolveSpec(s.doneWith, { ...bodyCtx, inSpec: true }) : null,
445
+ body: resolveBlock(s.body, bodyCtx),
446
+ }, ctx.env];
447
+ }
448
+ case "throw":
449
+ return [{ kind: "throw" }, ctx.env];
450
+ case "switch":
451
+ return [{
452
+ kind: "switch", expr: resolveExpr(s.expr, ctx), discriminant: s.discriminant,
453
+ cases: s.cases.map(c => ({ label: c.label, body: resolveBlock(c.body, ctx) })),
454
+ defaultBody: resolveBlock(s.defaultBody, ctx),
455
+ }, ctx.env];
456
+ case "ghostLet": {
457
+ const specCtx = { ...ctx, inSpec: true };
458
+ // Handle new Set<T>() / new Map<K,V>() constructors
459
+ const collMatch = s.init.match(/^new\s+(Set|Map)<(.+)>\(\)$/);
460
+ const init = collMatch
461
+ ? resolveExpr({ kind: "emptyCollection", collectionType: collMatch[1], tsType: `${collMatch[1]}<${collMatch[2]}>` }, specCtx)
462
+ : resolveExpr(parseExpr(s.init), specCtx);
463
+ const ty = s.tsType ? parseTsType(s.tsType) : init.ty;
464
+ return [{ kind: "ghostLet", name: s.name, ty, init }, extend(ctx.env, s.name, ty)];
465
+ }
466
+ case "ghostAssign": {
467
+ const specCtx = { ...ctx, inSpec: true };
468
+ const value = resolveExpr(parseExpr(s.value), specCtx);
469
+ return [{ kind: "ghostAssign", target: s.target, value }, ctx.env];
470
+ }
471
+ case "assert": {
472
+ const specCtx = { ...ctx, inSpec: true };
473
+ const expr = resolveExpr(parseExpr(s.expr), specCtx);
474
+ return [{ kind: "assert", expr }, ctx.env];
475
+ }
476
+ }
477
+ }
478
+ // ── Pure / return-in-loop detection ──────────────────────────
479
+ /** Syntactic purity: no while, no for-of, no mutable let. */
480
+ function isSyntacticallyPure(stmts) {
481
+ for (const s of stmts) {
482
+ switch (s.kind) {
483
+ case "while":
484
+ case "forof": return false;
485
+ case "let":
486
+ if (s.mutable)
487
+ return false;
488
+ break;
489
+ case "if":
490
+ if (!isSyntacticallyPure(s.then) || !isSyntacticallyPure(s.else))
491
+ return false;
492
+ break;
493
+ case "switch":
494
+ if (!s.cases.every(c => isSyntacticallyPure(c.body)) || !isSyntacticallyPure(s.defaultBody))
495
+ return false;
496
+ break;
497
+ }
498
+ }
499
+ return true;
500
+ }
501
+ // ── Call graph ──────────────────────────────────────────────
502
+ /** Collect all same-file function calls from expressions (including inside lambdas). */
503
+ function collectCallsExpr(e, fns, out) {
504
+ switch (e.kind) {
505
+ case "call":
506
+ if (e.fn.kind === "var" && fns.has(e.fn.name))
507
+ out.add(e.fn.name);
508
+ collectCallsExpr(e.fn, fns, out);
509
+ for (const a of e.args)
510
+ collectCallsExpr(a, fns, out);
511
+ return;
512
+ case "binop":
513
+ collectCallsExpr(e.left, fns, out);
514
+ collectCallsExpr(e.right, fns, out);
515
+ return;
516
+ case "unop":
517
+ collectCallsExpr(e.expr, fns, out);
518
+ return;
519
+ case "field":
520
+ collectCallsExpr(e.obj, fns, out);
521
+ return;
522
+ case "index":
523
+ collectCallsExpr(e.obj, fns, out);
524
+ collectCallsExpr(e.idx, fns, out);
525
+ return;
526
+ case "record":
527
+ if (e.spread)
528
+ collectCallsExpr(e.spread, fns, out);
529
+ for (const f of e.fields)
530
+ collectCallsExpr(f.value, fns, out);
531
+ return;
532
+ case "arrayLiteral":
533
+ for (const el of e.elems)
534
+ collectCallsExpr(el, fns, out);
535
+ return;
536
+ case "lambda":
537
+ if (Array.isArray(e.body))
538
+ collectCallsStmts(e.body, fns, out);
539
+ else
540
+ collectCallsExpr(e.body, fns, out);
541
+ return;
542
+ case "forall":
543
+ case "exists":
544
+ collectCallsExpr(e.body, fns, out);
545
+ return;
546
+ case "conditional":
547
+ collectCallsExpr(e.cond, fns, out);
548
+ collectCallsExpr(e.then, fns, out);
549
+ collectCallsExpr(e.else, fns, out);
550
+ return;
551
+ }
552
+ }
553
+ function collectCallsStmts(stmts, fns, out) {
554
+ for (const s of stmts) {
555
+ switch (s.kind) {
556
+ case "let":
557
+ collectCallsExpr(s.init, fns, out);
558
+ break;
559
+ case "assign":
560
+ collectCallsExpr(s.value, fns, out);
561
+ break;
562
+ case "return":
563
+ collectCallsExpr(s.value, fns, out);
564
+ break;
565
+ case "expr":
566
+ collectCallsExpr(s.expr, fns, out);
567
+ break;
568
+ case "if":
569
+ collectCallsExpr(s.cond, fns, out);
570
+ collectCallsStmts(s.then, fns, out);
571
+ collectCallsStmts(s.else, fns, out);
572
+ break;
573
+ case "while":
574
+ collectCallsExpr(s.cond, fns, out);
575
+ collectCallsStmts(s.body, fns, out);
576
+ break;
577
+ case "forof":
578
+ collectCallsExpr(s.iterable, fns, out);
579
+ collectCallsStmts(s.body, fns, out);
580
+ break;
581
+ case "switch":
582
+ collectCallsExpr(s.expr, fns, out);
583
+ for (const c of s.cases)
584
+ collectCallsStmts(c.body, fns, out);
585
+ collectCallsStmts(s.defaultBody, fns, out);
586
+ break;
587
+ }
588
+ }
589
+ }
590
+ function computePureFns(functions) {
591
+ const allFnNames = new Set(functions.map(fn => fn.name));
592
+ // Build call graph: fn → set of same-file functions it calls
593
+ const callGraph = new Map();
594
+ for (const fn of functions) {
595
+ const calls = new Set();
596
+ collectCallsStmts(fn.body, allFnNames, calls);
597
+ callGraph.set(fn.name, calls);
598
+ }
599
+ // Seed: syntactically non-pure functions
600
+ const nonPure = new Set(functions.filter(fn => !isSyntacticallyPure(fn.body)).map(fn => fn.name));
601
+ // Build reverse graph: fn → set of functions that call it
602
+ const callers = new Map();
603
+ for (const name of allFnNames)
604
+ callers.set(name, new Set());
605
+ for (const [caller, callees] of callGraph) {
606
+ for (const callee of callees)
607
+ callers.get(callee).add(caller);
608
+ }
609
+ // Propagate impurity through reverse call graph
610
+ const worklist = [...nonPure];
611
+ while (worklist.length > 0) {
612
+ const fn = worklist.pop();
613
+ for (const caller of callers.get(fn) ?? []) {
614
+ if (!nonPure.has(caller)) {
615
+ nonPure.add(caller);
616
+ worklist.push(caller);
617
+ }
618
+ }
619
+ }
620
+ return new Set(functions.map(fn => fn.name).filter(name => !nonPure.has(name)));
621
+ }
622
+ function hasReturnInLoop(stmts) {
623
+ for (const s of stmts) {
624
+ if ((s.kind === "while" || s.kind === "forof") && containsReturn(s.body))
625
+ return true;
626
+ if (s.kind === "if" && (hasReturnInLoop(s.then) || hasReturnInLoop(s.else)))
627
+ return true;
628
+ if (s.kind === "switch" && (s.cases.some(c => hasReturnInLoop(c.body)) || hasReturnInLoop(s.defaultBody)))
629
+ return true;
630
+ }
631
+ return false;
632
+ }
633
+ function containsReturn(stmts) {
634
+ for (const s of stmts) {
635
+ if (s.kind === "return")
636
+ return true;
637
+ if (s.kind === "if" && (containsReturn(s.then) || containsReturn(s.else)))
638
+ return true;
639
+ if ((s.kind === "while" || s.kind === "forof") && containsReturn(s.body))
640
+ return true;
641
+ if (s.kind === "switch" && (s.cases.some(c => containsReturn(c.body)) || containsReturn(s.defaultBody)))
642
+ return true;
643
+ }
644
+ return false;
645
+ }
646
+ // ── 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
+ }
651
+ const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
652
+ const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
653
+ const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
654
+ let env = null;
655
+ for (const p of params)
656
+ env = extend(env, p.name, p.ty);
657
+ const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
658
+ const requiresCtx = { ...baseCtx, inSpec: true };
659
+ const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
660
+ return {
661
+ name: fn.name, params, returnTy,
662
+ requires: resolveSpecs(fn.requires, requiresCtx),
663
+ ensures: resolveSpecs(fn.ensures, ensuresCtx),
664
+ isPure: pureFns.has(fn.name),
665
+ body: resolveBlock(fn.body, baseCtx),
666
+ };
667
+ }
668
+ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
669
+ const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
670
+ // Create a synthetic record type for 'this' so field access resolves
671
+ 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 })) };
673
+ 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
+ });
694
+ return { name: cls.name, fields, methods };
695
+ }
696
+ export function resolveModule(raw) {
697
+ const pureFns = computePureFns(raw.functions);
698
+ // Pre-compute function parameter types for optional coercion
699
+ const fnParams = new Map();
700
+ for (const fn of raw.functions) {
701
+ const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
702
+ fnParams.set(fn.name, fn.params.map(p => resolveTsType(p.tsType, overrides, p.name)));
703
+ }
704
+ const emptyCtx = { env: null, typeDecls: raw.typeDecls, overrides: new Map(), allowResult: false, returnTy: { kind: "int" }, pureFns, fnParams, inSpec: false, inLambda: false };
705
+ const constants = (raw.constants ?? []).map(c => ({
706
+ name: c.name,
707
+ ty: parseTsType(c.tsType),
708
+ value: resolveExpr(c.value, emptyCtx),
709
+ }));
710
+ return {
711
+ file: raw.file,
712
+ typeDecls: raw.typeDecls,
713
+ constants,
714
+ functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns, fnParams)),
715
+ classes: (raw.classes ?? []).map(cls => resolveClass(cls, raw.typeDecls, pureFns, fnParams)),
716
+ };
717
+ }