lemmascript 0.1.0 → 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.
@@ -33,12 +33,96 @@ function coerceStr(expr, targetTy) {
33
33
  return expr;
34
34
  }
35
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
+ }
36
57
  function findDecl(ctx, name) {
37
58
  return ctx.typeDecls.find(d => d.name === name);
38
59
  }
39
60
  function getDiscriminant(ctx, typeName) {
40
61
  return findDecl(ctx, typeName)?.discriminant;
41
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
+ }
42
126
  function classifyCall(fn, ctx) {
43
127
  if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Math")
44
128
  return "pure";
@@ -59,11 +143,23 @@ function resolveExpr(e, ctx) {
59
143
  case "var":
60
144
  return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
61
145
  case "num":
146
+ if (!Number.isInteger(e.value))
147
+ return { kind: "num", value: e.value, ty: { kind: "real" } };
62
148
  return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
63
149
  case "str":
64
150
  return { kind: "str", value: e.value, ty: { kind: "string" } };
65
151
  case "bool":
66
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
+ }
67
163
  case "binop": {
68
164
  let left = resolveExpr(e.left, ctx);
69
165
  let right = resolveExpr(e.right, ctx);
@@ -72,18 +168,80 @@ function resolveExpr(e, ctx) {
72
168
  right = coerceStr(right, left.ty);
73
169
  }
74
170
  let ty = { kind: "unknown" };
75
- if (["===", "!==", ">=", "<=", ">", "<", "&&", "||"].includes(e.op))
171
+ if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op))
76
172
  ty = { kind: "bool" };
77
- else if (["+", "-", "*", "/", "%"].includes(e.op))
78
- ty = left.ty;
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
+ }
79
182
  return { kind: "binop", op: e.op, left, right, ty };
80
183
  }
81
184
  case "unop": {
82
185
  const expr = resolveExpr(e.expr, ctx);
83
186
  return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
84
187
  }
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) };
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
+ }
87
245
  case "index": {
88
246
  const obj = resolveExpr(e.obj, ctx);
89
247
  const idx = resolveExpr(e.idx, ctx);
@@ -96,6 +254,9 @@ function resolveExpr(e, ctx) {
96
254
  if (e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
97
255
  ty = { kind: "nat" };
98
256
  }
257
+ else if (e.field === "size" && (obj.ty.kind === "map" || obj.ty.kind === "set")) {
258
+ ty = { kind: "nat" };
259
+ }
99
260
  else if (obj.ty.kind === "user") {
100
261
  if (getDiscriminant(ctx, obj.ty.name) === e.field)
101
262
  isDiscriminant = true;
@@ -128,11 +289,13 @@ function resolveExpr(e, ctx) {
128
289
  throw new Error("\\result is only valid in ensures");
129
290
  return { kind: "result", ty: ctx.returnTy };
130
291
  case "forall": {
131
- const varTy = e.varType === "nat" ? { kind: "nat" } : { kind: "int" };
292
+ const varTy = e.varType === "nat" ? { kind: "nat" }
293
+ : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
132
294
  return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
133
295
  }
134
296
  case "exists": {
135
- const varTy = e.varType === "nat" ? { kind: "nat" } : { kind: "int" };
297
+ const varTy = e.varType === "nat" ? { kind: "nat" }
298
+ : inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
136
299
  return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
137
300
  }
138
301
  case "arrayLiteral": {
@@ -166,6 +329,12 @@ function resolveExpr(e, ctx) {
166
329
  const ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
167
330
  return { kind: "conditional", cond, then: then_, else: else_, ty };
168
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) };
169
338
  }
170
339
  }
171
340
  // ── Resolve specs ────────────────────────────────────────────
@@ -202,7 +371,9 @@ function resolveStmt(s, ctx) {
202
371
  case "let": {
203
372
  const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
204
373
  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)];
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)];
206
377
  }
207
378
  case "assign": {
208
379
  const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
@@ -216,8 +387,19 @@ function resolveStmt(s, ctx) {
216
387
  return [{ kind: "continue" }, ctx.env];
217
388
  case "expr":
218
389
  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];
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
+ }
221
403
  case "while": {
222
404
  const whileSpecCtx = { ...ctx, inSpec: true };
223
405
  return [{
@@ -231,24 +413,66 @@ function resolveStmt(s, ctx) {
231
413
  }
232
414
  case "forof": {
233
415
  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);
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);
239
441
  return [{
240
- kind: "forof", varName: s.varName, varTy: elemTy, iterable,
442
+ kind: "forof", names: s.names, nameTypes, iterable,
241
443
  invariants: resolveSpecs(s.invariants, { ...bodyCtx, inSpec: true }),
242
444
  doneWith: s.doneWith ? resolveSpec(s.doneWith, { ...bodyCtx, inSpec: true }) : null,
243
445
  body: resolveBlock(s.body, bodyCtx),
244
446
  }, ctx.env];
245
447
  }
448
+ case "throw":
449
+ return [{ kind: "throw" }, ctx.env];
246
450
  case "switch":
247
451
  return [{
248
452
  kind: "switch", expr: resolveExpr(s.expr, ctx), discriminant: s.discriminant,
249
453
  cases: s.cases.map(c => ({ label: c.label, body: resolveBlock(c.body, ctx) })),
250
454
  defaultBody: resolveBlock(s.defaultBody, ctx),
251
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
+ }
252
476
  }
253
477
  }
254
478
  // ── Pure / return-in-loop detection ──────────────────────────
@@ -420,7 +644,7 @@ function containsReturn(stmts) {
420
644
  return false;
421
645
  }
