lemmascript 0.5.18 → 0.5.19
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 +124 -0
- package/tools/dist/condition-facts.js +364 -0
- package/tools/dist/dafny-emit.js +162 -35
- package/tools/dist/extract.js +108 -21
- package/tools/dist/info-command.js +68 -0
- package/tools/dist/ir.js +27 -7
- package/tools/dist/lean-emit.js +27 -16
- 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 +182 -203
- package/tools/dist/specparser.js +21 -17
- package/tools/dist/transform.js +298 -108
- 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;
|
|
@@ -68,7 +71,7 @@ function wrapSome(value, optionalTy) {
|
|
|
68
71
|
}
|
|
69
72
|
/** Find the synth array-union TypeDecl named `name` (discriminant `__isArray__`). */
|
|
70
73
|
function findSynthArrayUnion(name, typeDecls) {
|
|
71
|
-
const decl = typeDecls
|
|
74
|
+
const decl = declOf(typeDecls, name);
|
|
72
75
|
if (decl?.kind === "discriminated-union" && decl.discriminant === "__isArray__")
|
|
73
76
|
return decl;
|
|
74
77
|
return null;
|
|
@@ -103,68 +106,37 @@ function coerceToTargetTy(value, targetTy, typeDecls) {
|
|
|
103
106
|
}
|
|
104
107
|
return value;
|
|
105
108
|
}
|
|
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;
|
|
109
|
+
/** A negated presence check on a bare var (`v === undefined` / `!v`), via
|
|
110
|
+
* the shared condition analyzer (§4) on the *resolved* condition. Narrows
|
|
111
|
+
* the else-branch (or the rest of the block, after an early return). */
|
|
112
|
+
function negatedVarPresence(cond) {
|
|
113
|
+
const f = presentFact(cond);
|
|
114
|
+
if (f && f.negated && f.scrutinee.kind === "var") {
|
|
115
|
+
return { varName: f.scrutinee.name, innerTy: f.innerTy };
|
|
119
116
|
}
|
|
120
|
-
|
|
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 };
|
|
150
|
-
}
|
|
151
|
-
const resolved = resolveExpr(e, ctx);
|
|
152
|
-
if (resolved.ty.kind !== "optional")
|
|
153
|
-
return null;
|
|
154
|
-
return { varName: "", innerTy: resolved.ty.inner, fieldExpr: e };
|
|
117
|
+
return null;
|
|
155
118
|
}
|
|
156
119
|
/** Collect all optional narrowings from an early-return condition.
|
|
157
120
|
* Handles single checks (x === undefined) and compound || chains
|
|
158
121
|
* (x === undefined || y === undefined). */
|
|
159
|
-
function collectEarlyReturnNarrowings(cond
|
|
122
|
+
function collectEarlyReturnNarrowings(cond) {
|
|
160
123
|
if (cond.kind === "binop" && cond.op === "||") {
|
|
161
|
-
return [...collectEarlyReturnNarrowings(cond.left
|
|
124
|
+
return [...collectEarlyReturnNarrowings(cond.left), ...collectEarlyReturnNarrowings(cond.right)];
|
|
162
125
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
126
|
+
const n = negatedVarPresence(cond);
|
|
127
|
+
return n ? [n] : [];
|
|
128
|
+
}
|
|
129
|
+
/** Negated discriminant check (`path.kind !== "lit"`) on a var or field path.
|
|
130
|
+
* After `if (path.kind !== "lit") return`, the rest of the block knows the
|
|
131
|
+
* path is that variant. */
|
|
132
|
+
function negatedVariantCheck(cond) {
|
|
133
|
+
if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
|
|
134
|
+
cond.left.kind === "field" && cond.left.isDiscriminant && cond.left.obj.ty.kind === "user") {
|
|
135
|
+
const path = asTExprAccessPath(cond.left.obj);
|
|
136
|
+
if (path)
|
|
137
|
+
return { path, narrowedTy: cond.left.obj.ty, variant: cond.right.value };
|
|
166
138
|
}
|
|
167
|
-
return
|
|
139
|
+
return null;
|
|
168
140
|
}
|
|
169
141
|
/** TExpr → AccessPath. Counterpart to `asRawAccessPath` for resolved trees.
|
|
170
142
|
* Used by `extractInAtoms` when pulling atoms out of typed spec expressions. */
|
|
@@ -222,23 +194,36 @@ function withInAtoms(ctx, atoms) {
|
|
|
222
194
|
return ctx;
|
|
223
195
|
return { ...ctx, narrowedIndices: [...existing, ...added] };
|
|
224
196
|
}
|
|
225
|
-
/** Walk an `&&` chain of `e !== undefined` checks
|
|
226
|
-
*
|
|
227
|
-
*
|
|
197
|
+
/** Walk an `&&` chain of `e !== undefined` checks on the *resolved*
|
|
198
|
+
* condition, returning a Ctx with all narrowings applied. Earlier checks
|
|
199
|
+
* are in scope for later checks (the right conjunct was already resolved
|
|
200
|
+
* under the left's narrowings by the `&&` case of resolveExpr). Consults
|
|
201
|
+
* the shared condition analyzer (§4): a positive presence fact on a bare
|
|
202
|
+
* var extends the env; on a pure field path it extends `narrowedPaths`. */
|
|
228
203
|
function collectAndChainNarrowings(cond, ctx) {
|
|
229
204
|
if (cond.kind === "binop" && cond.op === "&&") {
|
|
230
205
|
const leftCtx = collectAndChainNarrowings(cond.left, ctx);
|
|
231
206
|
return collectAndChainNarrowings(cond.right, leftCtx);
|
|
232
207
|
}
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
208
|
+
const f = presentFact(cond);
|
|
209
|
+
if (f && !f.negated) {
|
|
210
|
+
if (f.scrutinee.kind === "var") {
|
|
211
|
+
return withEnv(ctx, extend(ctx.env, f.scrutinee.name, f.innerTy));
|
|
212
|
+
}
|
|
213
|
+
const path = asTExprAccessPath(f.scrutinee);
|
|
214
|
+
if (path) {
|
|
215
|
+
return { ...ctx, narrowedPaths: [...ctx.narrowedPaths, { path, narrowedTy: f.innerTy }] };
|
|
216
|
+
}
|
|
235
217
|
return ctx;
|
|
236
|
-
if (!n.fieldExpr) {
|
|
237
|
-
return withEnv(ctx, extend(ctx.env, n.varName, n.innerTy));
|
|
238
218
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
219
|
+
// Positive discriminant check (`x.kind === "bool"`): record the variant so
|
|
220
|
+
// field reads on the path resolve against that variant's field types.
|
|
221
|
+
const vf = variantFact(cond, { decls: ctx.typeDecls, oc: { n: 0 } });
|
|
222
|
+
if (vf) {
|
|
223
|
+
const path = asTExprAccessPath(vf.scrutinee);
|
|
224
|
+
if (path) {
|
|
225
|
+
return { ...ctx, narrowedPaths: [...ctx.narrowedPaths, { path, narrowedTy: vf.scrutinee.ty, variant: vf.variant }] };
|
|
226
|
+
}
|
|
242
227
|
}
|
|
243
228
|
return ctx;
|
|
244
229
|
}
|
|
@@ -247,16 +232,10 @@ function isRefMutableInTS(ty) {
|
|
|
247
232
|
return ty.kind === "array" || ty.kind === "map" || ty.kind === "set";
|
|
248
233
|
}
|
|
249
234
|
function findDecl(ctx, name) {
|
|
250
|
-
|
|
251
|
-
if (direct)
|
|
252
|
-
return direct;
|
|
253
|
-
// Dotted names (e.g. `Agent.Info`, `Permission.Ruleset`): fall back to the
|
|
235
|
+
// Dotted names (e.g. `Agent.Info`, `Permission.Ruleset`) fall back to the
|
|
254
236
|
// last segment, so `//@ declare-type Info { ... }` matches a reference to
|
|
255
237
|
// `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;
|
|
238
|
+
return declOfDotted(ctx.typeDecls, name);
|
|
260
239
|
}
|
|
261
240
|
/** Expand alias-kind typeDecls when the alias target is structural (array,
|
|
262
241
|
* map, set, optional, or another user type). Primitive-typed aliases like
|
|
@@ -266,11 +245,7 @@ function expandAlias(ty, typeDecls, seen = new Set()) {
|
|
|
266
245
|
if (ty.kind === "user") {
|
|
267
246
|
if (seen.has(ty.name))
|
|
268
247
|
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
|
-
}
|
|
248
|
+
const decl = declOfDotted(typeDecls, ty.name);
|
|
274
249
|
if (decl?.kind === "alias" && decl.aliasOfTy) {
|
|
275
250
|
const target = decl.aliasOfTy;
|
|
276
251
|
if (target.kind === "array" || target.kind === "map" || target.kind === "set" || target.kind === "optional" || target.kind === "user") {
|
|
@@ -305,11 +280,7 @@ function refEqHazard(ty, typeDecls) {
|
|
|
305
280
|
if (ty.kind === "array" || ty.kind === "map" || ty.kind === "set" || ty.kind === "tuple")
|
|
306
281
|
return true;
|
|
307
282
|
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
|
-
}
|
|
283
|
+
const decl = declOfDotted(typeDecls, ty.name);
|
|
313
284
|
if (!decl)
|
|
314
285
|
return true; // generic type parameter / unknown → assume reference
|
|
315
286
|
if (decl.kind === "string-union")
|
|
@@ -348,35 +319,33 @@ function isUnmodeledTy(ty, typeDecls) {
|
|
|
348
319
|
return isUnmodeledTy(ty.elem, typeDecls);
|
|
349
320
|
if (ty.kind === "map")
|
|
350
321
|
return isUnmodeledTy(ty.key, typeDecls) || isUnmodeledTy(ty.value, typeDecls);
|
|
351
|
-
if (ty.kind === "user")
|
|
352
|
-
|
|
353
|
-
return !typeDecls.some(d => d.name === base);
|
|
354
|
-
}
|
|
322
|
+
if (ty.kind === "user")
|
|
323
|
+
return declOfTy(typeDecls, ty) === undefined;
|
|
355
324
|
return false;
|
|
356
325
|
}
|
|
357
326
|
/** A `user` type that resolves to a string-union declare-type — runs as a plain
|
|
358
327
|
* string at runtime, so it's a refinement of `string`, not an opaque blob. */
|
|
359
328
|
function isStringUnionTy(ty, typeDecls) {
|
|
360
|
-
|
|
361
|
-
return false;
|
|
362
|
-
const base = ty.name.includes("<") ? ty.name.slice(0, ty.name.indexOf("<")) : ty.name;
|
|
363
|
-
return typeDecls.some(d => d.name === base && d.kind === "string-union");
|
|
329
|
+
return declOfTy(typeDecls, ty)?.kind === "string-union";
|
|
364
330
|
}
|
|
365
331
|
/** Infer quantifier variable type from usage in body.
|
|
366
332
|
* If the variable is used as a map/set key (e.g. map.has(k), map.get(k)),
|
|
367
333
|
* return the collection's key type. Otherwise return null (default to int). */
|
|
368
334
|
function inferQuantVarType(varName, body, ctx) {
|
|
369
|
-
// Look for
|
|
335
|
+
// Look for membership/lookup builtins (map.has(k), map.get(k),
|
|
336
|
+
// array.includes(k) — registry `argIsKey`) where k is our variable
|
|
370
337
|
if (body.kind === "call" && body.fn.kind === "field" &&
|
|
371
|
-
(body.fn.field === "has" || body.fn.field === "get" || body.fn.field === "includes") &&
|
|
372
338
|
body.args.length === 1 && body.args[0].kind === "var" && body.args[0].name === varName) {
|
|
373
339
|
const objTy = lookup(ctx.env, body.fn.obj.kind === "var" ? body.fn.obj.name : "");
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
340
|
+
const id = objTy ? recognizeBuiltin(objTy, body.fn.field) : null;
|
|
341
|
+
if (id && builtinSpec(id).argIsKey && objTy) {
|
|
342
|
+
if (objTy.kind === "map")
|
|
343
|
+
return objTy.key;
|
|
344
|
+
if (objTy.kind === "set")
|
|
345
|
+
return objTy.elem;
|
|
346
|
+
if (objTy.kind === "array")
|
|
347
|
+
return objTy.elem;
|
|
348
|
+
}
|
|
380
349
|
}
|
|
381
350
|
// Recurse into subexpressions
|
|
382
351
|
if (body.kind === "binop") {
|
|
@@ -438,6 +407,8 @@ function classifyCall(fn, ctx) {
|
|
|
438
407
|
// become multi-statement, illegal in Dafny).
|
|
439
408
|
if (fn.kind === "var" && ctx.externs.has(fn.name))
|
|
440
409
|
return "pure";
|
|
410
|
+
if (fn.kind === "var" && lookup(ctx.env, fn.name)?.kind === "fn")
|
|
411
|
+
return "pure";
|
|
441
412
|
if (fn.kind === "var" && ctx.inSpec) {
|
|
442
413
|
// Not a known pure function — could be external (Lean-defined spec helper).
|
|
443
414
|
// Pass through as "pure" and let Lean catch any errors.
|
|
@@ -470,8 +441,11 @@ function tyToTsStr(ty) {
|
|
|
470
441
|
return undefined;
|
|
471
442
|
}
|
|
472
443
|
function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
444
|
+
const hofShape = fn.kind === "field"
|
|
445
|
+
? (id => id ? builtinSpec(id).hof?.shape : undefined)(recognizeBuiltin(fn.obj.ty, fn.field))
|
|
446
|
+
: undefined;
|
|
473
447
|
// sort's comparator takes two params, both the element type.
|
|
474
|
-
if (
|
|
448
|
+
if (hofShape === "comparator" && fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
475
449
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 1) {
|
|
476
450
|
const tsType = tyToTsStr(fn.obj.ty.elem);
|
|
477
451
|
if (tsType) {
|
|
@@ -481,7 +455,7 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
481
455
|
}
|
|
482
456
|
}
|
|
483
457
|
// reduce's callback is (acc, elem): acc from the init arg's type, elem from the array.
|
|
484
|
-
if (
|
|
458
|
+
if (hofShape === "reduce" && fn.kind === "field" && fn.obj.ty.kind === "array" && ctx &&
|
|
485
459
|
rawArgs.length >= 2 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 2) {
|
|
486
460
|
const accTs = tyToTsStr(resolveExpr(rawArgs[1], ctx).ty);
|
|
487
461
|
const elemTs = tyToTsStr(fn.obj.ty.elem);
|
|
@@ -491,8 +465,7 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
491
465
|
return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
|
|
492
466
|
}
|
|
493
467
|
}
|
|
494
|
-
if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
495
|
-
["map", "filter", "every", "some", "find", "findLast", "findIndex", "findLastIndex"].includes(fn.field) &&
|
|
468
|
+
if (hofShape === "unary" && fn.kind === "field" && fn.obj.ty.kind === "array" &&
|
|
496
469
|
rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
|
|
497
470
|
rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
|
|
498
471
|
const elemTy = fn.obj.ty.elem;
|
|
@@ -513,7 +486,7 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
|
|
|
513
486
|
return a;
|
|
514
487
|
let pTy = paramTys[i];
|
|
515
488
|
if (pTy.kind === "user") {
|
|
516
|
-
const decl = ctx.typeDecls
|
|
489
|
+
const decl = declOf(ctx.typeDecls, pTy.name);
|
|
517
490
|
if (decl?.kind === "alias" && decl.aliasOfTy)
|
|
518
491
|
pTy = decl.aliasOfTy;
|
|
519
492
|
else if (decl?.kind === "alias" && decl.aliasOf)
|
|
@@ -584,68 +557,9 @@ function inferMethodReturnTy(fn, args, ctx) {
|
|
|
584
557
|
return { kind: "int" };
|
|
585
558
|
}
|
|
586
559
|
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
|
-
}
|
|
560
|
+
const id = recognizeBuiltin(objTy, fn.field);
|
|
561
|
+
if (id)
|
|
562
|
+
return builtinSpec(id).ret(objTy, args, { inSpec: ctx.inSpec });
|
|
649
563
|
return { kind: "unknown" };
|
|
650
564
|
}
|
|
651
565
|
/** Look up the type of `field` on `objTy`. Returns `unknown` if not found. */
|
|
@@ -657,7 +571,7 @@ function lookupFieldTy(objTy, field, ctx) {
|
|
|
657
571
|
return { ty: { kind: "nat" }, isDiscriminant: false };
|
|
658
572
|
}
|
|
659
573
|
if (objTy.kind === "user") {
|
|
660
|
-
const baseTyName =
|
|
574
|
+
const baseTyName = tyBaseName(objTy.name);
|
|
661
575
|
const isDiscriminant = getDiscriminant(ctx, baseTyName) === field;
|
|
662
576
|
const decl = findDecl(ctx, baseTyName);
|
|
663
577
|
if (decl?.kind === "record") {
|
|
@@ -691,7 +605,7 @@ function resolveRecordMerge(base, override, ctx) {
|
|
|
691
605
|
// into its inner type), else the base's.
|
|
692
606
|
const rTy = overInner.kind === "user" ? overInner
|
|
693
607
|
: tbase.ty.kind === "user" ? tbase.ty : null;
|
|
694
|
-
const decl = rTy ? ctx.typeDecls
|
|
608
|
+
const decl = rTy ? declOfKind(ctx.typeDecls, rTy.name, "record") : undefined;
|
|
695
609
|
if (!rTy || !decl?.fields) {
|
|
696
610
|
throw new Error(`object spread merge { ...a, ...b } needs a known record type for both operands ` +
|
|
697
611
|
`(base: ${tyToCanonical(tbase.ty)}, override: ${tyToCanonical(tover.ty)})`);
|
|
@@ -740,7 +654,7 @@ function tryRecordIndexByEnum(obj, idx, ctx) {
|
|
|
740
654
|
const objTy = obj.ty, keyTy = idx.ty;
|
|
741
655
|
if (objTy.kind !== "user")
|
|
742
656
|
return null;
|
|
743
|
-
const rec = ctx.typeDecls
|
|
657
|
+
const rec = declOfKind(ctx.typeDecls, objTy.name, "record");
|
|
744
658
|
if (!rec?.fields)
|
|
745
659
|
return null;
|
|
746
660
|
const fieldByName = new Map(rec.fields.map(f => [f.name, f]));
|
|
@@ -750,7 +664,7 @@ function tryRecordIndexByEnum(obj, idx, ctx) {
|
|
|
750
664
|
let values = null;
|
|
751
665
|
let datatype = null;
|
|
752
666
|
if (keyTy.kind === "user") {
|
|
753
|
-
const keyEnum = ctx.typeDecls
|
|
667
|
+
const keyEnum = declOfKind(ctx.typeDecls, keyTy.name, "string-union");
|
|
754
668
|
if (keyEnum?.values?.length) {
|
|
755
669
|
values = keyEnum.values;
|
|
756
670
|
datatype = keyEnum.name;
|
|
@@ -786,9 +700,11 @@ function resolveExpr(e, ctx) {
|
|
|
786
700
|
case "num":
|
|
787
701
|
if (!Number.isInteger(e.value))
|
|
788
702
|
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
703
|
return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
|
|
704
|
+
// Always `int` (never `nat`, even when non-negative), carrying `big` so the
|
|
705
|
+
// surrounding arithmetic picks bigint division semantics — see `isBigInt`.
|
|
706
|
+
case "bigint":
|
|
707
|
+
return { kind: "bigint", value: e.value, ty: { kind: "int", big: true } };
|
|
792
708
|
case "str":
|
|
793
709
|
return { kind: "str", value: e.value, ty: { kind: "string" } };
|
|
794
710
|
case "bool":
|
|
@@ -810,7 +726,7 @@ function resolveExpr(e, ctx) {
|
|
|
810
726
|
let rightCtx = ctx;
|
|
811
727
|
let rawRight = e.right;
|
|
812
728
|
if (e.op === "&&" || e.op === "==>") {
|
|
813
|
-
rightCtx = collectAndChainNarrowings(
|
|
729
|
+
rightCtx = collectAndChainNarrowings(left, ctx);
|
|
814
730
|
}
|
|
815
731
|
let right = resolveExpr(rawRight, rightCtx);
|
|
816
732
|
if (e.op === "===" || e.op === "!==") {
|
|
@@ -920,6 +836,12 @@ function resolveExpr(e, ctx) {
|
|
|
920
836
|
if (ty.kind === "unknown" && fn.kind === "var" && ctx.fnReturns.has(fn.name)) {
|
|
921
837
|
ty = ctx.fnReturns.get(fn.name);
|
|
922
838
|
}
|
|
839
|
+
// Call through a function-typed value: its fn type carries the result
|
|
840
|
+
if (ty.kind === "unknown" && fn.kind === "var") {
|
|
841
|
+
const varTy = lookup(ctx.env, fn.name);
|
|
842
|
+
if (varTy?.kind === "fn")
|
|
843
|
+
ty = varTy.result;
|
|
844
|
+
}
|
|
923
845
|
// filterMap: `seqOfOption.filter(x => x !== undefined)` (a defined-check,
|
|
924
846
|
// typically with an `x is T` type guard) drops the Nones AND unwraps to
|
|
925
847
|
// seq<T>. Rewrite to a synthetic `filterSome` call lowered to the proven
|
|
@@ -930,7 +852,9 @@ function resolveExpr(e, ctx) {
|
|
|
930
852
|
&& fn.kind === "field" && fn.obj.ty.kind === "array" && fn.obj.ty.elem.kind === "optional") {
|
|
931
853
|
return { kind: "call", fn: { ...fn, field: "filterSome" }, args: [], ty: { kind: "array", elem: fn.obj.ty.elem.inner }, callKind: "method" };
|
|
932
854
|
}
|
|
933
|
-
|
|
855
|
+
const builtinId = fn.kind === "field" ? recognizeBuiltin(fn.obj.ty, fn.field) : null;
|
|
856
|
+
return { kind: "call", fn, args, ty, callKind: classifyCall(e.fn, ctx),
|
|
857
|
+
...(builtinId ? { builtinId } : {}) };
|
|
934
858
|
}
|
|
935
859
|
case "index": {
|
|
936
860
|
const obj = resolveExpr(e.obj, ctx);
|
|
@@ -982,21 +906,41 @@ function resolveExpr(e, ctx) {
|
|
|
982
906
|
ty = np.narrowedTy;
|
|
983
907
|
}
|
|
984
908
|
}
|
|
909
|
+
// A variant narrowing on the object picks the field's type from that
|
|
910
|
+
// variant (a shared field name can have a different type per variant).
|
|
911
|
+
let ofVariant;
|
|
912
|
+
if (ty.kind === "unknown" && ctx.narrowedPaths.length > 0 && obj.ty.kind === "user") {
|
|
913
|
+
const objPath = asRawAccessPath(e.obj);
|
|
914
|
+
const np = objPath ? ctx.narrowedPaths.find(n => n.variant && accessPathsEqual(n.path, objPath)) : undefined;
|
|
915
|
+
if (np?.variant) {
|
|
916
|
+
const decl = findDecl(ctx, tyBaseName(obj.ty.name));
|
|
917
|
+
const f = decl?.variants?.find(v => v.name === np.variant)?.fields.find(f => f.name === e.field);
|
|
918
|
+
if (f?.type) {
|
|
919
|
+
ty = f.type;
|
|
920
|
+
ofVariant = np.variant;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
985
924
|
if (ty.kind === "unknown") {
|
|
986
925
|
const lookup = lookupFieldTy(obj.ty, e.field, ctx);
|
|
987
926
|
ty = lookup.ty;
|
|
988
927
|
isDiscriminant = lookup.isDiscriminant;
|
|
989
928
|
}
|
|
990
|
-
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
929
|
+
return { kind: "field", obj, field: e.field, ty, isDiscriminant, ofVariant };
|
|
991
930
|
}
|
|
992
931
|
case "nullish": {
|
|
993
932
|
// left ?? right — result type is left's inner (when left is optional)
|
|
994
933
|
// or just left's type, unified with right's type.
|
|
995
934
|
const left = resolveExpr(e.left, ctx);
|
|
996
|
-
const
|
|
935
|
+
const inner = left.ty.kind === "optional" ? left.ty.inner : left.ty;
|
|
997
936
|
// The default shares the result type, so coerce a string literal to a
|
|
998
937
|
// string-union enum (e.g. `availableLevels[0] ?? "off"`).
|
|
999
|
-
const right = coerceStr(resolveExpr(e.right, ctx),
|
|
938
|
+
const right = coerceStr(resolveExpr(e.right, ctx), inner);
|
|
939
|
+
// `??` is only total when its default is: with a nullable right operand
|
|
940
|
+
// (rule-chain style `ruleA(e) ?? ruleB(e) ?? null`), the result stays
|
|
941
|
+
// optional — otherwise the enclosing chain level loses its optionality
|
|
942
|
+
// and narrowing can't rewrite it.
|
|
943
|
+
const ty = right.ty.kind === "optional" ? right.ty : inner;
|
|
1000
944
|
return { kind: "nullish", left, right, ty };
|
|
1001
945
|
}
|
|
1002
946
|
case "optChain": {
|
|
@@ -1041,7 +985,9 @@ function resolveExpr(e, ctx) {
|
|
|
1041
985
|
const args = rawArgs.map(a => resolveExpr(a, ctx));
|
|
1042
986
|
callTy = inferMethodReturnTy(fakeFn, args, ctx);
|
|
1043
987
|
callKind = "method";
|
|
1044
|
-
|
|
988
|
+
const builtinId = recognizeBuiltin(priorInTy, lastField.name);
|
|
989
|
+
chain.push({ kind: "call", args, ty: callTy, callKind,
|
|
990
|
+
...(builtinId ? { builtinId } : {}) });
|
|
1045
991
|
stepInTy = callTy;
|
|
1046
992
|
continue;
|
|
1047
993
|
}
|
|
@@ -1070,15 +1016,33 @@ function resolveExpr(e, ctx) {
|
|
|
1070
1016
|
// has ctx.returnTy = Option<T>, but the record literal's natural type is T.
|
|
1071
1017
|
const returnTyUnwrapped = ctx.returnTy.kind === "optional" ? ctx.returnTy.inner : ctx.returnTy;
|
|
1072
1018
|
const recordTy = ty.kind === "user" ? ty : returnTyUnwrapped.kind === "user" ? returnTyUnwrapped : null;
|
|
1073
|
-
const decl = recordTy ? ctx.typeDecls
|
|
1019
|
+
const decl = recordTy ? declOfKind(ctx.typeDecls, recordTy.name, "record") : undefined;
|
|
1020
|
+
// Union-variant literal in union-typed context (a constructed IR node,
|
|
1021
|
+
// `{ kind: "if", … }: TStmt`): contextual field types come from the
|
|
1022
|
+
// variant the literal's discriminant field selects.
|
|
1023
|
+
let declFields = decl?.fields;
|
|
1024
|
+
if (!declFields && recordTy) {
|
|
1025
|
+
const udecl = declOfKind(ctx.typeDecls, recordTy.name, "discriminated-union");
|
|
1026
|
+
if (udecl?.discriminant && udecl.variants) {
|
|
1027
|
+
const tagRaw = e.fields.find(f => f.name === udecl.discriminant)?.value;
|
|
1028
|
+
if (tagRaw?.kind === "str") {
|
|
1029
|
+
declFields = udecl.variants.find(v => v.name === tagRaw.value)?.fields;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1074
1033
|
// Clear returnTy for field values — it applies to THIS record, not nested ones
|
|
1075
1034
|
const fieldCtx = recordTy ? { ...ctx, returnTy: { kind: "unknown" } } : ctx;
|
|
1076
1035
|
const fields = e.fields.map(f => {
|
|
1077
|
-
const fieldDecl =
|
|
1036
|
+
const fieldDecl = declFields?.find(df => df.name === f.name);
|
|
1078
1037
|
// Propagate declared field type into context so nested records resolve
|
|
1079
|
-
// their union variant correctly (e.g., { kind: 'Idle' } → EffectMode.Idle)
|
|
1080
|
-
|
|
1081
|
-
|
|
1038
|
+
// their union variant correctly (e.g., { kind: 'Idle' } → EffectMode.Idle).
|
|
1039
|
+
// Optional fields propagate their inner type (the Some-wrap is restored
|
|
1040
|
+
// by coerceToTargetTy below); array fields propagate whole, so the
|
|
1041
|
+
// arrayLiteral case can thread the element type.
|
|
1042
|
+
const fdTy = fieldDecl?.type;
|
|
1043
|
+
const fdCtxTy = fdTy?.kind === "optional" ? fdTy.inner : fdTy;
|
|
1044
|
+
const valueCtx = fdCtxTy && (fdCtxTy.kind === "user" || fdCtxTy.kind === "array")
|
|
1045
|
+
? { ...fieldCtx, returnTy: fdCtxTy }
|
|
1082
1046
|
: fieldCtx;
|
|
1083
1047
|
let value = resolveExpr(f.value, valueCtx);
|
|
1084
1048
|
if (fieldDecl) {
|
|
@@ -1132,8 +1096,10 @@ function resolveExpr(e, ctx) {
|
|
|
1132
1096
|
// literal in an array resolves to its named datatype rather than an
|
|
1133
1097
|
// anonymous tuple (mirrors return-position and call-argument records, which
|
|
1134
1098
|
// get their type via ctx.returnTy). Only narrow when the context type is an
|
|
1135
|
-
// array
|
|
1136
|
-
|
|
1099
|
+
// array (unwrapping one optional level — `TStmt[] | null` return positions);
|
|
1100
|
+
// otherwise leave ctx untouched.
|
|
1101
|
+
const rtUnwrapped = ctx.returnTy.kind === "optional" ? ctx.returnTy.inner : ctx.returnTy;
|
|
1102
|
+
const expectedElem = rtUnwrapped.kind === "array" ? rtUnwrapped.elem : null;
|
|
1137
1103
|
const elemCtx = expectedElem ? { ...ctx, returnTy: expectedElem } : ctx;
|
|
1138
1104
|
const elems = e.elems.map(el => {
|
|
1139
1105
|
const r = resolveExpr(el, elemCtx);
|
|
@@ -1183,19 +1149,19 @@ function resolveExpr(e, ctx) {
|
|
|
1183
1149
|
// with method calls or index ops (bind-first required).
|
|
1184
1150
|
// For &&-chains, all positive checks narrow the then-branch; earlier
|
|
1185
1151
|
// checks are in scope when resolving later ones.
|
|
1186
|
-
let thenCtx = collectAndChainNarrowings(
|
|
1152
|
+
let thenCtx = collectAndChainNarrowings(cond, ctx);
|
|
1187
1153
|
let elseCtx = ctx;
|
|
1188
1154
|
// 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,
|
|
1155
|
+
if (cond.ty.kind === "optional" && cond.kind === "var") {
|
|
1156
|
+
thenCtx = withEnv(thenCtx, extend(thenCtx.env, cond.name, cond.ty.inner));
|
|
1191
1157
|
}
|
|
1192
1158
|
// Single === undefined check narrows the else-branch.
|
|
1193
|
-
const single =
|
|
1194
|
-
if (single
|
|
1159
|
+
const single = negatedVarPresence(cond);
|
|
1160
|
+
if (single) {
|
|
1195
1161
|
elseCtx = withEnv(elseCtx, extend(elseCtx.env, single.varName, single.innerTy));
|
|
1196
1162
|
}
|
|
1197
|
-
if (!single &&
|
|
1198
|
-
for (const n of collectEarlyReturnNarrowings(
|
|
1163
|
+
if (!single && cond.kind === "binop" && cond.op === "||") {
|
|
1164
|
+
for (const n of collectEarlyReturnNarrowings(cond)) {
|
|
1199
1165
|
elseCtx = withEnv(elseCtx, extend(elseCtx.env, n.varName, n.innerTy));
|
|
1200
1166
|
}
|
|
1201
1167
|
}
|
|
@@ -1256,8 +1222,9 @@ function resolveBlock(stmts, ctx) {
|
|
|
1256
1222
|
const result = [];
|
|
1257
1223
|
let env = ctx.env;
|
|
1258
1224
|
let narrowedIndices = ctx.narrowedIndices;
|
|
1225
|
+
let narrowedPaths = ctx.narrowedPaths;
|
|
1259
1226
|
for (const s of stmts) {
|
|
1260
|
-
const currentCtx = { ...ctx, env, narrowedIndices };
|
|
1227
|
+
const currentCtx = { ...ctx, env, narrowedIndices, narrowedPaths };
|
|
1261
1228
|
const [typed, nextEnv] = resolveStmt(s, currentCtx);
|
|
1262
1229
|
result.push(typed);
|
|
1263
1230
|
env = nextEnv;
|
|
@@ -1269,9 +1236,13 @@ function resolveBlock(stmts, ctx) {
|
|
|
1269
1236
|
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
1270
1237
|
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
1271
1238
|
if (s.kind === "if" && s.then.length > 0 && isTerminatorKind(s.then[s.then.length - 1].kind) && s.else.length === 0) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1239
|
+
if (typed.kind === "if") {
|
|
1240
|
+
for (const n of collectEarlyReturnNarrowings(typed.cond)) {
|
|
1241
|
+
env = extend(env, n.varName, n.innerTy);
|
|
1242
|
+
}
|
|
1243
|
+
const nv = negatedVariantCheck(typed.cond);
|
|
1244
|
+
if (nv)
|
|
1245
|
+
narrowedPaths = [...narrowedPaths, nv];
|
|
1275
1246
|
}
|
|
1276
1247
|
// Map-index narrowing: `if (!(k in m)) return;` means `k in m` holds in rest.
|
|
1277
1248
|
if (typed.kind === "if") {
|
|
@@ -1308,8 +1279,11 @@ function resolveStmt(s, ctx) {
|
|
|
1308
1279
|
// Propagate declared type as returnTy so nested record expressions resolve
|
|
1309
1280
|
// union variants correctly (e.g., EffectState → mode: EffectMode → { kind:
|
|
1310
1281
|
// 'Idle' }). Arrays too, so `const xs: Foo[] = [{...}]` threads the element
|
|
1311
|
-
// type into the array literal (see the arrayLiteral case).
|
|
1312
|
-
|
|
1282
|
+
// type into the array literal (see the arrayLiteral case). Optionals too
|
|
1283
|
+
// (`const r: TExpr | null = cond ? {…} : null`) — the record case unwraps
|
|
1284
|
+
// one optional level when consulting returnTy.
|
|
1285
|
+
const initCtx = (declTy.kind === "user" || declTy.kind === "array" || declTy.kind === "optional")
|
|
1286
|
+
? { ...ctx, returnTy: declTy } : ctx;
|
|
1313
1287
|
const init = coerceStr(resolveExpr(s.init, initCtx), declTy);
|
|
1314
1288
|
let ty;
|
|
1315
1289
|
if (isUnmodeledTy(declTy, ctx.typeDecls) && !isUnmodeledTy(init.ty, ctx.typeDecls)) {
|
|
@@ -1342,7 +1316,12 @@ function resolveStmt(s, ctx) {
|
|
|
1342
1316
|
}
|
|
1343
1317
|
case "assign": {
|
|
1344
1318
|
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
1345
|
-
|
|
1319
|
+
// Propagate the target's type as returnTy, mirroring the annotated-let
|
|
1320
|
+
// case, so record/union literals and array literals in the RHS resolve
|
|
1321
|
+
// to their named datatypes.
|
|
1322
|
+
const valueCtx = (targetTy.kind === "user" || targetTy.kind === "array" || targetTy.kind === "optional")
|
|
1323
|
+
? { ...ctx, returnTy: targetTy } : ctx;
|
|
1324
|
+
let value = coerceStr(resolveExpr(s.value, valueCtx), targetTy);
|
|
1346
1325
|
// Auto-wrap non-optional value in Some when target is optional
|
|
1347
1326
|
const isUndef = value.kind === "var" && value.name === "undefined";
|
|
1348
1327
|
if (targetTy.kind === "optional" && value.ty.kind !== "optional" && value.ty.kind !== "unknown" && !isUndef) {
|
|
@@ -1371,16 +1350,16 @@ function resolveStmt(s, ctx) {
|
|
|
1371
1350
|
// For &&-chains, all positive optional checks narrow the then-branch;
|
|
1372
1351
|
// earlier checks are in scope when resolving later ones.
|
|
1373
1352
|
// Single-check === undefined narrows the else-branch.
|
|
1374
|
-
|
|
1353
|
+
const resolvedCond = resolveExpr(s.cond, ctx);
|
|
1354
|
+
let thenCtx = collectAndChainNarrowings(resolvedCond, ctx);
|
|
1375
1355
|
let elseCtx = ctx;
|
|
1376
|
-
const single =
|
|
1377
|
-
if (single
|
|
1356
|
+
const single = negatedVarPresence(resolvedCond);
|
|
1357
|
+
if (single) {
|
|
1378
1358
|
elseCtx = withEnv(ctx, extend(ctx.env, single.varName, single.innerTy));
|
|
1379
1359
|
}
|
|
1380
1360
|
// Narrow map index access across `k in m` / `!(k in m)` in the cond:
|
|
1381
1361
|
// positive atoms (from `k in m` or &&-chains containing it) → then-branch;
|
|
1382
1362
|
// negated atoms (from `!(k in m)`) → else-branch.
|
|
1383
|
-
const resolvedCond = resolveExpr(s.cond, ctx);
|
|
1384
1363
|
thenCtx = withInAtoms(thenCtx, extractInAtoms(resolvedCond));
|
|
1385
1364
|
elseCtx = withInAtoms(elseCtx, extractInAtomsNegated(resolvedCond));
|
|
1386
1365
|
return [{ kind: "if", cond: resolvedCond, then: resolveBlock(s.then, thenCtx), else: resolveBlock(s.else, elseCtx) }, ctx.env];
|