lemmascript 0.3.0 → 0.3.2
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/package.json +1 -1
- package/tools/dist/dafny-commands.js +3 -2
- package/tools/dist/dafny-emit.js +111 -155
- package/tools/dist/extract.js +88 -4
- package/tools/dist/lsc.js +7 -1
- package/tools/dist/resolve.js +229 -181
- package/tools/dist/transform.js +245 -147
package/tools/dist/resolve.js
CHANGED
|
@@ -99,22 +99,47 @@ function coerceStr(expr, targetTy) {
|
|
|
99
99
|
return expr;
|
|
100
100
|
}
|
|
101
101
|
// ── Helpers ──────────────────────────────────────────────────
|
|
102
|
-
/**
|
|
103
|
-
function
|
|
102
|
+
/** Wrap a resolved expression in Some() for optional coercion. */
|
|
103
|
+
function wrapSome(value, optionalTy) {
|
|
104
|
+
return {
|
|
105
|
+
kind: "call", fn: { kind: "var", name: "Some", ty: optionalTy },
|
|
106
|
+
args: [value], ty: optionalTy, callKind: "pure",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Detect `v !== undefined` or `undefined !== v` where v: optional<T>.
|
|
110
|
+
* Handles simple variables, field access chains, and arbitrary expressions.
|
|
111
|
+
* When `fieldExpr` is returned, callers must use `substituteRawExpr` to narrow.
|
|
112
|
+
*
|
|
113
|
+
* Does NOT recurse into `&&` — callers that need to detect optional checks
|
|
114
|
+
* inside `&&` conditions should check `cond.left` explicitly. */
|
|
115
|
+
function detectOptionalCheck(cond, ctx) {
|
|
104
116
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
105
117
|
return null;
|
|
106
|
-
//
|
|
107
|
-
let
|
|
108
|
-
if (cond.
|
|
109
|
-
|
|
110
|
-
if (cond.
|
|
111
|
-
|
|
112
|
-
if (!
|
|
118
|
+
// Identify the expression being checked against undefined
|
|
119
|
+
let optExpr = null;
|
|
120
|
+
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
121
|
+
optExpr = cond.left;
|
|
122
|
+
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
123
|
+
optExpr = cond.right;
|
|
124
|
+
if (!optExpr)
|
|
113
125
|
return null;
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
// Simple variable — env lookup, no substitution needed
|
|
127
|
+
if (optExpr.kind === "var") {
|
|
128
|
+
const ty = lookup(ctx.env, optExpr.name);
|
|
129
|
+
if (!ty || ty.kind !== "optional")
|
|
130
|
+
return null;
|
|
131
|
+
return { varName: optExpr.name, innerTy: ty.inner, inThen: cond.op === "!==" };
|
|
132
|
+
}
|
|
133
|
+
// Field access chain or arbitrary expression — resolve to check type, needs substitution
|
|
134
|
+
const resolved = resolveExpr(optExpr, ctx);
|
|
135
|
+
if (resolved.ty.kind === "optional") {
|
|
136
|
+
const synVar = optExpr.kind === "field" ? `_narr${_synVarCounter++}` : `_opt${_synVarCounter++}`;
|
|
137
|
+
return {
|
|
138
|
+
varName: synVar, innerTy: resolved.ty.inner, inThen: cond.op === "!==",
|
|
139
|
+
fieldExpr: optExpr, narrowedExpr: resolved,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
118
143
|
}
|
|
119
144
|
/** TS reference types that become value types in Dafny/Lean — const bindings need mutable var. */
|
|
120
145
|
function isRefMutableInTS(ty) {
|
|
@@ -203,10 +228,101 @@ function classifyCall(fn, ctx) {
|
|
|
203
228
|
return "method";
|
|
204
229
|
return "unknown";
|
|
205
230
|
}
|
|
231
|
+
// ── Call resolution helpers ─────────────────────────────────
|
|
232
|
+
/** Infer lambda param types from array method context (map, filter, etc.).
|
|
233
|
+
* Returns updated rawArgs with inferred tsType on the first lambda param. */
|
|
234
|
+
function inferLambdaParamTypes(fn, rawArgs) {
|
|
235
|
+
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
236
|
+
["map", "filter", "every", "some", "find"].includes(fn.field) &&
|
|
237
|
+
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
238
|
+
rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
|
|
239
|
+
const elemTy = fn.obj.ty.elem;
|
|
240
|
+
const tsType = elemTy.kind === "user" ? elemTy.name
|
|
241
|
+
: elemTy.kind === "string" ? "string"
|
|
242
|
+
: elemTy.kind === "int" || elemTy.kind === "nat" ? "number"
|
|
243
|
+
: elemTy.kind === "bool" ? "boolean" : undefined;
|
|
244
|
+
if (tsType) {
|
|
245
|
+
const lam = rawArgs[0];
|
|
246
|
+
const updatedParams = [{ ...lam.params[0], tsType }, ...lam.params.slice(1)];
|
|
247
|
+
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return rawArgs;
|
|
251
|
+
}
|
|
252
|
+
/** Coerce call arguments: string literals → user types, non-optional → Some, pad missing optional args. */
|
|
253
|
+
function coerceCallArgs(args, fn, ctx) {
|
|
254
|
+
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
255
|
+
return args;
|
|
256
|
+
const paramTys = ctx.fnParams.get(fn.name);
|
|
257
|
+
args = args.map((a, i) => {
|
|
258
|
+
if (i >= paramTys.length)
|
|
259
|
+
return a;
|
|
260
|
+
a = coerceStr(a, paramTys[i]);
|
|
261
|
+
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
262
|
+
return wrapSome(a, paramTys[i]);
|
|
263
|
+
}
|
|
264
|
+
return a;
|
|
265
|
+
});
|
|
266
|
+
// Pad missing optional args with None
|
|
267
|
+
for (let i = args.length; i < paramTys.length; i++) {
|
|
268
|
+
if (paramTys[i].kind === "optional") {
|
|
269
|
+
args.push({ kind: "var", name: "undefined", ty: paramTys[i] });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return args;
|
|
273
|
+
}
|
|
274
|
+
/** Infer return type for collection/string method calls. */
|
|
275
|
+
function inferMethodReturnTy(fn, args, ctx) {
|
|
276
|
+
if (fn.kind !== "field")
|
|
277
|
+
return { kind: "unknown" };
|
|
278
|
+
const objTy = fn.obj.ty;
|
|
279
|
+
if (objTy.kind === "map") {
|
|
280
|
+
if (fn.field === "get")
|
|
281
|
+
return ctx.inSpec ? objTy.value : { kind: "optional", inner: objTy.value };
|
|
282
|
+
if (fn.field === "has")
|
|
283
|
+
return { kind: "bool" };
|
|
284
|
+
if (fn.field === "set" || fn.field === "delete")
|
|
285
|
+
return objTy;
|
|
286
|
+
}
|
|
287
|
+
else if (objTy.kind === "set") {
|
|
288
|
+
if (fn.field === "has")
|
|
289
|
+
return { kind: "bool" };
|
|
290
|
+
if (fn.field === "add" || fn.field === "delete")
|
|
291
|
+
return objTy;
|
|
292
|
+
}
|
|
293
|
+
else if (objTy.kind === "array") {
|
|
294
|
+
if (fn.field === "includes")
|
|
295
|
+
return { kind: "bool" };
|
|
296
|
+
if (fn.field === "indexOf")
|
|
297
|
+
return { kind: "int" };
|
|
298
|
+
if (fn.field === "shift")
|
|
299
|
+
return objTy.elem;
|
|
300
|
+
if (fn.field === "push" || fn.field === "concat")
|
|
301
|
+
return objTy;
|
|
302
|
+
if (fn.field === "filter")
|
|
303
|
+
return objTy;
|
|
304
|
+
if (fn.field === "every" || fn.field === "some")
|
|
305
|
+
return { kind: "bool" };
|
|
306
|
+
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
307
|
+
const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
|
|
308
|
+
? args[0].body[0].value.ty : { kind: "unknown" };
|
|
309
|
+
return { kind: "array", elem: retTy };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (objTy.kind === "string") {
|
|
313
|
+
if (fn.field === "trim" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
|
|
314
|
+
return { kind: "string" };
|
|
315
|
+
if (fn.field === "includes")
|
|
316
|
+
return { kind: "bool" };
|
|
317
|
+
}
|
|
318
|
+
return { kind: "unknown" };
|
|
319
|
+
}
|
|
206
320
|
// ── Resolve expressions ──────────────────────────────────────
|
|
207
321
|
function resolveExpr(e, ctx) {
|
|
208
322
|
switch (e.kind) {
|
|
209
323
|
case "var":
|
|
324
|
+
if (e.name === "undefined")
|
|
325
|
+
return { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
210
326
|
return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
|
|
211
327
|
case "num":
|
|
212
328
|
if (!Number.isInteger(e.value))
|
|
@@ -228,15 +344,18 @@ function resolveExpr(e, ctx) {
|
|
|
228
344
|
}
|
|
229
345
|
case "binop": {
|
|
230
346
|
let left = resolveExpr(e.left, ctx);
|
|
231
|
-
// && narrowing: if left is "x !== undefined", narrow x for right side
|
|
347
|
+
// && narrowing: if left is "x !== undefined", narrow x for right side.
|
|
348
|
+
// Field chains are excluded — resolve can't substitute in sub-expressions;
|
|
349
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
232
350
|
let rightCtx = ctx;
|
|
351
|
+
let rawRight = e.right;
|
|
233
352
|
if (e.op === "&&") {
|
|
234
|
-
const narrowed =
|
|
235
|
-
if (narrowed && narrowed.inThen) {
|
|
353
|
+
const narrowed = detectOptionalCheck(e.left, ctx);
|
|
354
|
+
if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
|
|
236
355
|
rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
237
356
|
}
|
|
238
357
|
}
|
|
239
|
-
let right = resolveExpr(
|
|
358
|
+
let right = resolveExpr(rawRight, rightCtx);
|
|
240
359
|
if (e.op === "===" || e.op === "!==") {
|
|
241
360
|
left = coerceStr(left, right.ty);
|
|
242
361
|
right = coerceStr(right, left.ty);
|
|
@@ -263,114 +382,22 @@ function resolveExpr(e, ctx) {
|
|
|
263
382
|
}
|
|
264
383
|
case "call": {
|
|
265
384
|
const fn = resolveExpr(e.fn, ctx);
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
385
|
+
const rawArgs = inferLambdaParamTypes(fn, e.args);
|
|
386
|
+
// For .push() on a typed array, resolve args with element type context
|
|
285
387
|
let argCtx = ctx;
|
|
286
388
|
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
|
|
287
389
|
fn.obj.ty.elem.kind === "user") {
|
|
288
390
|
argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
|
|
289
391
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (fn.kind === "var" && ctx.fnParams.has(fn.name)) {
|
|
293
|
-
const paramTys = ctx.fnParams.get(fn.name);
|
|
294
|
-
args = args.map((a, i) => {
|
|
295
|
-
if (i >= paramTys.length)
|
|
296
|
-
return a;
|
|
297
|
-
// Coerce string literal to user type (e.g., 'MissingList' → Err constructor)
|
|
298
|
-
a = coerceStr(a, paramTys[i]);
|
|
299
|
-
// Wrap non-optional in Some when callee expects optional param
|
|
300
|
-
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
301
|
-
return {
|
|
302
|
-
kind: "call",
|
|
303
|
-
fn: { kind: "var", name: "Some", ty: paramTys[i] },
|
|
304
|
-
args: [a],
|
|
305
|
-
ty: paramTys[i],
|
|
306
|
-
callKind: "pure",
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
return a;
|
|
310
|
-
});
|
|
311
|
-
// Pad missing optional args with None
|
|
312
|
-
for (let i = args.length; i < paramTys.length; i++) {
|
|
313
|
-
if (paramTys[i].kind === "optional") {
|
|
314
|
-
args.push({ kind: "var", name: "undefined", ty: paramTys[i] });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
let ty = { kind: "unknown" };
|
|
319
|
-
// Infer return types for collection methods
|
|
320
|
-
if (fn.kind === "field" && fn.obj.ty.kind === "map") {
|
|
321
|
-
if (fn.field === "get")
|
|
322
|
-
ty = ctx.inSpec ? fn.obj.ty.value : { kind: "optional", inner: fn.obj.ty.value };
|
|
323
|
-
else if (fn.field === "has")
|
|
324
|
-
ty = { kind: "bool" };
|
|
325
|
-
else if (fn.field === "set")
|
|
326
|
-
ty = fn.obj.ty;
|
|
327
|
-
else if (fn.field === "delete")
|
|
328
|
-
ty = fn.obj.ty;
|
|
329
|
-
}
|
|
330
|
-
else if (fn.kind === "field" && fn.obj.ty.kind === "set") {
|
|
331
|
-
if (fn.field === "has")
|
|
332
|
-
ty = { kind: "bool" };
|
|
333
|
-
else if (fn.field === "add")
|
|
334
|
-
ty = fn.obj.ty;
|
|
335
|
-
else if (fn.field === "delete")
|
|
336
|
-
ty = fn.obj.ty;
|
|
337
|
-
}
|
|
338
|
-
else if (fn.kind === "field" && fn.obj.ty.kind === "array") {
|
|
339
|
-
if (fn.field === "includes")
|
|
340
|
-
ty = { kind: "bool" };
|
|
341
|
-
else if (fn.field === "shift")
|
|
342
|
-
ty = fn.obj.ty.elem;
|
|
343
|
-
else if (fn.field === "push")
|
|
344
|
-
ty = fn.obj.ty;
|
|
345
|
-
else if (fn.field === "concat")
|
|
346
|
-
ty = fn.obj.ty;
|
|
347
|
-
else if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
348
|
-
const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
|
|
349
|
-
? args[0].body[0].value.ty : { kind: "unknown" };
|
|
350
|
-
ty = { kind: "array", elem: retTy };
|
|
351
|
-
}
|
|
352
|
-
else if (fn.field === "filter")
|
|
353
|
-
ty = fn.obj.ty;
|
|
354
|
-
else if (fn.field === "every" || fn.field === "some")
|
|
355
|
-
ty = { kind: "bool" };
|
|
356
|
-
}
|
|
357
|
-
else if (fn.kind === "field" && fn.obj.ty.kind === "string") {
|
|
358
|
-
if (fn.field === "trim")
|
|
359
|
-
ty = { kind: "string" };
|
|
360
|
-
else if (fn.field === "toLowerCase")
|
|
361
|
-
ty = { kind: "string" };
|
|
362
|
-
else if (fn.field === "toUpperCase")
|
|
363
|
-
ty = { kind: "string" };
|
|
364
|
-
else if (fn.field === "includes")
|
|
365
|
-
ty = { kind: "bool" };
|
|
366
|
-
}
|
|
392
|
+
const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
|
|
393
|
+
const ty = inferMethodReturnTy(fn, args, ctx);
|
|
367
394
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
368
395
|
}
|
|
369
396
|
case "index": {
|
|
370
397
|
const obj = resolveExpr(e.obj, ctx);
|
|
371
398
|
const idx = resolveExpr(e.idx, ctx);
|
|
372
399
|
const idxTy = obj.ty.kind === "array" ? obj.ty.elem
|
|
373
|
-
: obj.ty.kind === "map" ? obj.ty.value
|
|
400
|
+
: obj.ty.kind === "map" ? { kind: "optional", inner: obj.ty.value }
|
|
374
401
|
: { kind: "unknown" };
|
|
375
402
|
return { kind: "index", obj, idx, ty: idxTy };
|
|
376
403
|
}
|
|
@@ -391,7 +418,17 @@ function resolveExpr(e, ctx) {
|
|
|
391
418
|
if (decl?.kind === "record") {
|
|
392
419
|
const f = decl.fields?.find(f => f.name === e.field);
|
|
393
420
|
if (f)
|
|
394
|
-
ty =
|
|
421
|
+
ty = f.type;
|
|
422
|
+
}
|
|
423
|
+
// Also resolve fields from discriminated-union variants
|
|
424
|
+
if (ty.kind === "unknown" && decl?.kind === "discriminated-union" && decl.variants) {
|
|
425
|
+
for (const variant of decl.variants) {
|
|
426
|
+
const f = variant.fields.find(f => f.name === e.field);
|
|
427
|
+
if (f) {
|
|
428
|
+
ty = f.type;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
395
432
|
}
|
|
396
433
|
}
|
|
397
434
|
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
@@ -408,15 +445,15 @@ function resolveExpr(e, ctx) {
|
|
|
408
445
|
let value = resolveExpr(f.value, fieldCtx);
|
|
409
446
|
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
410
447
|
if (fieldDecl) {
|
|
411
|
-
const declTy =
|
|
448
|
+
const declTy = fieldDecl.type;
|
|
412
449
|
value = coerceStr(value, declTy);
|
|
450
|
+
// Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
|
|
451
|
+
if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
|
|
452
|
+
value = { kind: "arrayLiteral", elems: [], ty: declTy };
|
|
453
|
+
}
|
|
413
454
|
// Coerce non-optional to optional: wrap in Some (only when value type is concrete)
|
|
414
455
|
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
|
-
};
|
|
456
|
+
value = wrapSome(value, declTy);
|
|
420
457
|
}
|
|
421
458
|
}
|
|
422
459
|
return { name: f.name, value };
|
|
@@ -461,13 +498,11 @@ function resolveExpr(e, ctx) {
|
|
|
461
498
|
}
|
|
462
499
|
case "conditional": {
|
|
463
500
|
const cond = resolveExpr(e.cond, ctx);
|
|
464
|
-
// Optional truthiness: opt ? X : Y
|
|
465
|
-
// Narrow the optional to its inner type in the then-branch so that
|
|
466
|
-
// field accesses resolve correctly (e.g. entry.decision.field).
|
|
467
501
|
let narrowedVar;
|
|
468
502
|
let narrowedExprResolved;
|
|
469
503
|
let thenCtx = ctx;
|
|
470
504
|
let rawThen = e.then;
|
|
505
|
+
// Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
|
|
471
506
|
if (cond.ty.kind === "optional") {
|
|
472
507
|
const innerTy = cond.ty.inner;
|
|
473
508
|
if (e.cond.kind === "var") {
|
|
@@ -480,24 +515,18 @@ function resolveExpr(e, ctx) {
|
|
|
480
515
|
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
|
|
481
516
|
}
|
|
482
517
|
}
|
|
483
|
-
// Explicit
|
|
484
|
-
//
|
|
518
|
+
// Phase 2: Explicit check — v !== undefined (simple vars, field chains,
|
|
519
|
+
// complex expressions all handled uniformly by detectOptionalCheck)
|
|
485
520
|
if (!narrowedVar) {
|
|
486
|
-
const narrowed =
|
|
521
|
+
const narrowed = detectOptionalCheck(e.cond, ctx)
|
|
522
|
+
// Phase 3: && with optional check — (v !== undefined && ...) ? ... : ...
|
|
523
|
+
?? (e.cond.kind === "binop" && e.cond.op === "&&" ? detectOptionalCheck(e.cond.left, ctx) : null);
|
|
487
524
|
if (narrowed && narrowed.inThen) {
|
|
488
525
|
narrowedVar = narrowed.varName;
|
|
489
526
|
thenCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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));
|
|
527
|
+
if (narrowed.fieldExpr) {
|
|
528
|
+
narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
|
|
529
|
+
rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
|
|
501
530
|
}
|
|
502
531
|
}
|
|
503
532
|
}
|
|
@@ -506,8 +535,16 @@ function resolveExpr(e, ctx) {
|
|
|
506
535
|
then_ = coerceStr(then_, else_.ty);
|
|
507
536
|
else_ = coerceStr(else_, then_.ty);
|
|
508
537
|
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
509
|
-
// When
|
|
510
|
-
if (
|
|
538
|
+
// When one branch is undefined, result is optional
|
|
539
|
+
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
540
|
+
ty = { kind: "optional", inner: else_.ty };
|
|
541
|
+
}
|
|
542
|
+
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
543
|
+
ty = { kind: "optional", inner: then_.ty };
|
|
544
|
+
}
|
|
545
|
+
// When narrowedExpr is set AND a branch is void, the match produces Optional
|
|
546
|
+
const hasVoidBranch = then_.ty.kind === "void" || else_.ty.kind === "void";
|
|
547
|
+
if (narrowedExprResolved && hasVoidBranch && ty.kind !== "optional") {
|
|
511
548
|
ty = { kind: "optional", inner: ty };
|
|
512
549
|
}
|
|
513
550
|
return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
|
|
@@ -547,14 +584,25 @@ function resolveBlock(stmts, ctx) {
|
|
|
547
584
|
const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
|
|
548
585
|
result.push(typed);
|
|
549
586
|
env = nextEnv;
|
|
587
|
+
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
588
|
+
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
589
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
590
|
+
if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) {
|
|
591
|
+
const narrowed = detectOptionalCheck(s.cond, withEnv(ctx, env));
|
|
592
|
+
if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
|
|
593
|
+
env = extend(env, narrowed.varName, narrowed.innerTy);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
550
596
|
}
|
|
551
597
|
return result;
|
|
552
598
|
}
|
|
553
599
|
function resolveStmt(s, ctx) {
|
|
554
600
|
switch (s.kind) {
|
|
555
601
|
case "let": {
|
|
556
|
-
const
|
|
557
|
-
const init = coerceStr(resolveExpr(s.init, ctx),
|
|
602
|
+
const declTy = resolveTsType(s.tsType, ctx.overrides, s.name);
|
|
603
|
+
const init = coerceStr(resolveExpr(s.init, ctx), declTy);
|
|
604
|
+
// Map indexing: TS says T, but access can fail → use Optional<T> from init
|
|
605
|
+
const ty = (declTy.kind !== "optional" && init.ty.kind === "optional") ? init.ty : declTy;
|
|
558
606
|
// const collections are mutable in value-semantics world (TS mutates in place, Dafny/Lean reassign)
|
|
559
607
|
const mutable = s.mutable || isRefMutableInTS(ty);
|
|
560
608
|
return [{ kind: "let", name: s.name, ty, mutable, init }, extend(ctx.env, s.name, ty)];
|
|
@@ -569,11 +617,7 @@ function resolveStmt(s, ctx) {
|
|
|
569
617
|
// Skip if already optional, void, or undefined (which maps to None)
|
|
570
618
|
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
571
619
|
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
|
-
};
|
|
620
|
+
value = wrapSome(value, ctx.returnTy);
|
|
577
621
|
}
|
|
578
622
|
return [{ kind: "return", value }, ctx.env];
|
|
579
623
|
}
|
|
@@ -584,23 +628,20 @@ function resolveStmt(s, ctx) {
|
|
|
584
628
|
case "expr":
|
|
585
629
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
586
630
|
case "if": {
|
|
587
|
-
// Narrow optional<T> → T when checking !== undefined or undefined
|
|
631
|
+
// Narrow optional<T> → T when checking !== undefined or undefined !==.
|
|
632
|
+
// Also checks left side of && conditions: if (x !== undefined && ...) { ... }
|
|
633
|
+
// Field chains are excluded — resolve can't substitute in statement bodies;
|
|
634
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
588
635
|
let thenCtx = ctx, elseCtx = ctx;
|
|
589
|
-
const narrowed =
|
|
590
|
-
|
|
636
|
+
const narrowed = detectOptionalCheck(s.cond, ctx)
|
|
637
|
+
?? (s.cond.kind === "binop" && s.cond.op === "&&" ? detectOptionalCheck(s.cond.left, ctx) : null);
|
|
638
|
+
if (narrowed && !narrowed.fieldExpr) {
|
|
591
639
|
const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
|
|
592
640
|
if (narrowed.inThen)
|
|
593
641
|
thenCtx = withEnv(ctx, env);
|
|
594
642
|
else
|
|
595
643
|
elseCtx = withEnv(ctx, env);
|
|
596
644
|
}
|
|
597
|
-
// Also narrow from left side of && condition: if (x !== undefined && ...) { ... }
|
|
598
|
-
if (!narrowed && s.cond.kind === "binop" && s.cond.op === "&&") {
|
|
599
|
-
const leftNarrowed = narrowOptional(s.cond.left, ctx.env);
|
|
600
|
-
if (leftNarrowed && leftNarrowed.inThen) {
|
|
601
|
-
thenCtx = withEnv(ctx, extend(ctx.env, leftNarrowed.varName, leftNarrowed.innerTy));
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
645
|
return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
605
646
|
}
|
|
606
647
|
case "while": {
|
|
@@ -847,21 +888,28 @@ function containsReturn(stmts) {
|
|
|
847
888
|
return false;
|
|
848
889
|
}
|
|
849
890
|
// ── Resolve function / module ────────────────────────────────
|
|
850
|
-
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
|
|
891
|
+
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
|
|
851
892
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
852
893
|
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
853
894
|
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
854
895
|
let env = null;
|
|
896
|
+
if (opts?.thisBinding)
|
|
897
|
+
env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
|
|
855
898
|
for (const p of params)
|
|
856
899
|
env = extend(env, p.name, p.ty);
|
|
857
900
|
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
858
901
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
859
902
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
903
|
+
// Apply type parameter constraints from //@ type T (==) annotations
|
|
904
|
+
const typeParams = fn.typeParams.map(tp => {
|
|
905
|
+
const constraint = overrides.get(tp);
|
|
906
|
+
return constraint ? `${tp}${constraint}` : tp;
|
|
907
|
+
});
|
|
860
908
|
return {
|
|
861
|
-
name: fn.name, typeParams
|
|
909
|
+
name: fn.name, typeParams, params, returnTy,
|
|
862
910
|
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
863
911
|
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
864
|
-
isPure: pureFns.has(fn.name),
|
|
912
|
+
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
865
913
|
body: resolveBlock(fn.body, baseCtx),
|
|
866
914
|
};
|
|
867
915
|
}
|
|
@@ -869,31 +917,31 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
|
|
|
869
917
|
const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
|
|
870
918
|
// Create a synthetic record type for 'this' so field access resolves
|
|
871
919
|
const thisType = { kind: "user", name: cls.name };
|
|
872
|
-
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType })) };
|
|
920
|
+
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
|
|
873
921
|
const allTypeDecls = [...typeDecls, thisDecl];
|
|
874
|
-
const methods = cls.methods.map(fn => {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
879
|
-
let env = null;
|
|
880
|
-
env = extend(env, "this", thisType);
|
|
881
|
-
for (const p of params)
|
|
882
|
-
env = extend(env, p.name, p.ty);
|
|
883
|
-
const baseCtx = { env, typeDecls: allTypeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
884
|
-
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
885
|
-
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
886
|
-
return {
|
|
887
|
-
name: fn.name, typeParams: fn.typeParams, params, returnTy,
|
|
888
|
-
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
889
|
-
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
890
|
-
isPure: false, // class methods are never pure (they access this)
|
|
891
|
-
body: resolveBlock(fn.body, baseCtx),
|
|
892
|
-
};
|
|
893
|
-
});
|
|
922
|
+
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
|
|
923
|
+
thisBinding: { name: "this", ty: thisType },
|
|
924
|
+
forcePure: false, // class methods are never pure (they access this)
|
|
925
|
+
}));
|
|
894
926
|
return { name: cls.name, fields, methods };
|
|
895
927
|
}
|
|
928
|
+
/** Pre-compute Ty on all TypeDeclInfo fields/variants/aliases.
|
|
929
|
+
* Called once per module so consumers can read field.type instead of re-parsing tsType. */
|
|
930
|
+
function precomputeFieldTypes(typeDecls) {
|
|
931
|
+
for (const d of typeDecls) {
|
|
932
|
+
if (d.fields)
|
|
933
|
+
for (const f of d.fields)
|
|
934
|
+
f.type = parseTsType(f.tsType);
|
|
935
|
+
if (d.variants)
|
|
936
|
+
for (const v of d.variants)
|
|
937
|
+
for (const f of v.fields)
|
|
938
|
+
f.type = parseTsType(f.tsType);
|
|
939
|
+
if (d.aliasOf && !d.aliasOfTy)
|
|
940
|
+
d.aliasOfTy = parseTsType(d.aliasOf);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
896
943
|
export function resolveModule(raw) {
|
|
944
|
+
precomputeFieldTypes(raw.typeDecls);
|
|
897
945
|
const pureFns = computePureFns(raw.functions);
|
|
898
946
|
// Pre-compute function parameter types for optional coercion
|
|
899
947
|
const fnParams = new Map();
|