lemmascript 0.3.0 → 0.3.1
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 +87 -155
- package/tools/dist/extract.js +3 -1
- package/tools/dist/lsc.js +7 -1
- package/tools/dist/resolve.js +218 -178
- package/tools/dist/transform.js +217 -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,99 @@ 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 === "shift")
|
|
297
|
+
return objTy.elem;
|
|
298
|
+
if (fn.field === "push" || fn.field === "concat")
|
|
299
|
+
return objTy;
|
|
300
|
+
if (fn.field === "filter")
|
|
301
|
+
return objTy;
|
|
302
|
+
if (fn.field === "every" || fn.field === "some")
|
|
303
|
+
return { kind: "bool" };
|
|
304
|
+
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
305
|
+
const retTy = args[0].body.length > 0 && args[0].body[0].kind === "return"
|
|
306
|
+
? args[0].body[0].value.ty : { kind: "unknown" };
|
|
307
|
+
return { kind: "array", elem: retTy };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (objTy.kind === "string") {
|
|
311
|
+
if (fn.field === "trim" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
|
|
312
|
+
return { kind: "string" };
|
|
313
|
+
if (fn.field === "includes")
|
|
314
|
+
return { kind: "bool" };
|
|
315
|
+
}
|
|
316
|
+
return { kind: "unknown" };
|
|
317
|
+
}
|
|
206
318
|
// ── Resolve expressions ──────────────────────────────────────
|
|
207
319
|
function resolveExpr(e, ctx) {
|
|
208
320
|
switch (e.kind) {
|
|
209
321
|
case "var":
|
|
322
|
+
if (e.name === "undefined")
|
|
323
|
+
return { kind: "var", name: "undefined", ty: { kind: "void" } };
|
|
210
324
|
return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
|
|
211
325
|
case "num":
|
|
212
326
|
if (!Number.isInteger(e.value))
|
|
@@ -228,15 +342,18 @@ function resolveExpr(e, ctx) {
|
|
|
228
342
|
}
|
|
229
343
|
case "binop": {
|
|
230
344
|
let left = resolveExpr(e.left, ctx);
|
|
231
|
-
// && narrowing: if left is "x !== undefined", narrow x for right side
|
|
345
|
+
// && narrowing: if left is "x !== undefined", narrow x for right side.
|
|
346
|
+
// Field chains are excluded — resolve can't substitute in sub-expressions;
|
|
347
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
232
348
|
let rightCtx = ctx;
|
|
349
|
+
let rawRight = e.right;
|
|
233
350
|
if (e.op === "&&") {
|
|
234
|
-
const narrowed =
|
|
235
|
-
if (narrowed && narrowed.inThen) {
|
|
351
|
+
const narrowed = detectOptionalCheck(e.left, ctx);
|
|
352
|
+
if (narrowed && narrowed.inThen && !narrowed.fieldExpr) {
|
|
236
353
|
rightCtx = withEnv(ctx, extend(ctx.env, narrowed.varName, narrowed.innerTy));
|
|
237
354
|
}
|
|
238
355
|
}
|
|
239
|
-
let right = resolveExpr(
|
|
356
|
+
let right = resolveExpr(rawRight, rightCtx);
|
|
240
357
|
if (e.op === "===" || e.op === "!==") {
|
|
241
358
|
left = coerceStr(left, right.ty);
|
|
242
359
|
right = coerceStr(right, left.ty);
|
|
@@ -263,107 +380,15 @@ function resolveExpr(e, ctx) {
|
|
|
263
380
|
}
|
|
264
381
|
case "call": {
|
|
265
382
|
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
|
|
383
|
+
const rawArgs = inferLambdaParamTypes(fn, e.args);
|
|
384
|
+
// For .push() on a typed array, resolve args with element type context
|
|
285
385
|
let argCtx = ctx;
|
|
286
386
|
if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "push" &&
|
|
287
387
|
fn.obj.ty.elem.kind === "user") {
|
|
288
388
|
argCtx = { ...ctx, returnTy: fn.obj.ty.elem };
|
|
289
389
|
}
|
|
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
|
-
}
|
|
390
|
+
const args = coerceCallArgs(rawArgs.map(a => resolveExpr(a, argCtx)), fn, ctx);
|
|
391
|
+
const ty = inferMethodReturnTy(fn, args, ctx);
|
|
367
392
|
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx) };
|
|
368
393
|
}
|
|
369
394
|
case "index": {
|
|
@@ -391,7 +416,17 @@ function resolveExpr(e, ctx) {
|
|
|
391
416
|
if (decl?.kind === "record") {
|
|
392
417
|
const f = decl.fields?.find(f => f.name === e.field);
|
|
393
418
|
if (f)
|
|
394
|
-
ty =
|
|
419
|
+
ty = f.type;
|
|
420
|
+
}
|
|
421
|
+
// Also resolve fields from discriminated-union variants
|
|
422
|
+
if (ty.kind === "unknown" && decl?.kind === "discriminated-union" && decl.variants) {
|
|
423
|
+
for (const variant of decl.variants) {
|
|
424
|
+
const f = variant.fields.find(f => f.name === e.field);
|
|
425
|
+
if (f) {
|
|
426
|
+
ty = f.type;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
395
430
|
}
|
|
396
431
|
}
|
|
397
432
|
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
@@ -408,15 +443,11 @@ function resolveExpr(e, ctx) {
|
|
|
408
443
|
let value = resolveExpr(f.value, fieldCtx);
|
|
409
444
|
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
410
445
|
if (fieldDecl) {
|
|
411
|
-
const declTy =
|
|
446
|
+
const declTy = fieldDecl.type;
|
|
412
447
|
value = coerceStr(value, declTy);
|
|
413
448
|
// Coerce non-optional to optional: wrap in Some (only when value type is concrete)
|
|
414
449
|
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
|
-
};
|
|
450
|
+
value = wrapSome(value, declTy);
|
|
420
451
|
}
|
|
421
452
|
}
|
|
422
453
|
return { name: f.name, value };
|
|
@@ -461,13 +492,11 @@ function resolveExpr(e, ctx) {
|
|
|
461
492
|
}
|
|
462
493
|
case "conditional": {
|
|
463
494
|
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
495
|
let narrowedVar;
|
|
468
496
|
let narrowedExprResolved;
|
|
469
497
|
let thenCtx = ctx;
|
|
470
498
|
let rawThen = e.then;
|
|
499
|
+
// Phase 1: Optional truthiness — cond itself is optional (e.g. opt ? X : Y)
|
|
471
500
|
if (cond.ty.kind === "optional") {
|
|
472
501
|
const innerTy = cond.ty.inner;
|
|
473
502
|
if (e.cond.kind === "var") {
|
|
@@ -480,24 +509,18 @@ function resolveExpr(e, ctx) {
|
|
|
480
509
|
thenCtx = withEnv(ctx, extend(ctx.env, narrowedVar, innerTy));
|
|
481
510
|
}
|
|
482
511
|
}
|
|
483
|
-
// Explicit
|
|
484
|
-
//
|
|
512
|
+
// Phase 2: Explicit check — v !== undefined (simple vars, field chains,
|
|
513
|
+
// complex expressions all handled uniformly by detectOptionalCheck)
|
|
485
514
|
if (!narrowedVar) {
|
|
486
|
-
const narrowed =
|
|
515
|
+
const narrowed = detectOptionalCheck(e.cond, ctx)
|
|
516
|
+
// Phase 3: && with optional check — (v !== undefined && ...) ? ... : ...
|
|
517
|
+
?? (e.cond.kind === "binop" && e.cond.op === "&&" ? detectOptionalCheck(e.cond.left, ctx) : null);
|
|
487
518
|
if (narrowed && narrowed.inThen) {
|
|
488
519
|
narrowedVar = narrowed.varName;
|
|
489
520
|
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));
|
|
521
|
+
if (narrowed.fieldExpr) {
|
|
522
|
+
narrowedExprResolved = narrowed.narrowedExpr ?? resolveExpr(narrowed.fieldExpr, ctx);
|
|
523
|
+
rawThen = substituteRawExpr(e.then, narrowed.fieldExpr, { kind: "var", name: narrowed.varName });
|
|
501
524
|
}
|
|
502
525
|
}
|
|
503
526
|
}
|
|
@@ -506,8 +529,16 @@ function resolveExpr(e, ctx) {
|
|
|
506
529
|
then_ = coerceStr(then_, else_.ty);
|
|
507
530
|
else_ = coerceStr(else_, then_.ty);
|
|
508
531
|
let ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
509
|
-
// When
|
|
510
|
-
if (
|
|
532
|
+
// When one branch is undefined, result is optional
|
|
533
|
+
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
534
|
+
ty = { kind: "optional", inner: else_.ty };
|
|
535
|
+
}
|
|
536
|
+
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
537
|
+
ty = { kind: "optional", inner: then_.ty };
|
|
538
|
+
}
|
|
539
|
+
// When narrowedExpr is set AND a branch is void, the match produces Optional
|
|
540
|
+
const hasVoidBranch = then_.ty.kind === "void" || else_.ty.kind === "void";
|
|
541
|
+
if (narrowedExprResolved && hasVoidBranch && ty.kind !== "optional") {
|
|
511
542
|
ty = { kind: "optional", inner: ty };
|
|
512
543
|
}
|
|
513
544
|
return { kind: "conditional", cond, then: then_, else: else_, ty, narrowedVar, narrowedExpr: narrowedExprResolved };
|
|
@@ -547,6 +578,15 @@ function resolveBlock(stmts, ctx) {
|
|
|
547
578
|
const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
|
|
548
579
|
result.push(typed);
|
|
549
580
|
env = nextEnv;
|
|
581
|
+
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
582
|
+
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
583
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
584
|
+
if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) {
|
|
585
|
+
const narrowed = detectOptionalCheck(s.cond, withEnv(ctx, env));
|
|
586
|
+
if (narrowed && !narrowed.inThen && !narrowed.fieldExpr) {
|
|
587
|
+
env = extend(env, narrowed.varName, narrowed.innerTy);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
550
590
|
}
|
|
551
591
|
return result;
|
|
552
592
|
}
|
|
@@ -569,11 +609,7 @@ function resolveStmt(s, ctx) {
|
|
|
569
609
|
// Skip if already optional, void, or undefined (which maps to None)
|
|
570
610
|
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
571
611
|
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
|
-
};
|
|
612
|
+
value = wrapSome(value, ctx.returnTy);
|
|
577
613
|
}
|
|
578
614
|
return [{ kind: "return", value }, ctx.env];
|
|
579
615
|
}
|
|
@@ -584,23 +620,20 @@ function resolveStmt(s, ctx) {
|
|
|
584
620
|
case "expr":
|
|
585
621
|
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
586
622
|
case "if": {
|
|
587
|
-
// Narrow optional<T> → T when checking !== undefined or undefined
|
|
623
|
+
// Narrow optional<T> → T when checking !== undefined or undefined !==.
|
|
624
|
+
// Also checks left side of && conditions: if (x !== undefined && ...) { ... }
|
|
625
|
+
// Field chains are excluded — resolve can't substitute in statement bodies;
|
|
626
|
+
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
588
627
|
let thenCtx = ctx, elseCtx = ctx;
|
|
589
|
-
const narrowed =
|
|
590
|
-
|
|
628
|
+
const narrowed = detectOptionalCheck(s.cond, ctx)
|
|
629
|
+
?? (s.cond.kind === "binop" && s.cond.op === "&&" ? detectOptionalCheck(s.cond.left, ctx) : null);
|
|
630
|
+
if (narrowed && !narrowed.fieldExpr) {
|
|
591
631
|
const env = extend(ctx.env, narrowed.varName, narrowed.innerTy);
|
|
592
632
|
if (narrowed.inThen)
|
|
593
633
|
thenCtx = withEnv(ctx, env);
|
|
594
634
|
else
|
|
595
635
|
elseCtx = withEnv(ctx, env);
|
|
596
636
|
}
|
|
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
637
|
return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
605
638
|
}
|
|
606
639
|
case "while": {
|
|
@@ -847,21 +880,28 @@ function containsReturn(stmts) {
|
|
|
847
880
|
return false;
|
|
848
881
|
}
|
|
849
882
|
// ── Resolve function / module ────────────────────────────────
|
|
850
|
-
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map()) {
|
|
883
|
+
function resolveFunction(fn, typeDecls, pureFns, fnParams = new Map(), opts) {
|
|
851
884
|
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
852
885
|
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
853
886
|
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
854
887
|
let env = null;
|
|
888
|
+
if (opts?.thisBinding)
|
|
889
|
+
env = extend(env, opts.thisBinding.name, opts.thisBinding.ty);
|
|
855
890
|
for (const p of params)
|
|
856
891
|
env = extend(env, p.name, p.ty);
|
|
857
892
|
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, fnParams, inSpec: false, inLambda: false };
|
|
858
893
|
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
859
894
|
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
895
|
+
// Apply type parameter constraints from //@ type T (==) annotations
|
|
896
|
+
const typeParams = fn.typeParams.map(tp => {
|
|
897
|
+
const constraint = overrides.get(tp);
|
|
898
|
+
return constraint ? `${tp}${constraint}` : tp;
|
|
899
|
+
});
|
|
860
900
|
return {
|
|
861
|
-
name: fn.name, typeParams
|
|
901
|
+
name: fn.name, typeParams, params, returnTy,
|
|
862
902
|
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
863
903
|
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
864
|
-
isPure: pureFns.has(fn.name),
|
|
904
|
+
isPure: opts?.forcePure !== undefined ? opts.forcePure : pureFns.has(fn.name),
|
|
865
905
|
body: resolveBlock(fn.body, baseCtx),
|
|
866
906
|
};
|
|
867
907
|
}
|
|
@@ -869,31 +909,31 @@ function resolveClass(cls, typeDecls, pureFns, fnParams = new Map()) {
|
|
|
869
909
|
const fields = cls.fields.map(f => ({ name: f.name, ty: parseTsType(f.tsType) }));
|
|
870
910
|
// Create a synthetic record type for 'this' so field access resolves
|
|
871
911
|
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 })) };
|
|
912
|
+
const thisDecl = { name: cls.name, kind: "record", fields: cls.fields.map(f => ({ name: f.name, tsType: f.tsType, type: parseTsType(f.tsType) })) };
|
|
873
913
|
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
|
-
});
|
|
914
|
+
const methods = cls.methods.map(fn => resolveFunction(fn, allTypeDecls, pureFns, fnParams, {
|
|
915
|
+
thisBinding: { name: "this", ty: thisType },
|
|
916
|
+
forcePure: false, // class methods are never pure (they access this)
|
|
917
|
+
}));
|
|
894
918
|
return { name: cls.name, fields, methods };
|
|
895
919
|
}
|
|
920
|
+
/** Pre-compute Ty on all TypeDeclInfo fields/variants/aliases.
|
|
921
|
+
* Called once per module so consumers can read field.type instead of re-parsing tsType. */
|
|
922
|
+
function precomputeFieldTypes(typeDecls) {
|
|
923
|
+
for (const d of typeDecls) {
|
|
924
|
+
if (d.fields)
|
|
925
|
+
for (const f of d.fields)
|
|
926
|
+
f.type = parseTsType(f.tsType);
|
|
927
|
+
if (d.variants)
|
|
928
|
+
for (const v of d.variants)
|
|
929
|
+
for (const f of v.fields)
|
|
930
|
+
f.type = parseTsType(f.tsType);
|
|
931
|
+
if (d.aliasOf && !d.aliasOfTy)
|
|
932
|
+
d.aliasOfTy = parseTsType(d.aliasOf);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
896
935
|
export function resolveModule(raw) {
|
|
936
|
+
precomputeFieldTypes(raw.typeDecls);
|
|
897
937
|
const pureFns = computePureFns(raw.functions);
|
|
898
938
|
// Pre-compute function parameter types for optional coercion
|
|
899
939
|
const fnParams = new Map();
|