422
646
  // ── Resolve function / module ────────────────────────────────
423
- function resolveFunction(fn, typeDecls, pureFns) {
647
+ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
424
648
  if (hasReturnInLoop(fn.body)) {
425
649
  throw new Error(`${fn.name}: return inside a loop is not supported.`);
426
650
  }
@@ -430,7 +654,7 @@ function resolveFunction(fn, typeDecls, pureFns) {
430
654
  let env = null;
431
655
  for (const p of params)
432
656
  env = extend(env, p.name, p.ty);
433
- const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, inSpec: false, inLambda: false };
657
+ const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
434
658
  const requiresCtx = { ...baseCtx, inSpec: true };
435
659
  const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
436
660
  return {
@@ -441,11 +665,53 @@ function resolveFunction(fn, typeDecls, pureFns) {
441
665
  body: resolveBlock(fn.body, baseCtx),
442
666
  };
443
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
+ }
444
696
  export function resolveModule(raw) {
445
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
+ }));
446
710
  return {
447
711
  file: raw.file,
448
712
  typeDecls: raw.typeDecls,
449
- functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns)),
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)),
450
716
  };
451
717
  }
@@ -2,7 +2,7 @@
2
2
  * Spec expression parser.
3
3
  * Parses //@ annotation expressions into RawExpr AST nodes.
4
4
  */
5
- const MULTI_OPS = ["==>", "===", "!==", ">=", "<=", "&&", "||"];
5
+ const MULTI_OPS = ["==>", "===", "!==", "==", "!=", ">=", "<=", "&&", "||"];
6
6
  function tokenize(input) {
7
7
  const tokens = [];
8
8
  let i = 0;
@@ -28,10 +28,23 @@ function tokenize(input) {
28
28
  continue;
29
29
  }
30
30
  if (/[0-9]/.test(input[i])) {
31
- let n = "";
32
- while (i < input.length && /[0-9]/.test(input[i]))
33
- n += input[i++];
34
- tokens.push({ type: "num", value: parseInt(n) });
31
+ let value;
32
+ if (input[i] === "0" && i + 1 < input.length && input[i + 1] === "x") {
33
+ i += 2;
34
+ let hex = "";
35
+ while (i < input.length && /[0-9a-fA-F]/.test(input[i]))
36
+ hex += input[i++];
37
+ value = parseInt(hex, 16);
38
+ }
39
+ else {
40
+ let dec = "";
41
+ while (i < input.length && /[0-9]/.test(input[i]))
42
+ dec += input[i++];
43
+ value = parseInt(dec, 10);
44
+ }
45
+ if (i < input.length && input[i] === "n")
46
+ i++;
47
+ tokens.push({ type: "num", value });
35
48
  continue;
36
49
  }
37
50
  if (/[a-zA-Z_]/.test(input[i])) {
@@ -116,9 +129,11 @@ class Parser {
116
129
  parseCmp() {
117
130
  const left = this.parseAdd();
118
131
  const t = this.peek();
119
- if (t?.type === "op" && ["===", "!==", ">=", "<=", ">", "<"].includes(t.value)) {
132
+ if (t?.type === "op" && ["===", "!==", "==", "!=", ">=", "<=", ">", "<"].includes(t.value)) {
120
133
  this.advance();
121
- return { kind: "binop", op: t.value, left, right: this.parseAdd() };
134
+ // Normalize == to ===, != to !== so downstream sees one spelling
135
+ const op = t.value === "==" ? "===" : t.value === "!=" ? "!==" : t.value;
136
+ return { kind: "binop", op, left, right: this.parseAdd() };
122
137
  }
123
138
  return left;
124
139
  }
@@ -201,6 +216,34 @@ class Parser {
201
216
  this.advance();
202
217
  return { kind: "bool", value: false };
203
218
  }
219
+ // new Set<T>() / new Map<K,V>()
220
+ if (t.value === "new") {
221
+ this.advance();
222
+ const name = this.expect("ident").value;
223
+ if (name !== "Set" && name !== "Map")
224
+ throw new Error(`Unsupported constructor: new ${name}`);
225
+ // Skip <T> or <K,V> type arguments
226
+ let tsType = name;
227
+ if (this.match("op", "<")) {
228
+ let depth = 1;
229
+ let typeArgs = "";
230
+ while (depth > 0) {
231
+ const next = this.advance();
232
+ if (next.value === "<")
233
+ depth++;
234
+ else if (next.value === ">") {
235
+ depth--;
236
+ if (depth === 0)
237
+ break;
238
+ }
239
+ typeArgs += next.value;
240
+ }
241
+ tsType = `${name}<${typeArgs}>`;
242
+ }
243
+ this.expect("punc", "(");
244
+ this.expect("punc", ")");
245
+ return { kind: "emptyCollection", collectionType: name, tsType };
246
+ }
204
247
  if (t.value === "forall" || t.value === "exists") {
205
248
  const q = t.value;
206
249
  this.advance();
@@ -227,6 +270,17 @@ class Parser {
227
270
  this.expect("punc", ")");
228
271
  return expr;
229
272
  }
273
+ if (t.type === "punc" && t.value === "[") {
274
+ this.advance();
275
+ const elems = [];
276
+ if (!this.match("punc", "]")) {
277
+ elems.push(this.parseImplies());
278
+ while (this.match("punc", ","))
279
+ elems.push(this.parseImplies());
280
+ this.expect("punc", "]");
281
+ }
282
+ return { kind: "arrayLiteral", elems };
283
+ }
230
284
  if (t.type === "punc" && t.value === "{") {
231
285
  this.advance();
232
286
  const fields = [];