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.
- package/README.md +24 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +46 -52
- package/tools/dist/dafny-emit.js +362 -73
- package/tools/dist/extract.js +386 -15
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +44 -43
- package/tools/dist/resolve.js +285 -19
- package/tools/dist/specparser.js +61 -7
- package/tools/dist/transform.js +533 -187
- package/tools/dist/types.js +46 -13
package/tools/dist/resolve.js
CHANGED
|
@@ -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 (["===", "!==", ">=", "<=", ">", "<"
|
|
171
|
+
if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op))
|
|
76
172
|
ty = { kind: "bool" };
|
|
77
|
-
else if (
|
|
78
|
-
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
|
-
|
|
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" }
|
|
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" }
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
}
|
package/tools/dist/specparser.js
CHANGED
|
@@ -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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 = [];
|