lemmascript 0.2.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.
- package/README.md +3 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +1 -1
- package/tools/dist/dafny-emit.js +149 -36
- package/tools/dist/extract.js +480 -50
- package/tools/dist/lean-emit.js +6 -2
- package/tools/dist/lsc.js +21 -4
- package/tools/dist/resolve.js +225 -25
- package/tools/dist/specparser.js +5 -2
- package/tools/dist/transform.js +284 -19
- package/tools/dist/types.js +14 -1
package/tools/dist/lean-emit.js
CHANGED
|
@@ -51,7 +51,7 @@ function escapeName(name) {
|
|
|
51
51
|
const PREC = {
|
|
52
52
|
"→": 1, "∨": 2, "∧": 3,
|
|
53
53
|
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
54
|
-
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
|
|
54
|
+
"+": 5, "-": 5, "++": 5, "arrayConcat": 5, "*": 6, "/": 6, "%": 6,
|
|
55
55
|
};
|
|
56
56
|
function prec(op) { return PREC[op] ?? 10; }
|
|
57
57
|
// ── Method call → Lean syntax ───────────────────────────────
|
|
@@ -143,7 +143,8 @@ function emitExpr(e, parentPrec) {
|
|
|
143
143
|
return `-${e.expr.value}`;
|
|
144
144
|
return `(-${emitExpr(e.expr)})`;
|
|
145
145
|
case "binop": {
|
|
146
|
-
const
|
|
146
|
+
const op = e.op === "arrayConcat" ? "++" : e.op;
|
|
147
|
+
const s = `${emitExpr(e.left, prec(e.op))} ${op} ${emitExpr(e.right, prec(e.op))}`;
|
|
147
148
|
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
148
149
|
}
|
|
149
150
|
case "implies": {
|
|
@@ -317,6 +318,9 @@ function emitDecl(d) {
|
|
|
317
318
|
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
318
319
|
return lines.join("\n");
|
|
319
320
|
}
|
|
321
|
+
case "type-alias": {
|
|
322
|
+
return `abbrev ${d.name} := ${tyToLean(d.target)}`;
|
|
323
|
+
}
|
|
320
324
|
case "def": {
|
|
321
325
|
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
322
326
|
return `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
|
package/tools/dist/lsc.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync } from "fs";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { extractModule } from "./extract.js";
|
|
11
11
|
import { resolveModule } from "./resolve.js";
|
|
12
|
-
import {
|
|
12
|
+
import { transformModuleLean, transformModuleDafny } from "./transform.js";
|
|
13
13
|
import { emitLeanFile } from "./lean-emit.js";
|
|
14
14
|
import { emitDafnyFile } from "./dafny-emit.js";
|
|
15
15
|
import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
|
|
@@ -17,7 +17,7 @@ import { leanGen, leanCheck } from "./lean-commands.js";
|
|
|
17
17
|
function main() {
|
|
18
18
|
const args = process.argv.slice(2);
|
|
19
19
|
const backendIdx = args.findIndex(a => a.startsWith("--backend="));
|
|
20
|
-
let backend = "
|
|
20
|
+
let backend = "dafny";
|
|
21
21
|
if (backendIdx >= 0) {
|
|
22
22
|
const val = args[backendIdx].split("=")[1];
|
|
23
23
|
if (val !== "lean" && val !== "dafny") {
|
|
@@ -43,8 +43,25 @@ function main() {
|
|
|
43
43
|
console.error(`File not found: ${absPath}`);
|
|
44
44
|
process.exit(1);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
// Find nearest tsconfig.json for import resolution; fall back to bare options
|
|
47
|
+
function findTsConfig(from) {
|
|
48
|
+
let dir = path.dirname(from);
|
|
49
|
+
while (true) {
|
|
50
|
+
const candidate = path.join(dir, "tsconfig.json");
|
|
51
|
+
if (existsSync(candidate))
|
|
52
|
+
return candidate;
|
|
53
|
+
const parent = path.dirname(dir);
|
|
54
|
+
if (parent === dir)
|
|
55
|
+
return undefined;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const tsConfigFilePath = findTsConfig(absPath);
|
|
60
|
+
const project = tsConfigFilePath
|
|
61
|
+
? new Project({ tsConfigFilePath })
|
|
62
|
+
: new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
|
|
47
63
|
const sourceFile = project.addSourceFileAtPath(absPath);
|
|
64
|
+
project.resolveSourceFileDependencies();
|
|
48
65
|
// Check //@ backend directive — skip if backend doesn't match
|
|
49
66
|
const backendDirective = sourceFile.getFullText().match(/\/\/@ backend (\w+)/);
|
|
50
67
|
if (backendDirective && backendDirective[1] !== backend) {
|
|
@@ -98,7 +115,7 @@ function main() {
|
|
|
98
115
|
// ── Lean backend ──────────────────────────────────────────
|
|
99
116
|
const specPath = path.join(dir, `${base}.spec.lean`);
|
|
100
117
|
const specImport = existsSync(specPath) ? `«${base}.spec»` : undefined;
|
|
101
|
-
const { typesFile, defFile } =
|
|
118
|
+
const { typesFile, defFile } = transformModuleLean(typed, specImport);
|
|
102
119
|
const typesPath = typesFile ? path.join(dir, `${base}.types.lean`) : null;
|
|
103
120
|
const typesText = typesFile ? emitLeanFile(typesFile) : null;
|
|
104
121
|
const defPath = path.join(dir, `${base}.def.lean`);
|
package/tools/dist/resolve.js
CHANGED
|
@@ -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;
|
|
@@ -162,18 +228,28 @@ function resolveExpr(e, ctx) {
|
|
|
162
228
|
}
|
|
163
229
|
case "binop": {
|
|
164
230
|
let left = resolveExpr(e.left, ctx);
|
|
165
|
-
|
|
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);
|
|
166
240
|
if (e.op === "===" || e.op === "!==") {
|
|
167
241
|
left = coerceStr(left, right.ty);
|
|
168
242
|
right = coerceStr(right, left.ty);
|
|
169
243
|
}
|
|
170
244
|
let ty = { kind: "unknown" };
|
|
171
|
-
if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op))
|
|
245
|
+
if (["===", "!==", ">=", "<=", ">", "<", "in"].includes(e.op))
|
|
172
246
|
ty = { kind: "bool" };
|
|
173
247
|
else if (e.op === "&&")
|
|
174
248
|
ty = right.ty;
|
|
175
|
-
else if (e.op === "||" && left.ty.kind === "optional")
|
|
176
|
-
|
|
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
|
+
}
|
|
177
253
|
else if (e.op === "||")
|
|
178
254
|
ty = right.ty;
|
|
179
255
|
else if (["+", "-", "*", "/", "%"].includes(e.op)) {
|
|
@@ -187,12 +263,41 @@ function resolveExpr(e, ctx) {
|
|
|
187
263
|
}
|
|
188
264
|
case "call": {
|
|
189
265
|
const fn = resolveExpr(e.fn, ctx);
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
192
292
|
if (fn.kind === "var" && ctx.fnParams.has(fn.name)) {
|
|
193
293
|
const paramTys = ctx.fnParams.get(fn.name);
|
|
194
294
|
args = args.map((a, i) => {
|
|
195
|
-
if (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") {
|
|
196
301
|
return {
|
|
197
302
|
kind: "call",
|
|
198
303
|
fn: { kind: "var", name: "Some", ty: paramTys[i] },
|
|
@@ -203,6 +308,12 @@ function resolveExpr(e, ctx) {
|
|
|
203
308
|
}
|
|
204
309
|
return a;
|
|
205
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
|
+
}
|
|
206
317
|
}
|
|
207
318
|
let ty = { kind: "unknown" };
|
|
208
319
|
// Infer return types for collection methods
|
|
@@ -213,6 +324,8 @@ function resolveExpr(e, ctx) {
|
|
|
213
324
|
ty = { kind: "bool" };
|
|
214
325
|
else if (fn.field === "set")
|
|
215
326
|
ty = fn.obj.ty;
|
|
327
|
+
else if (fn.field === "delete")
|
|
328
|
+
ty = fn.obj.ty;
|
|
216
329
|
}
|
|
217
330
|
else if (fn.kind === "field" && fn.obj.ty.kind === "set") {
|
|
218
331
|
if (fn.field === "has")
|
|
@@ -229,6 +342,17 @@ function resolveExpr(e, ctx) {
|
|
|
229
342
|
ty = fn.obj.ty.elem;
|
|
230
343
|
else if (fn.field === "push")
|
|
231
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" };
|
|
232
356
|
}
|
|
233
357
|
else if (fn.kind === "field" && fn.obj.ty.kind === "string") {
|
|
234
358
|
if (fn.field === "trim")
|
|
@@ -245,7 +369,10 @@ function resolveExpr(e, ctx) {
|
|
|
245
369
|
case "index": {
|
|
246
370
|
const obj = resolveExpr(e.obj, ctx);
|
|
247
371
|
const idx = resolveExpr(e.idx, ctx);
|
|
248
|
-
|
|
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 };
|
|
249
376
|
}
|
|
250
377
|
case "field": {
|
|
251
378
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -275,11 +402,23 @@ function resolveExpr(e, ctx) {
|
|
|
275
402
|
// Infer record type: from spread, or from return type context
|
|
276
403
|
const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
|
|
277
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;
|
|
278
407
|
const fields = e.fields.map(f => {
|
|
279
|
-
let value = resolveExpr(f.value,
|
|
408
|
+
let value = resolveExpr(f.value, fieldCtx);
|
|
280
409
|
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
281
|
-
if (fieldDecl)
|
|
282
|
-
|
|
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
|
+
}
|
|
283
422
|
return { name: f.name, value };
|
|
284
423
|
});
|
|
285
424
|
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
@@ -289,12 +428,12 @@ function resolveExpr(e, ctx) {
|
|
|
289
428
|
throw new Error("\\result is only valid in ensures");
|
|
290
429
|
return { kind: "result", ty: ctx.returnTy };
|
|
291
430
|
case "forall": {
|
|
292
|
-
const varTy = e.varType
|
|
431
|
+
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
293
432
|
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
294
433
|
return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
295
434
|
}
|
|
296
435
|
case "exists": {
|
|
297
|
-
const varTy = e.varType
|
|
436
|
+
const varTy = e.varType !== "int" ? parseTsType(e.varType)
|
|
298
437
|
: inferQuantVarType(e.var, e.body, ctx) ?? { kind: "int" };
|
|
299
438
|
return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
300
439
|
}
|
|
@@ -322,16 +461,61 @@ function resolveExpr(e, ctx) {
|
|
|
322
461
|
}
|
|
323
462
|
case "conditional": {
|
|
324
463
|
const cond = resolveExpr(e.cond, ctx);
|
|
325
|
-
|
|
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);
|
|
326
505
|
let else_ = resolveExpr(e.else, ctx);
|
|
327
506
|
then_ = coerceStr(then_, else_.ty);
|
|
328
507
|
else_ = coerceStr(else_, then_.ty);
|
|
329
|
-
|
|
330
|
-
|
|
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 };
|
|
331
514
|
}
|
|
332
515
|
case "emptyCollection": {
|
|
333
516
|
const ty = parseTsType(e.tsType);
|
|
334
|
-
|
|
517
|
+
const elems = e.initElems ? e.initElems.map(el => resolveExpr(el, ctx)) : [];
|
|
518
|
+
return { kind: "arrayLiteral", elems, ty };
|
|
335
519
|
}
|
|
336
520
|
case "havoc":
|
|
337
521
|
return { kind: "havoc", ty: resolveTsType(e.tsType, ctx.overrides) };
|
|
@@ -379,8 +563,20 @@ function resolveStmt(s, ctx) {
|
|
|
379
563
|
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
380
564
|
return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
|
|
381
565
|
}
|
|
382
|
-
case "return":
|
|
383
|
-
|
|
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
|
+
}
|
|
384
580
|
case "break":
|
|
385
581
|
return [{ kind: "break" }, ctx.env];
|
|
386
582
|
case "continue":
|
|
@@ -398,6 +594,13 @@ function resolveStmt(s, ctx) {
|
|
|
398
594
|
else
|
|
399
595
|
elseCtx = withEnv(ctx, env);
|
|
400
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
|
+
}
|
|
401
604
|
return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
402
605
|
}
|
|
403
606
|
case "while": {
|
|
@@ -483,7 +686,7 @@ function isSyntacticallyPure(stmts) {
|
|
|
483
686
|
case "while":
|
|
484
687
|
case "forof": return false;
|
|
485
688
|
case "let":
|
|
486
|
-
if (s.mutable)
|
|
689
|
+
if (s.mutable || s.init.kind === "havoc")
|
|
487
690
|
return false;
|
|
488
691
|
break;
|
|
489
692
|
case "if":
|
|
@@ -645,9 +848,6 @@ function containsReturn(stmts) {
|
|
|
645
848
|
}
|
|
646
849
|
// ── Resolve function / module ────────────────────────────────
|
|
647
850
|
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
851
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
652
852
|
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
653
853
|
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
@@ -658,7 +858,7 @@ function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
|
|
|
658
858
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
659
859
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
660
860
|
return {
|
|
661
|
-
name: fn.name, params, returnTy,
|
|
861
|
+
name: fn.name, typeParams: fn.typeParams, params, returnTy,
|
|
662
862
|
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
663
863
|
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
664
864
|
isPure: pureFns.has(fn.name),
|
|
@@ -684,7 +884,7 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
|
|
|
684
884
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
685
885
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
686
886
|
return {
|
|
687
|
-
name: fn.name, params, returnTy,
|
|
887
|
+
name: fn.name, typeParams: fn.typeParams, params, returnTy,
|
|
688
888
|
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
689
889
|
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
690
890
|
isPure: false, // class methods are never pure (they access this)
|
package/tools/dist/specparser.js
CHANGED
|
@@ -129,6 +129,11 @@ class Parser {
|
|
|
129
129
|
parseCmp() {
|
|
130
130
|
const left = this.parseAdd();
|
|
131
131
|
const t = this.peek();
|
|
132
|
+
// 'in' as infix membership operator (set/seq/map): x in S
|
|
133
|
+
if (t?.type === "ident" && t.value === "in") {
|
|
134
|
+
this.advance();
|
|
135
|
+
return { kind: "binop", op: "in", left, right: this.parseAdd() };
|
|
136
|
+
}
|
|
132
137
|
if (t?.type === "op" && ["===", "!==", "==", "!=", ">=", "<=", ">", "<"].includes(t.value)) {
|
|
133
138
|
this.advance();
|
|
134
139
|
// Normalize == to ===, != to !== so downstream sees one spelling
|
|
@@ -252,8 +257,6 @@ class Parser {
|
|
|
252
257
|
let varType = "int";
|
|
253
258
|
if (this.match("punc", ":")) {
|
|
254
259
|
const ty = this.expect("ident").value;
|
|
255
|
-
if (ty !== "nat" && ty !== "int")
|
|
256
|
-
throw new Error(`Unknown type '${ty}'`);
|
|
257
260
|
varType = ty;
|
|
258
261
|
}
|
|
259
262
|
this.expect("punc", ",");
|