lemmascript 0.5.18 → 0.5.20
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 +1 -1
- package/package.json +1 -1
- package/tools/dist/autohavoc.js +2 -0
- package/tools/dist/builtins.js +125 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +228 -37
- package/tools/dist/extract.js +175 -37
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +29 -18
- package/tools/dist/lsc.js +53 -5
- package/tools/dist/names.js +10 -6
- package/tools/dist/narrow.js +296 -677
- package/tools/dist/peephole.js +12 -94
- package/tools/dist/rawir.js +15 -1
- package/tools/dist/resolve.js +268 -249
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +411 -131
- package/tools/dist/typedecls.js +59 -0
package/tools/dist/resolve.js
CHANGED
|
@@ -8,6 +8,9 @@ import { isBigInt, tyEqual, isTerminatorKind } from "./typedir.js";
|
|
|
8
8
|
import { parseTsType, tyToCanonical } from "./types.js";
|
|
9
9
|
import { parseExpr } from "./specparser.js";
|
|
10
10
|
import { freshName } from "./names.js";
|
|
11
|
+
import { recognizeBuiltin, builtinSpec } from "./builtins.js";
|
|
12
|
+
import { declOf, declOfKind, declOfDotted, declOfTy, tyBaseName } from "./typedecls.js";
|
|
13
|
+
import { presentFact, variantFact } from "./condition-facts.js";
|
|
11
14
|
function lookup(env, name) {
|
|
12
15
|
if (!env)
|
|
13
16
|
return undefined;
|
|
@@ -52,10 +55,36 @@ function resolveTsType(tsType, overrides, varName) {
|
|
|
52
55
|
}
|
|
53
56
|
return parseTsType(tsType);
|
|
54
57
|
}
|
|
55
|
-
/**
|
|
58
|
+
/** Infer a conditional's result type after its branches have been resolved or
|
|
59
|
+
* contextually coerced. A void branch is the source-level null/undefined arm. */
|
|
60
|
+
function conditionalResultTy(thenTy, elseTy) {
|
|
61
|
+
let ty = thenTy.kind !== "unknown" ? thenTy : elseTy;
|
|
62
|
+
if (thenTy.kind === "void" && elseTy.kind !== "void" && elseTy.kind !== "unknown") {
|
|
63
|
+
ty = { kind: "optional", inner: elseTy };
|
|
64
|
+
}
|
|
65
|
+
else if (elseTy.kind === "void" && thenTy.kind !== "void" && thenTy.kind !== "unknown") {
|
|
66
|
+
ty = { kind: "optional", inner: thenTy };
|
|
67
|
+
}
|
|
68
|
+
else if (thenTy.kind === "optional" && elseTy.kind !== "optional" && elseTy.kind !== "unknown") {
|
|
69
|
+
ty = thenTy;
|
|
70
|
+
}
|
|
71
|
+
else if (elseTy.kind === "optional" && thenTy.kind !== "optional" && thenTy.kind !== "unknown") {
|
|
72
|
+
ty = elseTy;
|
|
73
|
+
}
|
|
74
|
+
return ty;
|
|
75
|
+
}
|
|
76
|
+
/** Contextually type string literals as constructors. Ternary branches inherit
|
|
77
|
+
* the ternary's target, and an optional target contributes its payload type —
|
|
78
|
+
* the caller adds the Some wrapper only after the payload has been coerced. */
|
|
56
79
|
function coerceStr(expr, targetTy) {
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
const payloadTy = targetTy.kind === "optional" ? targetTy.inner : targetTy;
|
|
81
|
+
if (expr.kind === "str" && payloadTy.kind === "user")
|
|
82
|
+
return { ...expr, ty: payloadTy };
|
|
83
|
+
if (expr.kind === "conditional") {
|
|
84
|
+
const then_ = coerceStr(expr.then, payloadTy);
|
|
85
|
+
const else_ = coerceStr(expr.else, payloadTy);
|
|
86
|
+
return { ...expr, then: then_, else: else_, ty: conditionalResultTy(then_.ty, else_.ty) };
|
|
87
|
+
}
|
|
59
88
|
return expr;
|
|
60
89
|
}
|
|
61
90
|
// ── Helpers ──────────────────────────────────────────────────
|
|
@@ -68,7 +97,7 @@ function wrapSome(value, optionalTy) {
|
|
|
68
97
|
}
|
|
69
98
|
/** Find the synth array-union TypeDecl named `name` (discriminant `__isArray__`). */
|
|
70
99
|
function findSynthArrayUnion(name, typeDecls) {
|
|
71
|
-
const decl = typeDecls
|
|
100
|
+
const decl = declOf(typeDecls, name);
|
|
72
101
|
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__")
|
|
73
102
|
return decl;
|
|
74
103
|
return null;
|
|
@@ -81,6 +110,7 @@ function findSynthArrayUnion(name, typeDecls) {
|
|
|
81
110
|
* Returns `value` unchanged if no coercion applies (types already match,
|
|
82
111
|
* source is unknown, or no rule matches). */
|
|
83
112
|
function coerceToTargetTy(value, targetTy, typeDecls) {
|
|
113
|
+
value = coerceStr(value, targetTy);
|
|
84
114
|
if (value.ty.kind === "unknown" || value.ty.kind === "void")
|
|
85
115
|
return value;
|
|
86
116
|
if (targetTy.kind === "optional" && value.ty.kind !== "optional") {
|
|
@@ -103,68 +133,37 @@ function coerceToTargetTy(value, targetTy, typeDecls) {
|
|
|
103
133
|
}
|
|
104
134
|
return value;
|
|
105
135
|
}
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
* Does NOT recurse into `&&`. */
|
|
114
|
-
function detectOptionalCheck(cond, ctx) {
|
|
115
|
-
// `!v` where v is optional — same shape as `v === undefined` (inThen: false).
|
|
116
|
-
if (cond.kind === "unop" && cond.op === "!") {
|
|
117
|
-
const inner = classifyOptExpr(cond.expr, ctx);
|
|
118
|
-
return inner ? { ...inner, inThen: false } : null;
|
|
119
|
-
}
|
|
120
|
-
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
121
|
-
// Bare optional truthiness: `if (v)` where v: T | undefined — same as `v !== undefined`.
|
|
122
|
-
const inner = classifyOptExpr(cond, ctx);
|
|
123
|
-
return inner ? { ...inner, inThen: true } : null;
|
|
124
|
-
}
|
|
125
|
-
// Identify the expression being checked against undefined
|
|
126
|
-
let optExpr = null;
|
|
127
|
-
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
128
|
-
optExpr = cond.left;
|
|
129
|
-
if (cond.left.kind === "var" && cond.left.name === "undefined")
|
|
130
|
-
optExpr = cond.right;
|
|
131
|
-
if (!optExpr)
|
|
132
|
-
return null;
|
|
133
|
-
const inner = classifyOptExpr(optExpr, ctx);
|
|
134
|
-
return inner ? { ...inner, inThen: cond.op === "!==" } : null;
|
|
135
|
-
}
|
|
136
|
-
/** Classify an expression as a simple var or field-chain optional, returning
|
|
137
|
-
* the shape needed by detectOptionalCheck (sans inThen). */
|
|
138
|
-
function classifyOptExpr(e, ctx) {
|
|
139
|
-
if (e.kind === "var") {
|
|
140
|
-
const ty = lookup(ctx.env, e.name);
|
|
141
|
-
if (!ty || ty.kind !== "optional")
|
|
142
|
-
return null;
|
|
143
|
-
return { varName: e.name, innerTy: ty.inner };
|
|
144
|
-
}
|
|
145
|
-
if (e.kind === "result") {
|
|
146
|
-
const ty = lookup(ctx.env, "\\result");
|
|
147
|
-
if (!ty || ty.kind !== "optional")
|
|
148
|
-
return null;
|
|
149
|
-
return { varName: "\\result", innerTy: ty.inner };
|
|
136
|
+
/** A negated presence check on a bare var (`v === undefined` / `!v`), via
|
|
137
|
+
* the shared condition analyzer (§4) on the *resolved* condition. Narrows
|
|
138
|
+
* the else-branch (or the rest of the block, after an early return). */
|
|
139
|
+
function negatedVarPresence(cond) {
|
|
140
|
+
const f = presentFact(cond);
|
|
141
|
+
if (f && f.negated && f.scrutinee.kind === "var") {
|
|
142
|
+
return { varName: f.scrutinee.name, innerTy: f.innerTy };
|
|
150
143
|
}
|
|
151
|
-
|
|
152
|
-
if (resolved.ty.kind !== "optional")
|
|
153
|
-
return null;
|
|
154
|
-
return { varName: "", innerTy: resolved.ty.inner, fieldExpr: e };
|
|
144
|
+
return null;
|
|
155
145
|
}
|
|
156
146
|
/** Collect all optional narrowings from an early-return condition.
|
|
157
147
|
* Handles single checks (x === undefined) and compound || chains
|
|
158
148
|
* (x === undefined || y === undefined). */
|
|
159
|
-
function collectEarlyReturnNarrowings(cond
|
|
149
|
+
function collectEarlyReturnNarrowings(cond) {
|
|
160
150
|
if (cond.kind === "binop" && cond.op === "||") {
|
|
161
|
-
return [...collectEarlyReturnNarrowings(cond.left
|
|
151
|
+
return [...collectEarlyReturnNarrowings(cond.left), ...collectEarlyReturnNarrowings(cond.right)];
|
|
162
152
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
153
|
+
const n = negatedVarPresence(cond);
|
|
154
|
+
return n ? [n] : [];
|
|
155
|
+
}
|
|
156
|
+
/** Negated discriminant check (`path.kind !== "lit"`) on a var or field path.
|
|
157
|
+
* After `if (path.kind !== "lit") return`, the rest of the block knows the
|
|
158
|
+
* path is that variant. */
|
|
159
|
+
function negatedVariantCheck(cond) {
|
|
160
|
+
if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
|
|
161
|
+
cond.left.kind === "field" && cond.left.isDiscriminant && cond.left.obj.ty.kind === "user") {
|
|
162
|
+
const path = asTExprAccessPath(cond.left.obj);
|
|
163
|
+
if (path)
|
|
164
|
+
return { path, narrowedTy: cond.left.obj.ty, variant: cond.right.value };
|
|
166
165
|
}
|
|
167
|
-
return
|
|
166
|
+
return null;
|
|
168
167
|
}
|
|
169
168
|
/** TExpr → AccessPath. Counterpart to `asRawAccessPath` for resolved trees.
|
|
170
169
|
* Used by `extractInAtoms` when pulling atoms out of typed spec expressions. */
|
|
@@ -222,23 +221,36 @@ function withInAtoms(ctx, atoms) {
|
|
|
222
221
|
return ctx;
|
|
223
222
|
return { ...ctx, narrowedIndices: [...existing, ...added] };
|
|
224
223
|
}
|
|
225
|
-
/** Walk an `&&` chain of `e !== undefined` checks
|
|
226
|
-
*
|
|
227
|
-
*
|
|
224
|
+
/** Walk an `&&` chain of `e !== undefined` checks on the *resolved*
|
|
225
|
+
* condition, returning a Ctx with all narrowings applied. Earlier checks
|
|
226
|
+
* are in scope for later checks (the right conjunct was already resolved
|
|
227
|
+
* under the left's narrowings by the `&&` case of resolveExpr). Consults
|
|
228
|
+
* the shared condition analyzer (§4): a positive presence fact on a bare
|
|
229
|
+
* var extends the env; on a pure field path it extends `narrowedPaths`. */
|
|
228
230
|
function collectAndChainNarrowings(cond, ctx) {
|
|
229
231
|
if (cond.kind === "binop" && cond.op === "&&") {
|
|
230
232
|
const leftCtx = collectAndChainNarrowings(cond.left, ctx);
|
|
231
233
|
return collectAndChainNarrowings(cond.right, leftCtx);
|
|
232
234
|
}
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
+
const f = presentFact(cond);
|
|
236
|
+
if (f && !f.negated) {
|
|
237
|
+
if (f.scrutinee.kind === "var") {
|
|
238
|
+
return withEnv(ctx, extend(ctx.env, f.scrutinee.name, f.innerTy));
|
|
239
|
+
}
|
|
240
|
+
const path = asTExprAccessPath(f.scrutinee);
|
|
241
|
+
if (path) {
|
|
242
|
+
return { ...ctx, narrowedPaths: [...ctx.narrowedPaths, { path, narrowedTy: f.innerTy }] };
|
|
243
|
+
}
|
|
235
244
|
return ctx;
|
|
236
|
-
if (!n.fieldExpr) {
|
|
237
|
-
return withEnv(ctx, extend(ctx.env, n.varName, n.innerTy));
|
|
238
245
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
// Positive discriminant check (`x.kind === "bool"`): record the variant so
|
|
247
|
+
// field reads on the path resolve against that variant's field types.
|
|
248
|
+
const vf = variantFact(cond, { decls: ctx.typeDecls, oc: { n: 0 } });
|
|
249
|
+
if (vf) {
|
|
250
|
+
const path = asTExprAccessPath(vf.scrutinee);
|
|
251
|
+
if (path) {
|
|
252
|
+
return { ...ctx, narrowedPaths: [...ctx.narrowedPaths, { path, narrowedTy: vf.scrutinee.ty, variant: vf.variant }] };
|
|
253
|
+
}
|
|
242
254
|
}
|
|
243
255
|
return ctx;
|
|
244
256
|
}
|
|
@@ -247,16 +259,10 @@ function isRefMutableInTS(ty) {
|
|
|
247
259
|
return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
|
|
248
260
|
}
|
|
249
261
|
function findDecl(ctx, name) {
|
|
250
|
-
|
|
251
|
-
if (direct)
|
|
252
|
-
return direct;
|
|
253
|
-
// Dotted names (e.g. `Agent.Info`, `Permission.Ruleset`): fall back to the
|
|
262
|
+
// Dotted names (e.g. `Agent.Info`, `Permission.Ruleset`) fall back to the
|
|
254
263
|
// last segment, so `//@ declare-type Info { ... }` matches a reference to
|
|
255
264
|
// `Agent.Info` without forcing the user to repeat the namespace.
|
|
256
|
-
|
|
257
|
-
if (dotIdx >= 0)
|
|
258
|
-
return ctx.typeDecls.find(d => d.name === name.slice(dotIdx + 1));
|
|
259
|
-
return undefined;
|
|
265
|
+
return declOfDotted(ctx.typeDecls, name);
|
|
260
266
|
}
|
|
261
267
|
/** Expand alias-kind typeDecls when the alias target is structural (array,
|
|
262
268
|
* map, set, optional, or another user type). Primitive-typed aliases like
|
|
@@ -266,11 +272,7 @@ function expandAlias(ty, typeDecls, seen = new Set()) {
|
|
|
266
272
|
if (ty.kind === "user") {
|
|
267
273
|
if (seen.has(ty.name))
|
|
268
274
|
return ty;
|
|
269
|
-
|
|
270
|
-
if (!decl && ty.name.includes(".")) {
|
|
271
|
-
const tail = ty.name.slice(ty.name.lastIndexOf(".") + 1);
|
|
272
|
-
decl = typeDecls.find(d => d.name === tail);
|
|
273
|
-
}
|
|
275
|
+
const decl = declOfDotted(typeDecls, ty.name);
|
|
274
276
|
if (decl?.kind === "alias" && decl.aliasOfTy) {
|
|
275
277
|
const target = decl.aliasOfTy;
|
|
276
278
|
if (target.kind === "array" || target.kind === "map" || target.kind === "set" || target.kind === "optional" || target.kind === "user") {
|
|
@@ -305,11 +307,7 @@ function refEqHazard(ty, typeDecls) {
|
|
|
305
307
|
if (ty.kind === "array" || ty.kind === "map" || ty.kind === "set" || ty.kind === "tuple")
|
|
306
308
|
return true;
|
|
307
309
|
if (ty.kind === "user") {
|
|
308
|
-
|
|
309
|
-
if (!decl && ty.name.includes(".")) {
|
|
310
|
-
const tail = ty.name.slice(ty.name.lastIndexOf(".") + 1);
|
|
311
|
-
decl = typeDecls.find(d => d.name === tail);
|
|
312
|
-
}
|
|
310
|
+
const decl = declOfDotted(typeDecls, ty.name);
|
|
313
311
|
if (!decl)
|
|
314
312
|
return true; // generic type parameter / unknown → assume reference
|
|
315
313
|
if (decl.kind === "string-union")
|
|
@@ -348,35 +346,44 @@ function isUnmodeledTy(ty, typeDecls) {
|
|
|
348
346
|
return isUnmodeledTy(ty.elem, typeDecls);
|
|
349
347
|
if (ty.kind === "map")
|
|
350
348
|
return isUnmodeledTy(ty.key, typeDecls) || isUnmodeledTy(ty.value, typeDecls);
|
|
351
|
-
if (ty.kind === "user")
|
|
352
|
-
|
|
353
|
-
return !typeDecls.some(d => d.name === base);
|
|
354
|
-
}
|
|
349
|
+
if (ty.kind === "user")
|
|
350
|
+
return declOfTy(typeDecls, ty) === undefined;
|
|
355
351
|
return false;
|
|
356
352
|
}
|
|
357
353
|
/** A `user` type that resolves to a string-union declare-type — runs as a plain
|
|
358
354
|
* string at runtime, so it's a refinement of `string`, not an opaque blob. */
|
|
359
355
|
function isStringUnionTy(ty, typeDecls) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
356
|
+
return declOfTy(typeDecls, ty)?.kind === "string-union";
|
|
357
|
+
}
|
|
358
|
+
/** Whether ts-morph widened a string-union initializer to the declared string
|
|
359
|
+
* shape. Inferred optional locals need the same rescue as bare locals:
|
|
360
|
+
* `Option<string>` from TS must not erase an `Option<Color>` initializer. */
|
|
361
|
+
function isWidenedStringUnionTy(declTy, initTy, typeDecls) {
|
|
362
|
+
if (declTy.kind === "string" && isStringUnionTy(initTy, typeDecls))
|
|
363
|
+
return true;
|
|
364
|
+
if (declTy.kind === "optional" && initTy.kind === "optional") {
|
|
365
|
+
return isWidenedStringUnionTy(declTy.inner, initTy.inner, typeDecls);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
364
368
|
}
|
|
365
369
|
/** Infer quantifier variable type from usage in body.
|
|
366
370
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
367
371
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
368
372
|
function inferQuantVarType(varName, body, ctx) {
|
|
369
|
-
// Look for
|
|
373
|
+
// Look for membership/lookup builtins (map.has(k), map.get(k),
|
|
374
|
+
// array.includes(k) — registry `argIsKey`) where k is our variable
|
|
370
375
|
if (body.kind === "call" && body.fn.kind === "field" &&
|
|
371
|
-
(body.fn.field === "has" || body.fn.field === "get" || body.fn.field === "includes") &&
|
|
372
376
|
body.args.length === 1 && body.args[0].kind === "var" && body.args[0].name === varName) {
|
|
373
377
|
const objTy = lookup(ctx.env, body.fn.obj.kind === "var" ? body.fn.obj.name : "");
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
const id = objTy ? recognizeBuiltin(objTy, body.fn.field) : null;
|
|
379
|
+
if (id && builtinSpec(id).argIsKey && objTy) {
|
|
380
|
+
if (objTy.kind === "map")
|
|
381
|
+
return objTy.key;
|
|
382
|
+
if (objTy.kind === "set")
|
|
383
|
+
return objTy.elem;
|
|
384
|
+
if (objTy.kind === "array")
|
|
385
|
+
return objTy.elem;
|
|
386
|
+
}
|
|
380
387
|
}
|
|
381
388
|
// Recurse into subexpressions
|
|
382
389
|
if (body.kind === "binop") {
|
|
@@ -430,6 +437,8 @@ function classifyCall(fn, ctx) {
|
|
|
430
437
|
return "pure";
|
|
431
438
|
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray")
|
|
432
439
|
return "pure";
|
|
440
|
+
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode")
|
|
441
|
+
return "pure";
|
|
433
442
|
if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
|
|
434
443
|
return "spec-pure";
|
|
435
444
|
// Bare-name `//@ extern` declarations are emitted as `function {:axiom}` —
|
|
@@ -438,6 +447,8 @@ function classifyCall(fn, ctx) {
|
|
|
438
447
|
// become multi-statement, illegal in Dafny).
|
|
439
448
|
if (fn.kind === "var" && ctx.externs.has(fn.name))
|
|
440
449
|
return "pure";
|
|
450
|
+
if (fn.kind === "var" && lookup(ctx.env, fn.name)?.kind === "fn")
|
|
451
|
+
return "pure";
|
|
441
452
|
if (fn.kind === "var" && ctx.inSpec) {
|
|
442
453
|
// Not a known pure function — could be external (Lean-defined spec helper).
|
|
443
454
|
// Pass through as "pure" and let Lean catch any errors.
|
|
@@ -470,8 +481,11 @@ function tyToTsStr(ty) {
|
|
|
470
481
|
return undefined;
|
|
471
482
|
}
|
|
472
483
|
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
484
|
+
const hofShape = fn.kind === "field"
|
|
485
|
+
? (id => id ? builtinSpec(id).hof?.shape : undefined)(recognizeBuiltin(fn.obj.ty, fn.field))
|
|
486
|
+
: undefined;
|
|
473
487
|
// sort's comparator takes two params, both the element type.
|
|
474
|
-
if (
|
|
488
|
+
if (hofShape === "comparator" && fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
475
489
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 1) {
|
|
476
490
|
const tsType = tyToTsStr(fn.obj.ty.elem);
|
|
477
491
|
if (tsType) {
|
|
@@ -481,7 +495,7 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
481
495
|
}
|
|
482
496
|
}
|
|
483
497
|
// reduce's callback is (acc, elem): acc from the init arg's type, elem from the array.
|
|
484
|
-
if (
|
|
498
|
+
if (hofShape === "reduce" && fn.kind === "field" && fn.obj.ty.kind === "array" && ctx &&
|
|
485
499
|
rawArgs.length >= 2 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 2) {
|
|
486
500
|
const accTs = tyToTsStr(resolveExpr(rawArgs[1], ctx).ty);
|
|
487
501
|
const elemTs = tyToTsStr(fn.obj.ty.elem);
|
|
@@ -491,8 +505,7 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
491
505
|
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
492
506
|
}
|
|
493
507
|
}
|
|
494
|
-
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
495
|
-
["map", "filter", "every", "some", "find", "findLast", "findIndex", "findLastIndex"].includes(fn.field) &&
|
|
508
|
+
if (hofShape === "unary" && fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
496
509
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
497
510
|
rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
|
|
498
511
|
const elemTy = fn.obj.ty.elem;
|
|
@@ -513,7 +526,7 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
513
526
|
return a;
|
|
514
527
|
let pTy = paramTys[i];
|
|
515
528
|
if (pTy.kind === "user") {
|
|
516
|
-
const decl = ctx.typeDecls
|
|
529
|
+
const decl = declOf(ctx.typeDecls, pTy.name);
|
|
517
530
|
if (decl?.kind === "alias" && decl.aliasOfTy)
|
|
518
531
|
pTy = decl.aliasOfTy;
|
|
519
532
|
else if (decl?.kind === "alias" && decl.aliasOf)
|
|
@@ -542,7 +555,7 @@ function isDefinedCheckRawLambda(raw) {
|
|
|
542
555
|
const isUndef = (x) => x.kind === "var" && x.name === "undefined";
|
|
543
556
|
return (isParam(body.left) && isUndef(body.right)) || (isParam(body.right) && isUndef(body.left));
|
|
544
557
|
}
|
|
545
|
-
/** Coerce call arguments
|
|
558
|
+
/** Coerce call arguments to their declared parameter slots and pad missing optional args. */
|
|
546
559
|
function coerceCallArgs(args, fn, ctx) {
|
|
547
560
|
if (fn.kind !== "var" || !ctx.fnParams.has(fn.name))
|
|
548
561
|
return args;
|
|
@@ -550,11 +563,7 @@ function coerceCallArgs(args, fn, ctx) {
|
|
|
550
563
|
args = args.map((a, i) => {
|
|
551
564
|
if (i >= paramTys.length)
|
|
552
565
|
return a;
|
|
553
|
-
|
|
554
|
-
if (a.ty.kind !== "optional" && paramTys[i].kind === "optional") {
|
|
555
|
-
return wrapSome(a, paramTys[i]);
|
|
556
|
-
}
|
|
557
|
-
return a;
|
|
566
|
+
return coerceToTargetTy(a, paramTys[i], ctx.typeDecls);
|
|
558
567
|
});
|
|
559
568
|
// Pad missing optional args with None
|
|
560
569
|
for (let i = args.length; i < paramTys.length; i++) {
|
|
@@ -573,6 +582,11 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
573
582
|
if (fn.obj.kind === "var" && fn.obj.name === "Array" && fn.field === "isArray") {
|
|
574
583
|
return { kind: "bool" };
|
|
575
584
|
}
|
|
585
|
+
// `String.fromCharCode(n)` is the inverse of `s.charCodeAt(i)`: an int code
|
|
586
|
+
// point in, a one-character string out.
|
|
587
|
+
if (fn.obj.kind === "var" && fn.obj.name === "String" && fn.field === "fromCharCode") {
|
|
588
|
+
return { kind: "string" };
|
|
589
|
+
}
|
|
576
590
|
// Math.* numeric builtins: abs/min/max preserve the operand's numeric type
|
|
577
591
|
// (real if any operand is real); floor/ceil/round/trunc return an integer.
|
|
578
592
|
if (fn.obj.kind === "var" && fn.obj.name === "Math") {
|
|
@@ -584,68 +598,9 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
584
598
|
return { kind: "int" };
|
|
585
599
|
}
|
|
586
600
|
const objTy = fn.obj.ty;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (fn.field === "has")
|
|
591
|
-
return { kind: "bool" };
|
|
592
|
-
if (fn.field === "set" || fn.field === "delete")
|
|
593
|
-
return objTy;
|
|
594
|
-
}
|
|
595
|
-
else if (objTy.kind === "set") {
|
|
596
|
-
if (fn.field === "has")
|
|
597
|
-
return { kind: "bool" };
|
|
598
|
-
if (fn.field === "add" || fn.field === "delete")
|
|
599
|
-
return objTy;
|
|
600
|
-
}
|
|
601
|
-
else if (objTy.kind === "array") {
|
|
602
|
-
if (fn.field === "includes")
|
|
603
|
-
return { kind: "bool" };
|
|
604
|
-
if (fn.field === "indexOf")
|
|
605
|
-
return { kind: "int" };
|
|
606
|
-
if (fn.field === "shift")
|
|
607
|
-
return objTy.elem;
|
|
608
|
-
if (fn.field === "pop")
|
|
609
|
-
return { kind: "optional", inner: objTy.elem };
|
|
610
|
-
if (fn.field === "push" || fn.field === "unshift" || fn.field === "concat")
|
|
611
|
-
return objTy;
|
|
612
|
-
if (fn.field === "sort")
|
|
613
|
-
return objTy;
|
|
614
|
-
if (fn.field === "filter")
|
|
615
|
-
return objTy;
|
|
616
|
-
if (fn.field === "every" || fn.field === "some")
|
|
617
|
-
return { kind: "bool" };
|
|
618
|
-
if (fn.field === "reduce" && args.length === 2)
|
|
619
|
-
return args[1].ty;
|
|
620
|
-
if (fn.field === "find" || fn.field === "findLast")
|
|
621
|
-
return { kind: "optional", inner: objTy.elem };
|
|
622
|
-
if (fn.field === "findIndex" || fn.field === "findLastIndex")
|
|
623
|
-
return { kind: "int" };
|
|
624
|
-
if (fn.field === "flat" && objTy.elem.kind === "array")
|
|
625
|
-
return { kind: "array", elem: objTy.elem.elem };
|
|
626
|
-
if (fn.field === "slice")
|
|
627
|
-
return objTy;
|
|
628
|
-
if (fn.field === "join" && objTy.elem.kind === "string")
|
|
629
|
-
return { kind: "string" };
|
|
630
|
-
if (fn.field === "map" && args.length >= 1 && args[0].kind === "lambda") {
|
|
631
|
-
const lam = args[0];
|
|
632
|
-
// Prefer the lambda's declared return type (handles multi-statement bodies
|
|
633
|
-
// where body[0] is an `if`, not a `return`); fall back to the body's return.
|
|
634
|
-
const retTy = lam.ty.kind === "fn" ? lam.ty.result
|
|
635
|
-
: lam.body.length > 0 && lam.body[0].kind === "return" ? lam.body[0].value.ty : { kind: "unknown" };
|
|
636
|
-
return { kind: "array", elem: retTy };
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
else if (objTy.kind === "string") {
|
|
640
|
-
if (fn.field === "trim" || fn.field === "trimEnd" || fn.field === "trimStart" || fn.field === "toLowerCase" || fn.field === "toUpperCase")
|
|
641
|
-
return { kind: "string" };
|
|
642
|
-
if (fn.field === "slice" || fn.field === "substring")
|
|
643
|
-
return { kind: "string" };
|
|
644
|
-
if (fn.field === "split")
|
|
645
|
-
return { kind: "array", elem: { kind: "string" } };
|
|
646
|
-
if (fn.field === "includes" || fn.field === "startsWith" || fn.field === "endsWith")
|
|
647
|
-
return { kind: "bool" };
|
|
648
|
-
}
|
|
601
|
+
const id = recognizeBuiltin(objTy, fn.field);
|
|
602
|
+
if (id)
|
|
603
|
+
return builtinSpec(id).ret(objTy, args, { inSpec: ctx.inSpec });
|
|
649
604
|
return { kind: "unknown" };
|
|
650
605
|
}
|
|
651
606
|
/** Look up the type of `field` on `objTy`. Returns `unknown` if not found. */
|
|
@@ -657,7 +612,7 @@ function lookupFieldTy(objTy, field, ctx) {
|
|
|
657
612
|
return { ty: { kind: "nat" }, isDiscriminant: false };
|
|
658
613
|
}
|
|
659
614
|
if (objTy.kind === "user") {
|
|
660
|
-
const baseTyName =
|
|
615
|
+
const baseTyName = tyBaseName(objTy.name);
|
|
661
616
|
const isDiscriminant = getDiscriminant(ctx, baseTyName) === field;
|
|
662
617
|
const decl = findDecl(ctx, baseTyName);
|
|
663
618
|
if (decl?.kind === "record") {
|
|
@@ -691,7 +646,7 @@ function resolveRecordMerge(base, override, ctx) {
|
|
|
691
646
|
// into its inner type), else the base's.
|
|
692
647
|
const rTy = overInner.kind === "user" ? overInner
|
|
693
648
|
: tbase.ty.kind === "user" ? tbase.ty : null;
|
|
694
|
-
const decl = rTy ? ctx.typeDecls
|
|
649
|
+
const decl = rTy ? declOfKind(ctx.typeDecls, rTy.name, "record") : undefined;
|
|
695
650
|
if (!rTy || !decl?.fields) {
|
|
696
651
|
throw new Error(`object spread merge { ...a, ...b } needs a known record type for both operands ` +
|
|
697
652
|
`(base: ${tyToCanonical(tbase.ty)}, override: ${tyToCanonical(tover.ty)})`);
|
|
@@ -740,7 +695,7 @@ function tryRecordIndexByEnum(obj, idx, ctx) {
|
|
|
740
695
|
const objTy = obj.ty, keyTy = idx.ty;
|
|
741
696
|
if (objTy.kind !== "user")
|
|
742
697
|
return null;
|
|
743
|
-
const rec = ctx.typeDecls
|
|
698
|
+
const rec = declOfKind(ctx.typeDecls, objTy.name, "record");
|
|
744
699
|
if (!rec?.fields)
|
|
745
700
|
return null;
|
|
746
701
|
const fieldByName = new Map(rec.fields.map(f => [f.name, f]));
|
|
@@ -750,7 +705,7 @@ function tryRecordIndexByEnum(obj, idx, ctx) {
|
|
|
750
705
|
let values = null;
|
|
751
706
|
let datatype = null;
|
|
752
707
|
if (keyTy.kind === "user") {
|
|
753
|
-
const keyEnum = ctx.typeDecls
|
|
708
|
+
const keyEnum = declOfKind(ctx.typeDecls, keyTy.name, "string-union");
|
|
754
709
|
if (keyEnum?.values?.length) {
|
|
755
710
|
values = keyEnum.values;
|
|
756
711
|
datatype = keyEnum.name;
|
|
@@ -786,9 +741,11 @@ function resolveExpr(e, ctx) {
|
|
|
786
741
|
case "num":
|
|
787
742
|
if (!Number.isInteger(e.value))
|
|
788
743
|
return { kind: "num", value: e.value, ty: { kind: "real" } };
|
|
789
|
-
if (e.big)
|
|
790
|
-
return { kind: "num", value: e.value, ty: { kind: "int", big: true } };
|
|
791
744
|
return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
|
|
745
|
+
// Always `int` (never `nat`, even when non-negative), carrying `big` so the
|
|
746
|
+
// surrounding arithmetic picks bigint division semantics — see `isBigInt`.
|
|
747
|
+
case "bigint":
|
|
748
|
+
return { kind: "bigint", value: e.value, ty: { kind: "int", big: true } };
|
|
792
749
|
case "str":
|
|
793
750
|
return { kind: "str", value: e.value, ty: { kind: "string" } };
|
|
794
751
|
case "bool":
|
|
@@ -810,7 +767,7 @@ function resolveExpr(e, ctx) {
|
|
|
810
767
|
let rightCtx = ctx;
|
|
811
768
|
let rawRight = e.right;
|
|
812
769
|
if (e.op === "&&" || e.op === "==>") {
|
|
813
|
-
rightCtx = collectAndChainNarrowings(
|
|
770
|
+
rightCtx = collectAndChainNarrowings(left, ctx);
|
|
814
771
|
}
|
|
815
772
|
let right = resolveExpr(rawRight, rightCtx);
|
|
816
773
|
if (e.op === "===" || e.op === "!==") {
|
|
@@ -887,7 +844,7 @@ function resolveExpr(e, ctx) {
|
|
|
887
844
|
if (ext) {
|
|
888
845
|
const args = e.args.map(a => resolveExpr(a, ctx));
|
|
889
846
|
const fn = { kind: "var", name: ext.flat, ty: { kind: "unknown" } };
|
|
890
|
-
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure" };
|
|
847
|
+
return { kind: "call", fn, args, ty: ext.returnTy, callKind: "pure", paramTys: ext.params };
|
|
891
848
|
}
|
|
892
849
|
}
|
|
893
850
|
const fn = resolveExpr(e.fn, ctx);
|
|
@@ -903,7 +860,8 @@ function resolveExpr(e, ctx) {
|
|
|
903
860
|
const paramTypes = fn.kind === "var" && ctx.fnParams.has(fn.name) ? ctx.fnParams.get(fn.name) : null;
|
|
904
861
|
let args = coerceCallArgs(rawArgs.map((a, i) => {
|
|
905
862
|
let aCtx = argCtx;
|
|
906
|
-
if (paramTypes && i < paramTypes.length &&
|
|
863
|
+
if (paramTypes && i < paramTypes.length &&
|
|
864
|
+
(paramTypes[i].kind === "user" || paramTypes[i].kind === "array" || paramTypes[i].kind === "optional")) {
|
|
907
865
|
aCtx = { ...aCtx, returnTy: paramTypes[i] };
|
|
908
866
|
}
|
|
909
867
|
return resolveExpr(a, aCtx);
|
|
@@ -920,6 +878,12 @@ function resolveExpr(e, ctx) {
|
|
|
920
878
|
if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
|
|
921
879
|
ty = ctx.fnReturns.get(fn.name);
|
|
922
880
|
}
|
|
881
|
+
// Call through a function-typed value: its fn type carries the result
|
|
882
|
+
if (ty.kind === "unknown" && fn.kind === "var") {
|
|
883
|
+
const varTy = lookup(ctx.env, fn.name);
|
|
884
|
+
if (varTy?.kind === "fn")
|
|
885
|
+
ty = varTy.result;
|
|
886
|
+
}
|
|
923
887
|
// filterMap: `seqOfOption.filter(x => x !== undefined)` (a defined-check,
|
|
924
888
|
// typically with an `x is T` type guard) drops the Nones AND unwraps to
|
|
925
889
|
// seq<T>. Rewrite to a synthetic `filterSome` call lowered to the proven
|
|
@@ -930,7 +894,10 @@ function resolveExpr(e, ctx) {
|
|
|
930
894
|
&& fn.kind === "field" && fn.obj.ty.kind === "array" && fn.obj.ty.elem.kind === "optional") {
|
|
931
895
|
return { kind: "call", fn: { ...fn, field: "filterSome" }, args: [], ty: { kind: "array", elem: fn.obj.ty.elem.inner }, callKind: "method" };
|
|
932
896
|
}
|
|
933
|
-
|
|
897
|
+
const builtinId = fn.kind === "field" ? recognizeBuiltin(fn.obj.ty, fn.field) : null;
|
|
898
|
+
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx),
|
|
899
|
+
...(builtinId ? { builtinId } : {}),
|
|
900
|
+
...(paramTypes ? { paramTys: paramTypes } : {}) };
|
|
934
901
|
}
|
|
935
902
|
case "index": {
|
|
936
903
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -982,21 +949,41 @@ function resolveExpr(e, ctx) {
|
|
|
982
949
|
ty = np.narrowedTy;
|
|
983
950
|
}
|
|
984
951
|
}
|
|
952
|
+
// A variant narrowing on the object picks the field's type from that
|
|
953
|
+
// variant (a shared field name can have a different type per variant).
|
|
954
|
+
let ofVariant;
|
|
955
|
+
if (ty.kind === "unknown" && ctx.narrowedPaths.length > 0 && obj.ty.kind === "user") {
|
|
956
|
+
const objPath = asRawAccessPath(e.obj);
|
|
957
|
+
const np = objPath ? ctx.narrowedPaths.find(n => n.variant && accessPathsEqual(n.path, objPath)) : undefined;
|
|
958
|
+
if (np?.variant) {
|
|
959
|
+
const decl = findDecl(ctx, tyBaseName(obj.ty.name));
|
|
960
|
+
const f = decl?.variants?.find(v => v.name === np.variant)?.fields.find(f => f.name === e.field);
|
|
961
|
+
if (f?.type) {
|
|
962
|
+
ty = f.type;
|
|
963
|
+
ofVariant = np.variant;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
985
967
|
if (ty.kind === "unknown") {
|
|
986
968
|
const lookup = lookupFieldTy(obj.ty, e.field, ctx);
|
|
987
969
|
ty = lookup.ty;
|
|
988
970
|
isDiscriminant = lookup.isDiscriminant;
|
|
989
971
|
}
|
|
990
|
-
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
972
|
+
return { kind: "field", obj, field: e.field, ty, isDiscriminant, ofVariant };
|
|
991
973
|
}
|
|
992
974
|
case "nullish": {
|
|
993
975
|
// left ?? right — result type is left's inner (when left is optional)
|
|
994
976
|
// or just left's type, unified with right's type.
|
|
995
977
|
const left = resolveExpr(e.left, ctx);
|
|
996
|
-
const
|
|
978
|
+
const inner = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
997
979
|
// The default shares the result type, so coerce a string literal to a
|
|
998
980
|
// string-union enum (e.g. `availableLevels[0] ?? "off"`).
|
|
999
|
-
const right =
|
|
981
|
+
const right = coerceToTargetTy(resolveExpr(e.right, ctx), inner, ctx.typeDecls);
|
|
982
|
+
// `??` is only total when its default is: with a nullable right operand
|
|
983
|
+
// (rule-chain style `ruleA(e) ?? ruleB(e) ?? null`), the result stays
|
|
984
|
+
// optional — otherwise the enclosing chain level loses its optionality
|
|
985
|
+
// and narrowing can't rewrite it.
|
|
986
|
+
const ty = right.ty.kind === "optional" ? right.ty : inner;
|
|
1000
987
|
return { kind: "nullish", left, right, ty };
|
|
1001
988
|
}
|
|
1002
989
|
case "optChain": {
|
|
@@ -1041,7 +1028,9 @@ function resolveExpr(e, ctx) {
|
|
|
1041
1028
|
const args = rawArgs.map(a => resolveExpr(a, ctx));
|
|
1042
1029
|
callTy = inferMethodReturnTy(fakeFn, args, ctx);
|
|
1043
1030
|
callKind = "method";
|
|
1044
|
-
|
|
1031
|
+
const builtinId = recognizeBuiltin(priorInTy, lastField.name);
|
|
1032
|
+
chain.push({ kind: "call", args, ty: callTy, callKind,
|
|
1033
|
+
...(builtinId ? { builtinId } : {}) });
|
|
1045
1034
|
stepInTy = callTy;
|
|
1046
1035
|
continue;
|
|
1047
1036
|
}
|
|
@@ -1070,20 +1059,37 @@ function resolveExpr(e, ctx) {
|
|
|
1070
1059
|
// has ctx.returnTy = Option<T>, but the record literal's natural type is T.
|
|
1071
1060
|
const returnTyUnwrapped = ctx.returnTy.kind === "optional" ? ctx.returnTy.inner : ctx.returnTy;
|
|
1072
1061
|
const recordTy = ty.kind === "user" ? ty : returnTyUnwrapped.kind === "user" ? returnTyUnwrapped : null;
|
|
1073
|
-
const decl = recordTy ? ctx.typeDecls
|
|
1062
|
+
const decl = recordTy ? declOfKind(ctx.typeDecls, recordTy.name, "record") : undefined;
|
|
1063
|
+
// Union-variant literal in union-typed context (a constructed IR node,
|
|
1064
|
+
// `{ kind: "if", … }: TStmt`): contextual field types come from the
|
|
1065
|
+
// variant the literal's discriminant field selects.
|
|
1066
|
+
let declFields = decl?.fields;
|
|
1067
|
+
if (!declFields && recordTy) {
|
|
1068
|
+
const udecl = declOfKind(ctx.typeDecls, recordTy.name, "discriminated-union");
|
|
1069
|
+
if (udecl?.discriminant && udecl.variants) {
|
|
1070
|
+
const tagRaw = e.fields.find(f => f.name === udecl.discriminant)?.value;
|
|
1071
|
+
if (tagRaw?.kind === "str") {
|
|
1072
|
+
declFields = udecl.variants.find(v => v.name === tagRaw.value)?.fields;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1074
1076
|
// Clear returnTy for field values — it applies to THIS record, not nested ones
|
|
1075
1077
|
const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
|
|
1076
1078
|
const fields = e.fields.map(f => {
|
|
1077
|
-
const fieldDecl =
|
|
1079
|
+
const fieldDecl = declFields?.find(df => df.name === f.name);
|
|
1078
1080
|
// Propagate declared field type into context so nested records resolve
|
|
1079
|
-
// their union variant correctly (e.g., { kind: 'Idle' } → EffectMode.Idle)
|
|
1080
|
-
|
|
1081
|
-
|
|
1081
|
+
// their union variant correctly (e.g., { kind: 'Idle' } → EffectMode.Idle).
|
|
1082
|
+
// Optional fields propagate their inner type (the Some-wrap is restored
|
|
1083
|
+
// by coerceToTargetTy below); array fields propagate whole, so the
|
|
1084
|
+
// arrayLiteral case can thread the element type.
|
|
1085
|
+
const fdTy = fieldDecl?.type;
|
|
1086
|
+
const fdCtxTy = fdTy?.kind === "optional" ? fdTy.inner : fdTy;
|
|
1087
|
+
const valueCtx = fdCtxTy && (fdCtxTy.kind === "user" || fdCtxTy.kind === "array")
|
|
1088
|
+
? { ...fieldCtx, returnTy: fdCtxTy }
|
|
1082
1089
|
: fieldCtx;
|
|
1083
1090
|
let value = resolveExpr(f.value, valueCtx);
|
|
1084
1091
|
if (fieldDecl) {
|
|
1085
1092
|
const declTy = fieldDecl.type;
|
|
1086
|
-
value = coerceStr(value, declTy);
|
|
1087
1093
|
// Empty {} for map-typed fields → empty map (arrayLiteral with map type → emptyMap in transform)
|
|
1088
1094
|
if (value.kind === "record" && value.fields.length === 0 && !value.spread && declTy.kind === "map") {
|
|
1089
1095
|
value = { kind: "arrayLiteral", elems: [], ty: declTy };
|
|
@@ -1124,7 +1130,7 @@ function resolveExpr(e, ctx) {
|
|
|
1124
1130
|
const elems = e.elems.map((el, i) => {
|
|
1125
1131
|
const slot = slots[i];
|
|
1126
1132
|
const r = resolveExpr(el, slot ? { ...ctx, returnTy: slot } : ctx);
|
|
1127
|
-
return slot ?
|
|
1133
|
+
return slot ? coerceToTargetTy(r, slot, ctx.typeDecls) : r;
|
|
1128
1134
|
});
|
|
1129
1135
|
return { kind: "arrayLiteral", elems, ty: { kind: "tuple", elems: elems.map(x => x.ty) } };
|
|
1130
1136
|
}
|
|
@@ -1132,14 +1138,16 @@ function resolveExpr(e, ctx) {
|
|
|
1132
1138
|
// literal in an array resolves to its named datatype rather than an
|
|
1133
1139
|
// anonymous tuple (mirrors return-position and call-argument records, which
|
|
1134
1140
|
// get their type via ctx.returnTy). Only narrow when the context type is an
|
|
1135
|
-
// array
|
|
1136
|
-
|
|
1141
|
+
// array (unwrapping one optional level — `TStmt[] | null` return positions);
|
|
1142
|
+
// otherwise leave ctx untouched.
|
|
1143
|
+
const rtUnwrapped = ctx.returnTy.kind === "optional" ? ctx.returnTy.inner : ctx.returnTy;
|
|
1144
|
+
const expectedElem = rtUnwrapped.kind === "array" ? rtUnwrapped.elem : null;
|
|
1137
1145
|
const elemCtx = expectedElem ? { ...ctx, returnTy: expectedElem } : ctx;
|
|
1138
1146
|
const elems = e.elems.map(el => {
|
|
1139
1147
|
const r = resolveExpr(el, elemCtx);
|
|
1140
1148
|
// Coerce a bare string-literal element to a string-union enum (e.g.
|
|
1141
1149
|
// `["off", …]: ModelThinkingLevel[]`), like return/arg positions.
|
|
1142
|
-
return expectedElem ?
|
|
1150
|
+
return expectedElem ? coerceToTargetTy(r, expectedElem, ctx.typeDecls) : r;
|
|
1143
1151
|
});
|
|
1144
1152
|
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
1145
1153
|
// No expected collection type: infer array vs tuple from the elements —
|
|
@@ -1183,19 +1191,19 @@ function resolveExpr(e, ctx) {
|
|
|
1183
1191
|
// with method calls or index ops (bind-first required).
|
|
1184
1192
|
// For &&-chains, all positive checks narrow the then-branch; earlier
|
|
1185
1193
|
// checks are in scope when resolving later ones.
|
|
1186
|
-
let thenCtx = collectAndChainNarrowings(
|
|
1194
|
+
let thenCtx = collectAndChainNarrowings(cond, ctx);
|
|
1187
1195
|
let elseCtx = ctx;
|
|
1188
1196
|
// Truthiness — cond itself is optional (`opt ? a : b`), only for simple vars.
|
|
1189
|
-
if (cond.ty.kind === "optional" &&
|
|
1190
|
-
thenCtx = withEnv(thenCtx, extend(thenCtx.env,
|
|
1197
|
+
if (cond.ty.kind === "optional" && cond.kind === "var") {
|
|
1198
|
+
thenCtx = withEnv(thenCtx, extend(thenCtx.env, cond.name, cond.ty.inner));
|
|
1191
1199
|
}
|
|
1192
1200
|
// Single === undefined check narrows the else-branch.
|
|
1193
|
-
const single =
|
|
1194
|
-
if (single
|
|
1201
|
+
const single = negatedVarPresence(cond);
|
|
1202
|
+
if (single) {
|
|
1195
1203
|
elseCtx = withEnv(elseCtx, extend(elseCtx.env, single.varName, single.innerTy));
|
|
1196
1204
|
}
|
|
1197
|
-
if (!single &&
|
|
1198
|
-
for (const n of collectEarlyReturnNarrowings(
|
|
1205
|
+
if (!single && cond.kind === "binop" && cond.op === "||") {
|
|
1206
|
+
for (const n of collectEarlyReturnNarrowings(cond)) {
|
|
1199
1207
|
elseCtx = withEnv(elseCtx, extend(elseCtx.env, n.varName, n.innerTy));
|
|
1200
1208
|
}
|
|
1201
1209
|
}
|
|
@@ -1207,21 +1215,7 @@ function resolveExpr(e, ctx) {
|
|
|
1207
1215
|
let else_ = resolveExpr(e.else, elseCtx);
|
|
1208
1216
|
then_ = coerceStr(then_, else_.ty);
|
|
1209
1217
|
else_ = coerceStr(else_, then_.ty);
|
|
1210
|
-
|
|
1211
|
-
if (then_.ty.kind === "void" && else_.ty.kind !== "void" && else_.ty.kind !== "unknown") {
|
|
1212
|
-
ty = { kind: "optional", inner: else_.ty };
|
|
1213
|
-
}
|
|
1214
|
-
else if (else_.ty.kind === "void" && then_.ty.kind !== "void" && then_.ty.kind !== "unknown") {
|
|
1215
|
-
ty = { kind: "optional", inner: then_.ty };
|
|
1216
|
-
}
|
|
1217
|
-
else if (then_.ty.kind === "optional" && else_.ty.kind !== "optional" && else_.ty.kind !== "unknown") {
|
|
1218
|
-
// Asymmetric optional: one branch returns Option<T>, the other returns T.
|
|
1219
|
-
// Widen to Option<T> so callers/return-coercion see the wider type.
|
|
1220
|
-
ty = then_.ty;
|
|
1221
|
-
}
|
|
1222
|
-
else if (else_.ty.kind === "optional" && then_.ty.kind !== "optional" && then_.ty.kind !== "unknown") {
|
|
1223
|
-
ty = else_.ty;
|
|
1224
|
-
}
|
|
1218
|
+
const ty = conditionalResultTy(then_.ty, else_.ty);
|
|
1225
1219
|
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
1226
1220
|
}
|
|
1227
1221
|
case "emptyCollection": {
|
|
@@ -1256,8 +1250,9 @@ function resolveBlock(stmts, ctx) {
|
|
|
1256
1250
|
const result = [];
|
|
1257
1251
|
let env = ctx.env;
|
|
1258
1252
|
let narrowedIndices = ctx.narrowedIndices;
|
|
1253
|
+
let narrowedPaths = ctx.narrowedPaths;
|
|
1259
1254
|
for (const s of stmts) {
|
|
1260
|
-
const currentCtx = { ...ctx, env, narrowedIndices };
|
|
1255
|
+
const currentCtx = { ...ctx, env, narrowedIndices, narrowedPaths };
|
|
1261
1256
|
const [typed, nextEnv] = resolveStmt(s, currentCtx);
|
|
1262
1257
|
result.push(typed);
|
|
1263
1258
|
env = nextEnv;
|
|
@@ -1269,9 +1264,13 @@ function resolveBlock(stmts, ctx) {
|
|
|
1269
1264
|
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
1270
1265
|
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
1271
1266
|
if (s.kind === "if" && s.then.length > 0 && isTerminatorKind(s.then[s.then.length - 1].kind) && s.else.length === 0) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1267
|
+
if (typed.kind === "if") {
|
|
1268
|
+
for (const n of collectEarlyReturnNarrowings(typed.cond)) {
|
|
1269
|
+
env = extend(env, n.varName, n.innerTy);
|
|
1270
|
+
}
|
|
1271
|
+
const nv = negatedVariantCheck(typed.cond);
|
|
1272
|
+
if (nv)
|
|
1273
|
+
narrowedPaths = [...narrowedPaths, nv];
|
|
1275
1274
|
}
|
|
1276
1275
|
// Map-index narrowing: `if (!(k in m)) return;` means `k in m` holds in rest.
|
|
1277
1276
|
if (typed.kind === "if") {
|
|
@@ -1308,9 +1307,21 @@ function resolveStmt(s, ctx) {
|
|
|
1308
1307
|
// Propagate declared type as returnTy so nested record expressions resolve
|
|
1309
1308
|
// union variants correctly (e.g., EffectState → mode: EffectMode → { kind:
|
|
1310
1309
|
// 'Idle' }). Arrays too, so `const xs: Foo[] = [{...}]` threads the element
|
|
1311
|
-
// type into the array literal (see the arrayLiteral case).
|
|
1312
|
-
|
|
1313
|
-
|
|
1310
|
+
// type into the array literal (see the arrayLiteral case). Optionals too
|
|
1311
|
+
// (`const r: TExpr | null = cond ? {…} : null`) — the record case unwraps
|
|
1312
|
+
// one optional level when consulting returnTy.
|
|
1313
|
+
const initCtx = (declTy.kind === "user" || declTy.kind === "array" || declTy.kind === "optional")
|
|
1314
|
+
? { ...ctx, returnTy: declTy } : ctx;
|
|
1315
|
+
let init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
1316
|
+
// Under noUncheckedIndexedAccess, TS gives `const e = arr[i]` type T | undefined
|
|
1317
|
+
// while the index expression itself resolves to T. Leave that mismatch intact:
|
|
1318
|
+
// narrow.ts's ruleOptionalIndexBinding adds the runtime bounds guard and the
|
|
1319
|
+
// corresponding Some/None branches. Wrapping here would turn it into an
|
|
1320
|
+
// unconditional Some(arr[i]) and prevent that JS-semantics rewrite from firing.
|
|
1321
|
+
const deferOptionalIndex = declTy.kind === "optional" && init.kind === "index" &&
|
|
1322
|
+
init.obj.ty.kind === "array" && init.ty.kind !== "optional";
|
|
1323
|
+
if (!deferOptionalIndex)
|
|
1324
|
+
init = coerceToTargetTy(init, declTy, ctx.typeDecls);
|
|
1314
1325
|
let ty;
|
|
1315
1326
|
if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
|
|
1316
1327
|
// ts-morph's declared type is opaque to us (an expanded union it made
|
|
@@ -1321,7 +1332,7 @@ function resolveStmt(s, ctx) {
|
|
|
1321
1332
|
? { kind: "optional", inner: init.ty }
|
|
1322
1333
|
: init.ty;
|
|
1323
1334
|
}
|
|
1324
|
-
else if (declTy
|
|
1335
|
+
else if (isWidenedStringUnionTy(declTy, init.ty, ctx.typeDecls) && !ctx.overrides.has(s.name)) {
|
|
1325
1336
|
// ts-morph widened a string-union to `string`; keep the initializer's
|
|
1326
1337
|
// datatype so `local === "lit"` lowers to a discriminant test.
|
|
1327
1338
|
ty = init.ty;
|
|
@@ -1342,22 +1353,16 @@ function resolveStmt(s, ctx) {
|
|
|
1342
1353
|
}
|
|
1343
1354
|
case "assign": {
|
|
1344
1355
|
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
1345
|
-
|
|
1346
|
-
//
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1356
|
+
// Propagate the target's type as returnTy, mirroring the annotated-let
|
|
1357
|
+
// case, so record/union literals and array literals in the RHS resolve
|
|
1358
|
+
// to their named datatypes.
|
|
1359
|
+
const valueCtx = (targetTy.kind === "user" || targetTy.kind === "array" || targetTy.kind === "optional")
|
|
1360
|
+
? { ...ctx, returnTy: targetTy } : ctx;
|
|
1361
|
+
const value = coerceToTargetTy(resolveExpr(s.value, valueCtx), targetTy, ctx.typeDecls);
|
|
1351
1362
|
return [{ kind: "assign", target: s.target, value }, ctx.env];
|
|
1352
1363
|
}
|
|
1353
1364
|
case "return": {
|
|
1354
|
-
|
|
1355
|
-
// Wrap non-optional return value in Some when function returns optional
|
|
1356
|
-
// Skip if already optional, void, or undefined (which maps to None)
|
|
1357
|
-
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
1358
|
-
if (ctx.returnTy.kind === "optional" && value.ty.kind !== "optional" && !isUndef) {
|
|
1359
|
-
value = wrapSome(value, ctx.returnTy);
|
|
1360
|
-
}
|
|
1365
|
+
const value = coerceToTargetTy(resolveExpr(s.value, ctx), ctx.returnTy, ctx.typeDecls);
|
|
1361
1366
|
return [{ kind: "return", value }, ctx.env];
|
|
1362
1367
|
}
|
|
1363
1368
|
case "break":
|
|
@@ -1371,16 +1376,16 @@ function resolveStmt(s, ctx) {
|
|
|
1371
1376
|
// For &&-chains, all positive optional checks narrow the then-branch;
|
|
1372
1377
|
// earlier checks are in scope when resolving later ones.
|
|
1373
1378
|
// Single-check === undefined narrows the else-branch.
|
|
1374
|
-
|
|
1379
|
+
const resolvedCond = resolveExpr(s.cond, ctx);
|
|
1380
|
+
let thenCtx = collectAndChainNarrowings(resolvedCond, ctx);
|
|
1375
1381
|
let elseCtx = ctx;
|
|
1376
|
-
const single =
|
|
1377
|
-
if (single
|
|
1382
|
+
const single = negatedVarPresence(resolvedCond);
|
|
1383
|
+
if (single) {
|
|
1378
1384
|
elseCtx = withEnv(ctx, extend(ctx.env, single.varName, single.innerTy));
|
|
1379
1385
|
}
|
|
1380
1386
|
// Narrow map index access across `k in m` / `!(k in m)` in the cond:
|
|
1381
1387
|
// positive atoms (from `k in m` or &&-chains containing it) → then-branch;
|
|
1382
1388
|
// negated atoms (from `!(k in m)`) → else-branch.
|
|
1383
|
-
const resolvedCond = resolveExpr(s.cond, ctx);
|
|
1384
1389
|
thenCtx = withInAtoms(thenCtx, extractInAtoms(resolvedCond));
|
|
1385
1390
|
elseCtx = withInAtoms(elseCtx, extractInAtomsNegated(resolvedCond));
|
|
1386
1391
|
return [{ kind: "if", cond: resolvedCond, then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|
|
@@ -1473,14 +1478,27 @@ function resolveStmt(s, ctx) {
|
|
|
1473
1478
|
}
|
|
1474
1479
|
}
|
|
1475
1480
|
// ── Pure / return-in-loop detection ──────────────────────────
|
|
1476
|
-
/**
|
|
1481
|
+
/** Whether a raw statement or expression tree contains a havoc anywhere. */
|
|
1482
|
+
function containsHavoc(v) {
|
|
1483
|
+
if (Array.isArray(v))
|
|
1484
|
+
return v.some(containsHavoc);
|
|
1485
|
+
if (v === null || typeof v !== "object")
|
|
1486
|
+
return false;
|
|
1487
|
+
if (v.kind === "havoc")
|
|
1488
|
+
return true;
|
|
1489
|
+
return Object.values(v).some(containsHavoc);
|
|
1490
|
+
}
|
|
1491
|
+
/** Syntactic purity: no while, no for-of, no mutable let, no havoc. */
|
|
1477
1492
|
function isSyntacticallyPure(stmts) {
|
|
1478
1493
|
for (const s of stmts) {
|
|
1494
|
+
// Havoc lowers to Dafny's `*`, which is only valid in a method.
|
|
1495
|
+
if (containsHavoc(s))
|
|
1496
|
+
return false;
|
|
1479
1497
|
switch (s.kind) {
|
|
1480
1498
|
case "while":
|
|
1481
1499
|
case "forof": return false;
|
|
1482
1500
|
case "let":
|
|
1483
|
-
if (s.mutable
|
|
1501
|
+
if (s.mutable)
|
|
1484
1502
|
return false;
|
|
1485
1503
|
break;
|
|
1486
1504
|
case "if":
|
|
@@ -1800,7 +1818,8 @@ export function resolveModule(raw) {
|
|
|
1800
1818
|
// record literals on map-typed constants (e.g. `Record<string, number>`)
|
|
1801
1819
|
// get their `ty` set to `map<...>` rather than `user("...")`.
|
|
1802
1820
|
const valueCtx = { ...emptyCtx, returnTy: ty };
|
|
1803
|
-
|
|
1821
|
+
const value = coerceToTargetTy(resolveExpr(c.value, valueCtx), ty, raw.typeDecls);
|
|
1822
|
+
return { name: c.name, ty, value };
|
|
1804
1823
|
});
|
|
1805
1824
|
const moduleConstants = new Map(constants.map(c => [c.name, c.ty]));
|
|
1806
1825
|
return {
|