@scriptc/compiler 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ambient/scriptc-node-fallback.d.ts +45 -17
- package/ambient/scriptc.d.ts +2 -1
- package/dist/backend/build-cache.js.map +1 -1
- package/dist/backend/c/async.js +24 -2
- package/dist/backend/c/async.js.map +1 -1
- package/dist/backend/c/c-emitter.d.ts +22 -10
- package/dist/backend/c/c-emitter.js +60 -15
- package/dist/backend/c/c-emitter.js.map +1 -1
- package/dist/backend/c/exprs.js +220 -10
- package/dist/backend/c/exprs.js.map +1 -1
- package/dist/backend/c/island.js.map +1 -1
- package/dist/backend/c/may-throw.js.map +1 -1
- package/dist/backend/c/stmts.d.ts +3 -5
- package/dist/backend/c/stmts.js +58 -17
- package/dist/backend/c/stmts.js.map +1 -1
- package/dist/backend/c/types.js.map +1 -1
- package/dist/backend/c/walkers.js +31 -5
- package/dist/backend/c/walkers.js.map +1 -1
- package/dist/backend/external-c.js.map +1 -1
- package/dist/backend/linker.js.map +1 -1
- package/dist/backend/llvm/blocks.js.map +1 -1
- package/dist/backend/llvm/dyn.js +56 -3
- package/dist/backend/llvm/dyn.js.map +1 -1
- package/dist/backend/llvm/emitter.js +98 -19
- package/dist/backend/llvm/emitter.js.map +1 -1
- package/dist/backend/llvm/expr-async.js +85 -3
- package/dist/backend/llvm/expr-async.js.map +1 -1
- package/dist/backend/llvm/expr-bytes.js.map +1 -1
- package/dist/backend/llvm/expr-calls.js.map +1 -1
- package/dist/backend/llvm/expr-containers.js.map +1 -1
- package/dist/backend/llvm/expr-context.d.ts +1 -0
- package/dist/backend/llvm/expr-control.js.map +1 -1
- package/dist/backend/llvm/expr-dispatch.js.map +1 -1
- package/dist/backend/llvm/expr-dynamic.js +6 -3
- package/dist/backend/llvm/expr-dynamic.js.map +1 -1
- package/dist/backend/llvm/expr-island.js.map +1 -1
- package/dist/backend/llvm/expr-primitives.js.map +1 -1
- package/dist/backend/llvm/expr-records.js.map +1 -1
- package/dist/backend/llvm/expr-stream-bridges.js +220 -9
- package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
- package/dist/backend/llvm/lib-dispatch.js +2 -0
- package/dist/backend/llvm/lib-dispatch.js.map +1 -1
- package/dist/backend/llvm/lib-shared.js +18 -0
- package/dist/backend/llvm/lib-shared.js.map +1 -1
- package/dist/backend/llvm/lib-stream.js +6 -3
- package/dist/backend/llvm/lib-stream.js.map +1 -1
- package/dist/backend/llvm/split.js.map +1 -1
- package/dist/backend/llvm/unsupported.js.map +1 -1
- package/dist/backend/llvm/walkers.js.map +1 -1
- package/dist/backend/native-codegen.js.map +1 -1
- package/dist/backend/native-codegen.test.js +2 -0
- package/dist/backend/native-codegen.test.js.map +1 -1
- package/dist/backend/native-toolchain.js.map +1 -1
- package/dist/backend/native-toolchain.test.js.map +1 -1
- package/dist/backend/runtime-pack.js.map +1 -1
- package/dist/backend/targets.js.map +1 -1
- package/dist/backend/vendor-archives.js.map +1 -1
- package/dist/compat/fetch-profile.js +4 -2
- package/dist/compat/fetch-profile.js.map +1 -1
- package/dist/coverage/report.js.map +1 -1
- package/dist/coverage/surface-manifest.js +12 -0
- package/dist/coverage/surface-manifest.js.map +1 -1
- package/dist/diagnostics/diagnostic.d.ts +6 -5
- package/dist/diagnostics/diagnostic.js +7 -6
- package/dist/diagnostics/diagnostic.js.map +1 -1
- package/dist/diagnostics/render.js.map +1 -1
- package/dist/ffi/ffi-manifest.js.map +1 -1
- package/dist/frontend/cjs-lexer.js.map +1 -1
- package/dist/frontend/dts-paths.js.map +1 -1
- package/dist/frontend/input-tracker.d.ts +10 -0
- package/dist/frontend/input-tracker.js +26 -3
- package/dist/frontend/input-tracker.js.map +1 -1
- package/dist/frontend/input-tracker.test.js +8 -1
- package/dist/frontend/input-tracker.test.js.map +1 -1
- package/dist/frontend/lib-contract.js.map +1 -1
- package/dist/frontend/lowering/array-values.d.ts +4 -0
- package/dist/frontend/lowering/array-values.js +18 -2
- package/dist/frontend/lowering/array-values.js.map +1 -1
- package/dist/frontend/lowering/callback-arg.js.map +1 -1
- package/dist/frontend/lowering/containers/bytes.d.ts +35 -0
- package/dist/frontend/lowering/containers/bytes.js +1021 -0
- package/dist/frontend/lowering/containers/bytes.js.map +1 -0
- package/dist/frontend/lowering/containers/string-and-regexp.d.ts +17 -0
- package/dist/frontend/lowering/containers/string-and-regexp.js +345 -0
- package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -0
- package/dist/frontend/lowering/expressions/evaluation-safety.d.ts +13 -0
- package/dist/frontend/lowering/expressions/evaluation-safety.js +76 -0
- package/dist/frontend/lowering/expressions/evaluation-safety.js.map +1 -0
- package/dist/frontend/lowering/expressions/object-literals.d.ts +44 -0
- package/dist/frontend/lowering/expressions/object-literals.js +1815 -0
- package/dist/frontend/lowering/expressions/object-literals.js.map +1 -0
- package/dist/frontend/lowering/expressions/optional-chains.d.ts +31 -0
- package/dist/frontend/lowering/expressions/optional-chains.js +382 -0
- package/dist/frontend/lowering/expressions/optional-chains.js.map +1 -0
- package/dist/frontend/lowering/expressions/try-lower-expression.d.ts +7 -0
- package/dist/frontend/lowering/expressions/try-lower-expression.js +26 -0
- package/dist/frontend/lowering/expressions/try-lower-expression.js.map +1 -0
- package/dist/frontend/lowering/lib-boundary.js +7 -2
- package/dist/frontend/lowering/lib-boundary.js.map +1 -1
- package/dist/frontend/lowering/lower-assert.js.map +1 -1
- package/dist/frontend/lowering/lower-async-iteration.d.ts +7 -0
- package/dist/frontend/lowering/lower-async-iteration.js +657 -0
- package/dist/frontend/lowering/lower-async-iteration.js.map +1 -0
- package/dist/frontend/lowering/lower-builtins.d.ts +18 -0
- package/dist/frontend/lowering/lower-builtins.js +219 -48
- package/dist/frontend/lowering/lower-builtins.js.map +1 -1
- package/dist/frontend/lowering/lower-calls.d.ts +13 -4
- package/dist/frontend/lowering/lower-calls.js +407 -64
- package/dist/frontend/lowering/lower-calls.js.map +1 -1
- package/dist/frontend/lowering/lower-classes.d.ts +7 -13
- package/dist/frontend/lowering/lower-classes.js +128 -35
- package/dist/frontend/lowering/lower-classes.js.map +1 -1
- package/dist/frontend/lowering/lower-comptime.js.map +1 -1
- package/dist/frontend/lowering/lower-containers.d.ts +1 -47
- package/dist/frontend/lowering/lower-containers.js +135 -1467
- package/dist/frontend/lowering/lower-containers.js.map +1 -1
- package/dist/frontend/lowering/lower-event-emitter.js +51 -5
- package/dist/frontend/lowering/lower-event-emitter.js.map +1 -1
- package/dist/frontend/lowering/lower-exprs.d.ts +0 -78
- package/dist/frontend/lowering/lower-exprs.js +402 -2351
- package/dist/frontend/lowering/lower-exprs.js.map +1 -1
- package/dist/frontend/lowering/lower-generators.d.ts +18 -3
- package/dist/frontend/lowering/lower-generators.js +145 -9
- package/dist/frontend/lowering/lower-generators.js.map +1 -1
- package/dist/frontend/lowering/lower-inspect.js +3 -3
- package/dist/frontend/lowering/lower-inspect.js.map +1 -1
- package/dist/frontend/lowering/lower-island.d.ts +6 -0
- package/dist/frontend/lowering/lower-island.js +37 -5
- package/dist/frontend/lowering/lower-island.js.map +1 -1
- package/dist/frontend/lowering/lower-modules.js +52 -15
- package/dist/frontend/lowering/lower-modules.js.map +1 -1
- package/dist/frontend/lowering/lower-node-module.d.ts +18 -0
- package/dist/frontend/lowering/lower-node-module.js +302 -0
- package/dist/frontend/lowering/lower-node-module.js.map +1 -0
- package/dist/frontend/lowering/lower-server.js +1 -1
- package/dist/frontend/lowering/lower-server.js.map +1 -1
- package/dist/frontend/lowering/lower-stmts.d.ts +5 -24
- package/dist/frontend/lowering/lower-stmts.js +519 -253
- package/dist/frontend/lowering/lower-stmts.js.map +1 -1
- package/dist/frontend/lowering/lower-stream.js +4 -4
- package/dist/frontend/lowering/lower-stream.js.map +1 -1
- package/dist/frontend/lowering/lowerer.d.ts +37 -34
- package/dist/frontend/lowering/lowerer.js +143 -57
- package/dist/frontend/lowering/lowerer.js.map +1 -1
- package/dist/frontend/lowering/optional-arguments.d.ts +9 -0
- package/dist/frontend/lowering/optional-arguments.js +57 -0
- package/dist/frontend/lowering/optional-arguments.js.map +1 -0
- package/dist/frontend/npm-static-declarations.d.ts +35 -0
- package/dist/frontend/npm-static-declarations.js +506 -0
- package/dist/frontend/npm-static-declarations.js.map +1 -0
- package/dist/frontend/npm-static-declarations.test.d.ts +1 -0
- package/dist/frontend/npm-static-declarations.test.js +122 -0
- package/dist/frontend/npm-static-declarations.test.js.map +1 -0
- package/dist/frontend/npm-static.d.ts +2 -0
- package/dist/frontend/npm-static.js +26 -10
- package/dist/frontend/npm-static.js.map +1 -1
- package/dist/frontend/npm.d.ts +60 -0
- package/dist/frontend/npm.js +38 -4
- package/dist/frontend/npm.js.map +1 -1
- package/dist/frontend/npm.test.d.ts +1 -0
- package/dist/frontend/npm.test.js +50 -0
- package/dist/frontend/npm.test.js.map +1 -0
- package/dist/frontend/program.d.ts +3 -2
- package/dist/frontend/program.js +495 -68
- package/dist/frontend/program.js.map +1 -1
- package/dist/frontend/resolve.d.ts +10 -2
- package/dist/frontend/resolve.js +90 -14
- package/dist/frontend/resolve.js.map +1 -1
- package/dist/frontend/resolve.test.js +4 -1
- package/dist/frontend/resolve.test.js.map +1 -1
- package/dist/frontend/runtime-resolve.d.ts +22 -0
- package/dist/frontend/runtime-resolve.js +101 -0
- package/dist/frontend/runtime-resolve.js.map +1 -0
- package/dist/frontend/runtime-resolve.test.d.ts +1 -0
- package/dist/frontend/runtime-resolve.test.js +18 -0
- package/dist/frontend/runtime-resolve.test.js.map +1 -0
- package/dist/frontend/ts7/adapter.js +12 -13
- package/dist/frontend/ts7/adapter.js.map +1 -1
- package/dist/frontend/ts7/ast.js.map +1 -1
- package/dist/frontend/ts7/census-check.js +3 -3
- package/dist/frontend/ts7/census-check.js.map +1 -1
- package/dist/frontend/ts7/checker.d.ts +3 -0
- package/dist/frontend/ts7/checker.js +12 -3
- package/dist/frontend/ts7/checker.js.map +1 -1
- package/dist/frontend/ts7/enums.js.map +1 -1
- package/dist/frontend/ts7/program-adapter.js +42 -3
- package/dist/frontend/ts7/program-adapter.js.map +1 -1
- package/dist/frontend/type-mapper.js +120 -7
- package/dist/frontend/type-mapper.js.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/ir/analysis.js.map +1 -1
- package/dist/ir/ir.d.ts +60 -30
- package/dist/ir/ir.js +57 -6
- package/dist/ir/ir.js.map +1 -1
- package/dist/ir/validate.js +44 -59
- package/dist/ir/validate.js.map +1 -1
- package/dist/library/cache-primitives.js.map +1 -1
- package/dist/library/cache-reset.test.js.map +1 -1
- package/dist/library/compiler-self-identity.js.map +1 -1
- package/dist/library/int-infer.js.map +1 -1
- package/dist/library/int-infer.test.js.map +1 -1
- package/dist/library/library-profile.js.map +1 -1
- package/dist/library/sidecar-validate.js.map +1 -1
- package/dist/library/sidecar.js.map +1 -1
- package/dist/startup-cache.js.map +1 -1
- package/dist/wasi-paths.js.map +1 -1
- package/package.json +18 -18
- package/surface-manifest.json +60 -4
|
@@ -9,12 +9,12 @@ import * as ts from "../ts7/adapter.js";
|
|
|
9
9
|
import { dirname, posix } from "node:path";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
import { wasiGuestPath } from "../../wasi-paths.js";
|
|
12
|
-
import { BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canBoxFuncIntoDyn,
|
|
12
|
+
import { BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionFuncSetArmsOk } from "../../ir/ir.js";
|
|
13
13
|
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
|
|
14
14
|
import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
|
|
15
|
-
import { blockedBindingUseDiag,
|
|
15
|
+
import { blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
|
16
16
|
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
|
|
17
|
-
import {
|
|
17
|
+
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
|
|
18
18
|
import { arrayValueStore } from "./array-values.js";
|
|
19
19
|
import { npmStaticPackageOfPath } from "../npm-static.js";
|
|
20
20
|
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
|
|
@@ -29,6 +29,11 @@ import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTy
|
|
|
29
29
|
import { lowerYield } from "./lower-generators.js";
|
|
30
30
|
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
|
|
31
31
|
import { countedFor, numLit, varRef } from "../../ir/build.js";
|
|
32
|
+
import { isSafeToDiscard, isSafeToMoveConditionEarlier, isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
|
33
|
+
import { hasOptionalChainGuard, isOptionalChainTail, isRequireMainFilename } from "./expressions/optional-chains.js";
|
|
34
|
+
import { conditionalSpreadOf, foldedStringKeyOf } from "./expressions/object-literals.js";
|
|
35
|
+
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
|
36
|
+
import { fenceNodeModuleMutation, isNodeModuleValue, lowerNodeModuleIdentifier, lowerNodeModuleProperty, lowerRequireCacheElement, lowerRequireCacheHas, lowerRequireMainProperty } from "./lower-node-module.js";
|
|
32
37
|
/** A template piece's RAW text (String.raw's contract: escapes stay
|
|
33
38
|
* characters). 7's client AST ships no rawText at runtime (the typing
|
|
34
39
|
* declares it; the serialized node data omits it), so the raw span comes
|
|
@@ -405,7 +410,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
405
410
|
lowerer.unsupported("SC1090", prop, "a conditional spread mixed with plain spreads in an 'any'-typed object literal");
|
|
406
411
|
}
|
|
407
412
|
const cond = lowerer.lowerCondition(cs.cond);
|
|
408
|
-
if (!
|
|
413
|
+
if (!isSafeToMoveConditionEarlier(cond)) {
|
|
409
414
|
lowerer.unsupported("SC1090", prop, "conditional spreads with effectful conditions in an 'any'-typed object literal (bind the condition to a const first)");
|
|
410
415
|
}
|
|
411
416
|
spread = { cond, whenTrue: cs.whenTrue };
|
|
@@ -533,6 +538,14 @@ function lowerExprInner(lowerer, expr) {
|
|
|
533
538
|
return { kind: "jsOp", op: "arrLit", args, type: JSVAL, loc };
|
|
534
539
|
}
|
|
535
540
|
if (ts.isTypeOfExpression(expr)) {
|
|
541
|
+
if (isNodeModuleValue(lowerer, expr.expression)) {
|
|
542
|
+
return { kind: "strLit", value: "object", type: STRING, loc };
|
|
543
|
+
}
|
|
544
|
+
if (ts.isPropertyAccessExpression(expr.expression)) {
|
|
545
|
+
const presence = lowerPromiseThenPresence(lowerer, expr.expression, { kind: "strLit", value: "function", type: STRING, loc });
|
|
546
|
+
if (presence !== null)
|
|
547
|
+
return presence;
|
|
548
|
+
}
|
|
536
549
|
// `typeof queueMicrotask` / `typeof DOMException` on a STDLIB global
|
|
537
550
|
// whose declared type is callable or constructable: folds to
|
|
538
551
|
// "function" BEFORE the operand lowers — the identity-token story
|
|
@@ -578,7 +591,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
578
591
|
undefinedT: "undefined", nullT: "object",
|
|
579
592
|
};
|
|
580
593
|
const folded = FOLD[operand.type.kind];
|
|
581
|
-
if (folded &&
|
|
594
|
+
if (folded && isSafeToDiscard(operand)) {
|
|
582
595
|
return { kind: "strLit", value: folded, type: STRING, loc };
|
|
583
596
|
}
|
|
584
597
|
// A union operand: every arm's typeof answer is static, so the value
|
|
@@ -587,7 +600,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
587
600
|
// tests, so only side-effect-free reads compose — and when every arm
|
|
588
601
|
// agrees the whole expression folds to that one string (dropping only
|
|
589
602
|
// the pure read, the trust-the-checker bet lowerUnitComparison makes).
|
|
590
|
-
if (operand.type.kind === "union" &&
|
|
603
|
+
if (operand.type.kind === "union" && isSafeToRepeat(operand)) {
|
|
591
604
|
const def = lowerer.unions.get(operand.type.unionId);
|
|
592
605
|
const answers = def?.arms.map(typeofAnswer);
|
|
593
606
|
if (def && answers && answers.every((s) => s !== null)) {
|
|
@@ -614,6 +627,9 @@ function lowerExprInner(lowerer, expr) {
|
|
|
614
627
|
lowerer.unsupported("SC1090", expr, "typeof expressions on statically-typed values");
|
|
615
628
|
}
|
|
616
629
|
if (ts.isIdentifier(expr)) {
|
|
630
|
+
const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
|
|
631
|
+
if (moduleValue)
|
|
632
|
+
return moduleValue;
|
|
617
633
|
if (lowerer.isSelfReference(expr)) {
|
|
618
634
|
return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
|
|
619
635
|
}
|
|
@@ -1276,8 +1292,8 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1276
1292
|
// The tail of an optional chain whose `?.` sits deeper
|
|
1277
1293
|
// (`x?.trim().toLowerCase()`): the whole tail short-circuits with
|
|
1278
1294
|
// the guard, so it lowers as one chain (dyn/island receivers keep
|
|
1279
|
-
// their own undefined-propagating reads — see
|
|
1280
|
-
if (
|
|
1295
|
+
// their own undefined-propagating reads — see isOptionalChainTail).
|
|
1296
|
+
if (isOptionalChainTail(lowerer, expr))
|
|
1281
1297
|
return lowerer.lowerOptionalChain(expr);
|
|
1282
1298
|
return lowerer.lowerCall(expr);
|
|
1283
1299
|
}
|
|
@@ -1325,6 +1341,17 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1325
1341
|
if (isRequireMainFilename(lowerer, expr)) {
|
|
1326
1342
|
return { kind: "strLit", value: lowerer.entry.fileName, type: STRING, loc };
|
|
1327
1343
|
}
|
|
1344
|
+
const requireMain = lowerRequireMainProperty(lowerer, expr);
|
|
1345
|
+
if (requireMain)
|
|
1346
|
+
return requireMain;
|
|
1347
|
+
const moduleProperty = lowerNodeModuleProperty(lowerer, expr);
|
|
1348
|
+
if (moduleProperty)
|
|
1349
|
+
return moduleProperty;
|
|
1350
|
+
if (expr.questionDotToken && expr.name.text === "electron") {
|
|
1351
|
+
const processProperty = lowerer.lowerProcessProperty(expr);
|
|
1352
|
+
if (processProperty)
|
|
1353
|
+
return processProperty;
|
|
1354
|
+
}
|
|
1328
1355
|
// Optional chaining `a?.b`: the guard lowers here (a tag test around
|
|
1329
1356
|
// the plain property lowering below); the handled marker keeps this
|
|
1330
1357
|
// re-entrant dispatch from looping.
|
|
@@ -1333,7 +1360,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1333
1360
|
}
|
|
1334
1361
|
// `a?.b.c` — the tail of a chain whose token sits deeper: the whole
|
|
1335
1362
|
// tail short-circuits with the guard.
|
|
1336
|
-
if (!expr.questionDotToken &&
|
|
1363
|
+
if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
|
|
1337
1364
|
return lowerer.lowerOptionalChain(expr);
|
|
1338
1365
|
}
|
|
1339
1366
|
// Island receiver: o.x is an engine property read (getProp may throw
|
|
@@ -1662,7 +1689,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1662
1689
|
const recv = lowerer.lowerExpr(expr.expression);
|
|
1663
1690
|
if (recv.type.kind === "dyn") {
|
|
1664
1691
|
const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
|
|
1665
|
-
const opt =
|
|
1692
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
1666
1693
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc }, expr);
|
|
1667
1694
|
}
|
|
1668
1695
|
}
|
|
@@ -1676,7 +1703,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1676
1703
|
// `f.call` as a value). JS files only; TS keeps the fence.
|
|
1677
1704
|
if (!["apply", "bind", "call", "toString", "constructor", "prototype", "caller", "arguments"].includes(expr.name.text) &&
|
|
1678
1705
|
isJsSourceFile(expr.getSourceFile())) {
|
|
1679
|
-
const probed =
|
|
1706
|
+
const probed = tryLowerExpression(lowerer, expr.expression);
|
|
1680
1707
|
if (probed?.type.kind === "func" &&
|
|
1681
1708
|
canBoxFuncIntoDyn(probed.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
|
|
1682
1709
|
const fnName = jsFuncNameOf(expr.expression);
|
|
@@ -1741,7 +1768,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1741
1768
|
type: STRING,
|
|
1742
1769
|
loc: locOf(expr.name),
|
|
1743
1770
|
};
|
|
1744
|
-
const opt =
|
|
1771
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
1745
1772
|
return lowerer.maybeNarrow({
|
|
1746
1773
|
kind: "dynKeyGet",
|
|
1747
1774
|
key,
|
|
@@ -1801,7 +1828,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1801
1828
|
// validated dynCheck as usual.
|
|
1802
1829
|
if (recvLowered.type.kind === "dyn") {
|
|
1803
1830
|
const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
|
|
1804
|
-
const opt =
|
|
1831
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
1805
1832
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recvLowered, type: DYN, loc }, expr);
|
|
1806
1833
|
}
|
|
1807
1834
|
// The lowered receiver is a RECORD the checker spelled wider —
|
|
@@ -1868,9 +1895,6 @@ function importMetaProperty(lowerer, expr) {
|
|
|
1868
1895
|
return null;
|
|
1869
1896
|
const sf = expr.getSourceFile();
|
|
1870
1897
|
const fileName = moduleFileName(lowerer, sf);
|
|
1871
|
-
if (lowerer.dynamic) {
|
|
1872
|
-
lowerer.unsupported("SC1090", expr, `'import.meta.${expr.name.text}' in a dynamic island (module-loader metadata is supported only in static ESM)`);
|
|
1873
|
-
}
|
|
1874
1898
|
switch (expr.name.text) {
|
|
1875
1899
|
case "url":
|
|
1876
1900
|
return {
|
|
@@ -2437,7 +2461,7 @@ export function lowerUnitComparison(lowerer, left, right, negated, loc) {
|
|
|
2437
2461
|
// callback's effects are observable) rides one throwaway tag
|
|
2438
2462
|
// test; droppable reads fold to the bare literal.
|
|
2439
2463
|
const answer = { kind: "boolLit", value: negated, type: BOOL, loc };
|
|
2440
|
-
if (
|
|
2464
|
+
if (isSafeToDiscard(other))
|
|
2441
2465
|
return answer;
|
|
2442
2466
|
const evalOnce = {
|
|
2443
2467
|
kind: "unionIsTag", unionId: other.type.unionId, tag: 0, negated: false, value: other, type: BOOL, loc,
|
|
@@ -2545,7 +2569,7 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
|
|
|
2545
2569
|
// backend emits it per test — restricted to re-emittable pure reads;
|
|
2546
2570
|
// anything effectful gets its own actionable fence (the generic
|
|
2547
2571
|
// SC1040 hint would wrongly suggest `== null` as the fix).
|
|
2548
|
-
if (!
|
|
2572
|
+
if (!isSafeToRepeat(other)) {
|
|
2549
2573
|
lowerer.unsupported("SC1090", expr, `'== null' / '!= null' on a '${lowerer.fmt(ut)}' value that isn't a plain read ` +
|
|
2550
2574
|
`(both unit arms need the operand twice — bind it to a const first)`);
|
|
2551
2575
|
}
|
|
@@ -2575,88 +2599,6 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
|
|
|
2575
2599
|
// `== null` is statically false, `!= null` statically true.
|
|
2576
2600
|
return { kind: "boolLit", value: negated, type: BOOL, loc };
|
|
2577
2601
|
}
|
|
2578
|
-
/** Safe to EMIT twice: plain reads with no side effects — local/global
|
|
2579
|
-
* reads and record/class field reads over such (accessor properties
|
|
2580
|
-
* lower to getter CALLS, so they never appear as field reads). Used by
|
|
2581
|
-
* the two-unit-arm `== null` composition, the union `typeof` forms, and
|
|
2582
|
-
* the union-switch desugar, whose operands ride several tests. */
|
|
2583
|
-
/** A static expression whose evaluation is unobservable — safe to DROP
|
|
2584
|
-
* (typeof's constant fold: JS evaluates the operand, but when nothing in
|
|
2585
|
-
* it can throw, call, write, or allocate observably, folding it away
|
|
2586
|
-
* changes nothing). Strictly wider than pureReemittable: numeric
|
|
2587
|
-
* arithmetic (f64 ops never throw — division by zero is Infinity),
|
|
2588
|
-
* string concatenation, logical/ternary composition, and unit/literal
|
|
2589
|
-
* leaves compose; anything with a call, an island op, an index read (can
|
|
2590
|
-
* trap), or an unknown kind answers false. */
|
|
2591
|
-
export function droppableStatic(e) {
|
|
2592
|
-
switch (e.kind) {
|
|
2593
|
-
case "numLit":
|
|
2594
|
-
case "strLit":
|
|
2595
|
-
case "boolLit":
|
|
2596
|
-
case "unitLit":
|
|
2597
|
-
return true;
|
|
2598
|
-
case "bin":
|
|
2599
|
-
return droppableStatic(e.left) && droppableStatic(e.right);
|
|
2600
|
-
case "unary":
|
|
2601
|
-
return droppableStatic(e.operand);
|
|
2602
|
-
case "logical":
|
|
2603
|
-
return droppableStatic(e.left) && droppableStatic(e.right);
|
|
2604
|
-
case "ternary":
|
|
2605
|
-
return droppableStatic(e.cond) && droppableStatic(e.then) && droppableStatic(e.else_);
|
|
2606
|
-
case "strConcat":
|
|
2607
|
-
return droppableStatic(e.left) && droppableStatic(e.right);
|
|
2608
|
-
case "toBool":
|
|
2609
|
-
return droppableStatic(e.operand);
|
|
2610
|
-
case "unionIsTag":
|
|
2611
|
-
return droppableStatic(e.value);
|
|
2612
|
-
case "unionWrap":
|
|
2613
|
-
return droppableStatic(e.value);
|
|
2614
|
-
// Fresh allocations are unobservable too — only their PIECES can
|
|
2615
|
-
// carry effects (a spread re-reads its source: pure; element and
|
|
2616
|
-
// field initializers recurse).
|
|
2617
|
-
case "arrayLit":
|
|
2618
|
-
return e.elems.every(droppableStatic);
|
|
2619
|
-
case "recordLit":
|
|
2620
|
-
return e.fields.every((f) => droppableStatic(f.value));
|
|
2621
|
-
case "recordClone":
|
|
2622
|
-
return droppableStatic(e.source) && e.overrides.every((f) => droppableStatic(f.value));
|
|
2623
|
-
case "closure":
|
|
2624
|
-
return true;
|
|
2625
|
-
default:
|
|
2626
|
-
return pureReemittable(e);
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
export function pureReemittable(e) {
|
|
2630
|
-
if (e.kind === "varRef")
|
|
2631
|
-
return true;
|
|
2632
|
-
if (e.kind === "recordGet" || e.kind === "fieldGet")
|
|
2633
|
-
return pureReemittable(e.obj);
|
|
2634
|
-
// The trust-the-checker narrowing bridges over a pure read: extraction
|
|
2635
|
-
// and downcast are reads too (the +1 on ref payloads is RC bookkeeping,
|
|
2636
|
-
// not an observable effect — each emission owns its own copy).
|
|
2637
|
-
if (e.kind === "unionNarrow" || e.kind === "downcast")
|
|
2638
|
-
return pureReemittable(e.value);
|
|
2639
|
-
return false;
|
|
2640
|
-
}
|
|
2641
|
-
/** A side-effect-free CONDITION: the ToBoolean/test wrappers lowerCondition
|
|
2642
|
-
* builds over pure reads (evaluating one early or twice changes nothing
|
|
2643
|
-
* observable). Used by the island-literal conditional spread, whose
|
|
2644
|
-
* desugar hoists the condition ahead of the properties. */
|
|
2645
|
-
function pureCondExpr(e) {
|
|
2646
|
-
if (e.kind === "boolLit")
|
|
2647
|
-
return true;
|
|
2648
|
-
if (e.kind === "toBool")
|
|
2649
|
-
return pureReemittable(e.operand);
|
|
2650
|
-
if (e.kind === "unionIsTag" || e.kind === "dynTest")
|
|
2651
|
-
return pureReemittable(e.value);
|
|
2652
|
-
if (e.kind === "unary" && e.op === "!")
|
|
2653
|
-
return pureCondExpr(e.operand);
|
|
2654
|
-
if (e.kind === "logical")
|
|
2655
|
-
return pureCondExpr(e.left) && pureCondExpr(e.right);
|
|
2656
|
-
if (e.kind === "jsOp" && (e.op === "truthy" || e.op === "not"))
|
|
2657
|
-
return e.args.every(pureReemittable);
|
|
2658
|
-
return pureReemittable(e);
|
|
2659
|
-
}
|
|
2660
2602
|
/** `a ?? b` — JS-exact nullish coalescing: ONLY null/undefined take the
|
|
2661
2603
|
* default (0, "", and false do not), and the right side evaluates lazily.
|
|
2662
2604
|
* On a unit-armed union left this is the `nullish` node (a runtime tag
|
|
@@ -2705,6 +2647,42 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
|
|
|
2705
2647
|
const right = lowerer.lowerExprExpecting(expr.right, type);
|
|
2706
2648
|
return { kind: "nullish", left, right, type, loc };
|
|
2707
2649
|
}
|
|
2650
|
+
if (type.kind !== "union" && rest.length > 1 && rest.some((arm) => typeEquals(arm, type))) {
|
|
2651
|
+
const helper = lowerer.narrowedArmHelper(left.type.unionId, type, loc);
|
|
2652
|
+
if (helper !== null) {
|
|
2653
|
+
const stmts = [];
|
|
2654
|
+
let stable = left;
|
|
2655
|
+
if (!isSafeToRepeat(left)) {
|
|
2656
|
+
const local = lowerer.declareHiddenLocal("%nullish", left.type);
|
|
2657
|
+
stmts.push({ kind: "varDecl", localId: local.id, init: left, loc });
|
|
2658
|
+
stable = varRef(local.id, left.type, loc);
|
|
2659
|
+
}
|
|
2660
|
+
const unitTags = def.arms
|
|
2661
|
+
.map((arm, tag) => ({ arm, tag }))
|
|
2662
|
+
.filter(({ arm }) => isUnitType(arm))
|
|
2663
|
+
.map(({ tag }) => tag);
|
|
2664
|
+
let isNullish = { kind: "unionIsTag", unionId: left.type.unionId, tag: unitTags[0], negated: false, value: stable, type: BOOL, loc };
|
|
2665
|
+
for (const tag of unitTags.slice(1)) {
|
|
2666
|
+
isNullish = {
|
|
2667
|
+
kind: "logical",
|
|
2668
|
+
op: "||",
|
|
2669
|
+
left: isNullish,
|
|
2670
|
+
right: { kind: "unionIsTag", unionId: left.type.unionId, tag, negated: false, value: stable, type: BOOL, loc },
|
|
2671
|
+
type: BOOL,
|
|
2672
|
+
loc,
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
const result = {
|
|
2676
|
+
kind: "ternary",
|
|
2677
|
+
cond: isNullish,
|
|
2678
|
+
then: lowerer.lowerExprExpecting(expr.right, type),
|
|
2679
|
+
else_: { kind: "call", callee: helper, args: [stable], type, loc },
|
|
2680
|
+
type,
|
|
2681
|
+
loc,
|
|
2682
|
+
};
|
|
2683
|
+
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type, loc };
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2708
2686
|
// The RETAGGED shape — the default changes the result union (`s ??
|
|
2709
2687
|
// null` over `string | undefined` answers `null | string`): every
|
|
2710
2688
|
// non-unit arm of the left has a home in the result, so an interned
|
|
@@ -2715,9 +2693,72 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
|
|
|
2715
2693
|
if (type.kind === "union") {
|
|
2716
2694
|
const leftT = left.type;
|
|
2717
2695
|
const armPairs = rest.map((a) => ({ arm: a, src: lowerer.armTag(leftT.unionId, a), dst: lowerer.armTag(type.unionId, a) }));
|
|
2696
|
+
if (rest.length > 0 && armPairs.every((pair) => pair.src >= 0 && pair.dst >= 0)) {
|
|
2697
|
+
const stmts = [];
|
|
2698
|
+
let stable = left;
|
|
2699
|
+
if (!isSafeToRepeat(left)) {
|
|
2700
|
+
const local = lowerer.declareHiddenLocal("%nullish", leftT);
|
|
2701
|
+
stmts.push({ kind: "varDecl", localId: local.id, init: left, loc });
|
|
2702
|
+
stable = varRef(local.id, leftT, loc);
|
|
2703
|
+
}
|
|
2704
|
+
const unitTags = def.arms
|
|
2705
|
+
.map((arm, tag) => ({ arm, tag }))
|
|
2706
|
+
.filter(({ arm }) => isUnitType(arm))
|
|
2707
|
+
.map(({ tag }) => tag);
|
|
2708
|
+
let isNullish = {
|
|
2709
|
+
kind: "unionIsTag",
|
|
2710
|
+
unionId: leftT.unionId,
|
|
2711
|
+
tag: unitTags[0],
|
|
2712
|
+
negated: false,
|
|
2713
|
+
value: stable,
|
|
2714
|
+
type: BOOL,
|
|
2715
|
+
loc,
|
|
2716
|
+
};
|
|
2717
|
+
for (const tag of unitTags.slice(1)) {
|
|
2718
|
+
isNullish = {
|
|
2719
|
+
kind: "logical",
|
|
2720
|
+
op: "||",
|
|
2721
|
+
left: isNullish,
|
|
2722
|
+
right: { kind: "unionIsTag", unionId: leftT.unionId, tag, negated: false, value: stable, type: BOOL, loc },
|
|
2723
|
+
type: BOOL,
|
|
2724
|
+
loc,
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
let present = null;
|
|
2728
|
+
for (let i = armPairs.length - 1; i >= 0; i--) {
|
|
2729
|
+
const pair = armPairs[i];
|
|
2730
|
+
const wrapped = {
|
|
2731
|
+
kind: "unionWrap",
|
|
2732
|
+
unionId: type.unionId,
|
|
2733
|
+
tag: pair.dst,
|
|
2734
|
+
value: { kind: "unionNarrow", unionId: leftT.unionId, tag: pair.src, value: stable, type: pair.arm, loc },
|
|
2735
|
+
type,
|
|
2736
|
+
loc,
|
|
2737
|
+
};
|
|
2738
|
+
present = present === null
|
|
2739
|
+
? wrapped
|
|
2740
|
+
: {
|
|
2741
|
+
kind: "ternary",
|
|
2742
|
+
cond: { kind: "unionIsTag", unionId: leftT.unionId, tag: pair.src, negated: false, value: stable, type: BOOL, loc },
|
|
2743
|
+
then: wrapped,
|
|
2744
|
+
else_: present,
|
|
2745
|
+
type,
|
|
2746
|
+
loc,
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
const result = {
|
|
2750
|
+
kind: "ternary",
|
|
2751
|
+
cond: isNullish,
|
|
2752
|
+
then: lowerer.lowerExprExpecting(expr.right, type),
|
|
2753
|
+
else_: present,
|
|
2754
|
+
type,
|
|
2755
|
+
loc,
|
|
2756
|
+
};
|
|
2757
|
+
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type, loc };
|
|
2758
|
+
}
|
|
2718
2759
|
if (armPairs.every((p) => p.src >= 0 && p.dst >= 0)) {
|
|
2719
2760
|
const right = lowerer.lowerExprExpecting(expr.right, type);
|
|
2720
|
-
if (
|
|
2761
|
+
if (isSafeToDiscard(right)) {
|
|
2721
2762
|
const helper = nullishRetagHelper(lowerer, leftT, type, loc);
|
|
2722
2763
|
return { kind: "call", callee: helper, args: [left, right], type, loc };
|
|
2723
2764
|
}
|
|
@@ -2784,384 +2825,6 @@ function nullishRetagHelper(lowerer, leftT, resT, loc) {
|
|
|
2784
2825
|
});
|
|
2785
2826
|
return name;
|
|
2786
2827
|
}
|
|
2787
|
-
/** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
|
|
2788
|
-
* the member access) and `f?.()` (the token on the call). The receiver
|
|
2789
|
-
* lowers once; when it is a unit-armed union with ONE non-unit arm, the
|
|
2790
|
-
* member/call lowers exactly as its non-optional spelling would — the
|
|
2791
|
-
* receiver node reads back as the chain's bound narrowed value
|
|
2792
|
-
* (chainRecv) and types as its non-nullish type — and the whole thing
|
|
2793
|
-
* becomes an optChain node: tag test, undefined on the unit path (JS:
|
|
2794
|
-
* null receivers still yield undefined), the body lazily otherwise,
|
|
2795
|
-
* argument side effects included. A receiver the checker types
|
|
2796
|
-
* never-nullish makes `?.` behave exactly like `.` — the guard folds
|
|
2797
|
-
* away and the plain lowering is the value (trust-the-checker, like
|
|
2798
|
-
* ??'s fold). Multi-step TAILS (`a?.b.c`, `x?.trim().toLowerCase()`)
|
|
2799
|
-
* short-circuit whole: the guarded member step is the chain's dot and
|
|
2800
|
-
* every later step lowers inside the guard, checker-narrowed non-nullish
|
|
2801
|
-
* (see chainTailDot); sub-union receivers are fenced with rewrite
|
|
2802
|
-
* hints. */
|
|
2803
|
-
/** The unhandled `?.`-carrying MEMBER step in this node's receiver
|
|
2804
|
-
* spine, when the node is the TAIL of an optional chain whose token sits
|
|
2805
|
-
* deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x
|
|
2806
|
-
* is nullish, so the whole tail must lower inside it. Walks
|
|
2807
|
-
* property/element accesses and call steps only (parens and `!` break
|
|
2808
|
-
* the chain per the grammar: `(a?.b).c` throws on undefined in JS);
|
|
2809
|
-
* the node's OWN token — and a call's immediate callee token — are the
|
|
2810
|
-
* plain entries' cases, not tails, and handled markers make the chain
|
|
2811
|
-
* lowering's re-dispatch walk past its own step. `f?.()` steps stay out
|
|
2812
|
-
* (a call carrying the token has no member step to re-enter — the
|
|
2813
|
-
* split-it fence keeps them). */
|
|
2814
|
-
function chainTailDot(lowerer, expr) {
|
|
2815
|
-
let cur = expr;
|
|
2816
|
-
for (;;) {
|
|
2817
|
-
// An active chain's bound receiver read is a LEAF — its own token
|
|
2818
|
-
// was consumed by the chain that bound it.
|
|
2819
|
-
if (lowerer.chainRecvByNode.has(cur))
|
|
2820
|
-
return null;
|
|
2821
|
-
if (ts.isCallExpression(cur)) {
|
|
2822
|
-
// `f?.()` steps route through their own entry (or are being
|
|
2823
|
-
// handled); a deeper token under one is that chain's business.
|
|
2824
|
-
if (cur.questionDotToken)
|
|
2825
|
-
return null;
|
|
2826
|
-
cur = cur.expression;
|
|
2827
|
-
continue;
|
|
2828
|
-
}
|
|
2829
|
-
if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
|
|
2830
|
-
if (cur.questionDotToken) {
|
|
2831
|
-
// The node's own token and a call's immediate callee token are
|
|
2832
|
-
// the plain entries' cases; a handled one is mid-re-dispatch
|
|
2833
|
-
// (its receiver read, further down, ends the walk). The walk
|
|
2834
|
-
// NEVER continues past an unhandled deeper token: that guard is
|
|
2835
|
-
// where this tail's chain enters — anything below it belongs to
|
|
2836
|
-
// the receiver's own (nested) chain.
|
|
2837
|
-
if (cur === expr || lowerer.chainHandled.has(cur)) {
|
|
2838
|
-
cur = cur.expression;
|
|
2839
|
-
continue;
|
|
2840
|
-
}
|
|
2841
|
-
if (ts.isCallExpression(expr) && expr.expression === cur)
|
|
2842
|
-
return null;
|
|
2843
|
-
return cur;
|
|
2844
|
-
}
|
|
2845
|
-
cur = cur.expression;
|
|
2846
|
-
continue;
|
|
2847
|
-
}
|
|
2848
|
-
return null;
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
/** True when `expr` is `require.main.filename` (either dot optional) on
|
|
2852
|
-
* the AMBIENT CommonJS require — the entry-module identity read. The
|
|
2853
|
-
* value is the ENTRY file's path, a compile-time constant like
|
|
2854
|
-
* __filename; ESM files stay out (Node never defines require there —
|
|
2855
|
-
* the ambient symbol would not resolve anyway). Shared by the property
|
|
2856
|
-
* lowering (the fold) and lowerStringMethodCall's receiver gate (the
|
|
2857
|
-
* checker types the chain `string | undefined`, but the folded receiver
|
|
2858
|
-
* IS a string). */
|
|
2859
|
-
export function isRequireMainFilename(lowerer, expr) {
|
|
2860
|
-
if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "filename")
|
|
2861
|
-
return false;
|
|
2862
|
-
const main = expr.expression;
|
|
2863
|
-
if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main")
|
|
2864
|
-
return false;
|
|
2865
|
-
if (!lowerer.isStdlibGlobal(main.expression, "require"))
|
|
2866
|
-
return false;
|
|
2867
|
-
return !isNodeEsmFile(expr.getSourceFile());
|
|
2868
|
-
}
|
|
2869
|
-
/** True when `expr` is the tail of an optional chain the STATIC chain
|
|
2870
|
-
* machinery should short-circuit whole: an unhandled deeper `?.` whose
|
|
2871
|
-
* guarded receiver lowers as a unit-armed union. Dyn ('unknown') and
|
|
2872
|
-
* island ('any') receivers answer their tails through their own
|
|
2873
|
-
* undefined-propagating reads and stay out; never-nullish receivers fold
|
|
2874
|
-
* at the token's own entry. */
|
|
2875
|
-
function chainTailClaimed(lowerer, expr) {
|
|
2876
|
-
const tail = chainTailDot(lowerer, expr);
|
|
2877
|
-
if (!tail)
|
|
2878
|
-
return false;
|
|
2879
|
-
const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
|
|
2880
|
-
if (recvT?.kind !== "union")
|
|
2881
|
-
return false;
|
|
2882
|
-
const def = lowerer.unions.get(recvT.unionId);
|
|
2883
|
-
return !!def && def.arms.some(isUnitType);
|
|
2884
|
-
}
|
|
2885
|
-
/** True when an EARLIER step of this access chain carries `?.` — JS
|
|
2886
|
-
* short-circuits the whole tail (`a?.b.c` reads nothing when a is
|
|
2887
|
-
* nullish), so a dyn tail read must answer undefined instead of
|
|
2888
|
-
* throwing. Walks the receiver spine only; the argument of the access
|
|
2889
|
-
* itself is not part of the guard. */
|
|
2890
|
-
function chainGuardedByQuestionDot(expr) {
|
|
2891
|
-
let cur = expr;
|
|
2892
|
-
for (;;) {
|
|
2893
|
-
if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) {
|
|
2894
|
-
cur = cur.expression;
|
|
2895
|
-
continue;
|
|
2896
|
-
}
|
|
2897
|
-
if (ts.isPropertyAccessExpression(cur) ||
|
|
2898
|
-
ts.isElementAccessExpression(cur) ||
|
|
2899
|
-
ts.isCallExpression(cur)) {
|
|
2900
|
-
if (cur.questionDotToken)
|
|
2901
|
-
return true;
|
|
2902
|
-
cur = cur.expression;
|
|
2903
|
-
continue;
|
|
2904
|
-
}
|
|
2905
|
-
return false;
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
export function lowerOptionalChain(lowerer, expr) {
|
|
2909
|
-
const loc = locOf(expr);
|
|
2910
|
-
// The node CARRYING the ?. token and the receiver expression it guards.
|
|
2911
|
-
let dotNode;
|
|
2912
|
-
let recvNode;
|
|
2913
|
-
if (ts.isCallExpression(expr) && expr.questionDotToken) {
|
|
2914
|
-
dotNode = expr; // f?.()
|
|
2915
|
-
recvNode = expr.expression;
|
|
2916
|
-
}
|
|
2917
|
-
else if (ts.isCallExpression(expr) &&
|
|
2918
|
-
ts.isPropertyAccessExpression(expr.expression) &&
|
|
2919
|
-
expr.expression.questionDotToken) {
|
|
2920
|
-
dotNode = expr.expression; // a?.m()
|
|
2921
|
-
recvNode = expr.expression.expression;
|
|
2922
|
-
}
|
|
2923
|
-
else if ((ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) &&
|
|
2924
|
-
expr.questionDotToken) {
|
|
2925
|
-
dotNode = expr; // a?.b / a?.[i]
|
|
2926
|
-
recvNode = expr.expression;
|
|
2927
|
-
}
|
|
2928
|
-
else {
|
|
2929
|
-
// TAIL entry: the token sits deeper in the receiver spine and JS
|
|
2930
|
-
// short-circuits the WHOLE tail with it (`x?.trim().toLowerCase()`
|
|
2931
|
-
// reads nothing when x is nullish). The guarded member step becomes
|
|
2932
|
-
// the chain's dot; every step above it lowers inside the guard, its
|
|
2933
|
-
// checker type narrowed non-nullish below (the tag test proved it).
|
|
2934
|
-
const tail = chainTailDot(lowerer, expr);
|
|
2935
|
-
if (!tail) {
|
|
2936
|
-
// A `f?.()` step under a member tail, or no token at all (a
|
|
2937
|
-
// dispatch bug): short-circuiting those is not modeled.
|
|
2938
|
-
lowerer.unsupported("SC1090", expr, "multi-step optional chains (split them: const v = a?.b; then use v?.c)");
|
|
2939
|
-
}
|
|
2940
|
-
dotNode = tail;
|
|
2941
|
-
recvNode = tail.expression;
|
|
2942
|
-
}
|
|
2943
|
-
const loweredReceiver = lowerer.lowerExpr(recvNode);
|
|
2944
|
-
const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
|
|
2945
|
-
if (receiver.type.kind === "dyn") {
|
|
2946
|
-
// `pkg?.name` / `pkg?.scripts?.[k]` on a JSON.parse result: dyn
|
|
2947
|
-
// represents undefined directly, so the chain step IS the keyed
|
|
2948
|
-
// read with the optional (unit-answers-undefined) policy — no
|
|
2949
|
-
// optChain wrapper needed; nested steps compose the same way.
|
|
2950
|
-
if (dotNode === expr && ts.isPropertyAccessExpression(expr)) {
|
|
2951
|
-
const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
|
|
2952
|
-
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
|
|
2953
|
-
}
|
|
2954
|
-
if (dotNode === expr && ts.isElementAccessExpression(expr)) {
|
|
2955
|
-
const key = lowerer.lowerExpr(expr.argumentExpression);
|
|
2956
|
-
if (key.type.kind === "string") {
|
|
2957
|
-
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
|
|
2958
|
-
}
|
|
2959
|
-
// NUMBER-typed indices (`entries?.[0]`): the property key is
|
|
2960
|
-
// ToString(i), exactly JS — the canonical number text answers
|
|
2961
|
-
// array indices in the dyn helper, anything else (fractions,
|
|
2962
|
-
// negatives, NaN) reads as an absent key.
|
|
2963
|
-
if (key.type.kind === "f64") {
|
|
2964
|
-
const skey = { kind: "toString", operand: key, type: STRING, loc: key.loc };
|
|
2965
|
-
return lowerer.maybeNarrow({ kind: "dynKeyGet", key: skey, optional: true, value: receiver, type: DYN, loc }, expr);
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
// The METHOD-call step (`rawName?.match(re)` on a dyn value): a
|
|
2969
|
-
// nullish receiver short-circuits to the undefined dyn singleton
|
|
2970
|
-
// (dyn represents undefined directly); anything else runs the
|
|
2971
|
-
// VALIDATED dynamic dispatch — the same one the truthy-guarded
|
|
2972
|
-
// spelling (`v ? v.match(...) : null`) compiles to, Node-shaped
|
|
2973
|
-
// TypeError on a kind mismatch included. The dispatch's static
|
|
2974
|
-
// result converts back into the checked-dynamic tree (the chain's checker type is
|
|
2975
|
-
// the error-any world's `any`, so dyn IS its representation).
|
|
2976
|
-
if (ts.isCallExpression(expr) &&
|
|
2977
|
-
ts.isPropertyAccessExpression(dotNode) &&
|
|
2978
|
-
dotNode === expr.expression) {
|
|
2979
|
-
const m = dotNode.name.text;
|
|
2980
|
-
const id = `chain.${lowerer.chainCounter++}`;
|
|
2981
|
-
const recvRef = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) };
|
|
2982
|
-
lowerer.chainRecvByNode.set(recvNode, recvRef);
|
|
2983
|
-
lowerer.chainHandled.add(dotNode);
|
|
2984
|
-
let body;
|
|
2985
|
-
try {
|
|
2986
|
-
body = lowerer.lowerExpr(expr);
|
|
2987
|
-
}
|
|
2988
|
-
finally {
|
|
2989
|
-
lowerer.chainRecvByNode.delete(recvNode);
|
|
2990
|
-
lowerer.chainHandled.delete(dotNode);
|
|
2991
|
-
}
|
|
2992
|
-
if (body.type.kind !== "dyn") {
|
|
2993
|
-
if (body.type.kind !== "void" && !lowerer.dynConvertible(body.type)) {
|
|
2994
|
-
lowerer.unsupported("SC1100", expr, `optional METHOD calls on 'unknown' values where the result ('${lowerer.fmt(body.type)}' ` +
|
|
2995
|
-
`from '.${m}(...)') has no dynamic representation`);
|
|
2996
|
-
}
|
|
2997
|
-
if (body.type.kind === "void") {
|
|
2998
|
-
return { kind: "optChain", id, receiver, body, type: VOID, loc };
|
|
2999
|
-
}
|
|
3000
|
-
body = { kind: "dynFrom", value: body, type: DYN, loc };
|
|
3001
|
-
}
|
|
3002
|
-
return { kind: "optChain", id, receiver, body, type: DYN, loc };
|
|
3003
|
-
}
|
|
3004
|
-
lowerer.unsupported("SC1100", expr, "optional chaining on 'unknown' values");
|
|
3005
|
-
}
|
|
3006
|
-
// An 'any' (island-handle) receiver: the nullish test asks the ENGINE
|
|
3007
|
-
// value at runtime — null/undefined short-circuit to the engine's
|
|
3008
|
-
// undefined (JS: null receivers yield undefined too), anything else
|
|
3009
|
-
// proceeds as the plain island operation with the receiver evaluated
|
|
3010
|
-
// once (argument side effects stay lazy, like every optChain). The
|
|
3011
|
-
// result is an island value again ('any' in, 'any' out).
|
|
3012
|
-
if (receiver.type.kind === "jsval") {
|
|
3013
|
-
const id = `chain.${lowerer.chainCounter++}`;
|
|
3014
|
-
const recvRef = { kind: "chainRecv", id, type: JSVAL, loc: locOf(recvNode) };
|
|
3015
|
-
if (dotNode === expr && ts.isCallExpression(expr)) {
|
|
3016
|
-
// `a.b?.(...)` — a MEMBER callee: JS calls it with `this` bound to
|
|
3017
|
-
// `a` and short-circuits a nullish member — exactly the engine's
|
|
3018
|
-
// own `o.name?.()` (optCallMethod: the RECEIVER is `a`, evaluated
|
|
3019
|
-
// once through the chain; a nullish `a` itself short-circuits
|
|
3020
|
-
// earlier like any optional chain). Computed members keep a fence.
|
|
3021
|
-
if (ts.isPropertyAccessExpression(recvNode) || ts.isElementAccessExpression(recvNode)) {
|
|
3022
|
-
if (ts.isPropertyAccessExpression(recvNode) &&
|
|
3023
|
-
!recvNode.questionDotToken) {
|
|
3024
|
-
const obj = lowerer.lowerExpr(recvNode.expression);
|
|
3025
|
-
if (obj.type.kind !== "jsval")
|
|
3026
|
-
lowerer.badType(recvNode.expression, lowerer.typeOf(recvNode.expression));
|
|
3027
|
-
const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
|
|
3028
|
-
return { kind: "jsOp", op: "optCallMethod", name: recvNode.name.text, args: [obj, ...args], type: JSVAL, loc };
|
|
3029
|
-
}
|
|
3030
|
-
lowerer.unsupported("SC1090", expr, "optional calls of computed 'any' member values (a[k]?.() — bind the member to a const first)");
|
|
3031
|
-
}
|
|
3032
|
-
const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
|
|
3033
|
-
const body = { kind: "jsOp", op: "callFn", args: [recvRef, ...args], type: JSVAL, loc };
|
|
3034
|
-
return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
|
|
3035
|
-
}
|
|
3036
|
-
// Member forms (`x?.y`, `x?.y(...)`, `x?.[i]`): re-dispatch the plain
|
|
3037
|
-
// island lowering with the receiver node bound to the chain.
|
|
3038
|
-
lowerer.chainRecvByNode.set(recvNode, recvRef);
|
|
3039
|
-
lowerer.chainHandled.add(dotNode);
|
|
3040
|
-
let body;
|
|
3041
|
-
try {
|
|
3042
|
-
body = lowerer.lowerExpr(expr);
|
|
3043
|
-
}
|
|
3044
|
-
finally {
|
|
3045
|
-
lowerer.chainRecvByNode.delete(recvNode);
|
|
3046
|
-
lowerer.chainHandled.delete(dotNode);
|
|
3047
|
-
}
|
|
3048
|
-
if (body.type.kind !== "jsval")
|
|
3049
|
-
lowerer.badType(expr, lowerer.typeOf(expr));
|
|
3050
|
-
return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
|
|
3051
|
-
}
|
|
3052
|
-
const def = receiver.type.kind === "union" ? lowerer.unions.get(receiver.type.unionId) : undefined;
|
|
3053
|
-
if (!def || !def.arms.some(isUnitType)) {
|
|
3054
|
-
// Never nullish: `?.` IS `.` — re-dispatch the plain lowering. The
|
|
3055
|
-
// receiver subtree above is discarded (lowering is pure IR
|
|
3056
|
-
// construction); the fresh dispatch lowers it again in place. The
|
|
3057
|
-
// checker may still SPELL the receiver nullable while the lowering
|
|
3058
|
-
// answers the plain value (`process.getuid?.()` is the POSIX
|
|
3059
|
-
// number), so narrow the node too — downstream receiver-typed
|
|
3060
|
-
// dispatch must agree with the lowered value, not the spelling.
|
|
3061
|
-
lowerer.chainHandled.add(dotNode);
|
|
3062
|
-
const hadNarrow = lowerer.chainNarrowedType.has(recvNode);
|
|
3063
|
-
if (!hadNarrow) {
|
|
3064
|
-
lowerer.chainNarrowedType.set(recvNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode)));
|
|
3065
|
-
}
|
|
3066
|
-
try {
|
|
3067
|
-
return lowerer.lowerExpr(expr);
|
|
3068
|
-
}
|
|
3069
|
-
finally {
|
|
3070
|
-
lowerer.chainHandled.delete(dotNode);
|
|
3071
|
-
if (!hadNarrow)
|
|
3072
|
-
lowerer.chainNarrowedType.delete(recvNode);
|
|
3073
|
-
}
|
|
3074
|
-
}
|
|
3075
|
-
const rest = def.arms.filter((a) => !isUnitType(a));
|
|
3076
|
-
if (rest.length !== 1) {
|
|
3077
|
-
lowerer.unsupported("SC1090", expr, `'?.' on '${lowerer.fmt(receiver.type)}' (the guarded receiver is a sub-union; check a discriminant field first)`);
|
|
3078
|
-
}
|
|
3079
|
-
const narrowed = rest[0];
|
|
3080
|
-
const id = `chain.${lowerer.chainCounter++}`;
|
|
3081
|
-
const recvRef = { kind: "chainRecv", id, type: narrowed, loc: locOf(recvNode) };
|
|
3082
|
-
// `f?.()`: the callee IS the guarded value — build the indirect call
|
|
3083
|
-
// directly (no member dispatch exists to re-enter).
|
|
3084
|
-
if (dotNode === expr && ts.isCallExpression(expr)) {
|
|
3085
|
-
if (narrowed.kind !== "func")
|
|
3086
|
-
lowerer.badType(recvNode, lowerer.typeOf(recvNode));
|
|
3087
|
-
const params = narrowed.params;
|
|
3088
|
-
const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
|
|
3089
|
-
const body = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
|
|
3090
|
-
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
|
|
3091
|
-
}
|
|
3092
|
-
// Member forms: re-dispatch the normal lowering with the receiver node
|
|
3093
|
-
// bound to the chain (reads as chainRecv, types as non-nullish).
|
|
3094
|
-
const narrowedTs = lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode));
|
|
3095
|
-
lowerer.chainRecvByNode.set(recvNode, recvRef);
|
|
3096
|
-
lowerer.chainNarrowedType.set(recvNode, narrowedTs);
|
|
3097
|
-
lowerer.chainHandled.add(dotNode);
|
|
3098
|
-
// A TAIL entry's intermediate steps (`x?.trim()` inside
|
|
3099
|
-
// `x?.trim().toLowerCase()`) checker-type with the chain's `|
|
|
3100
|
-
// undefined` even though inside the guard they are proven non-nullish
|
|
3101
|
-
// — narrow each so the downstream method/member dispatch rides the
|
|
3102
|
-
// real receiver kind. Only nodes this chain registers are cleaned up.
|
|
3103
|
-
const tailSteps = [];
|
|
3104
|
-
if (dotNode !== expr && !(ts.isCallExpression(expr) && expr.expression === dotNode)) {
|
|
3105
|
-
for (let cur = expr; cur !== dotNode;) {
|
|
3106
|
-
const next = ts.isCallExpression(cur)
|
|
3107
|
-
? cur.expression
|
|
3108
|
-
: cur.expression;
|
|
3109
|
-
if (next === dotNode)
|
|
3110
|
-
break;
|
|
3111
|
-
if (!lowerer.chainNarrowedType.has(next)) {
|
|
3112
|
-
lowerer.chainNarrowedType.set(next, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(next)));
|
|
3113
|
-
tailSteps.push(next);
|
|
3114
|
-
}
|
|
3115
|
-
cur = next;
|
|
3116
|
-
}
|
|
3117
|
-
if (!lowerer.chainNarrowedType.has(dotNode)) {
|
|
3118
|
-
lowerer.chainNarrowedType.set(dotNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(dotNode)));
|
|
3119
|
-
tailSteps.push(dotNode);
|
|
3120
|
-
}
|
|
3121
|
-
}
|
|
3122
|
-
let body;
|
|
3123
|
-
try {
|
|
3124
|
-
body = lowerer.lowerExpr(expr);
|
|
3125
|
-
}
|
|
3126
|
-
finally {
|
|
3127
|
-
lowerer.chainRecvByNode.delete(recvNode);
|
|
3128
|
-
lowerer.chainNarrowedType.delete(recvNode);
|
|
3129
|
-
lowerer.chainHandled.delete(dotNode);
|
|
3130
|
-
for (const n of tailSteps)
|
|
3131
|
-
lowerer.chainNarrowedType.delete(n);
|
|
3132
|
-
}
|
|
3133
|
-
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
|
|
3134
|
-
}
|
|
3135
|
-
/** The optChain node around a lowered chain body: void bodies keep the
|
|
3136
|
-
* statement form (`cb?.();` — the checker's `void | undefined` result IS
|
|
3137
|
-
* void here); value bodies wrap into the checker's undefined-armed
|
|
3138
|
-
* result union. */
|
|
3139
|
-
export function finishOptionalChain(lowerer, expr, id, receiver, body, loc) {
|
|
3140
|
-
if (body.type.kind === "void") {
|
|
3141
|
-
return { kind: "optChain", id, receiver, body, type: VOID, loc };
|
|
3142
|
-
}
|
|
3143
|
-
// A dyn body (`pricing?.[key]` — an unknown-valued index-signature
|
|
3144
|
-
// read) stays dyn: `unknown | undefined` IS unknown, and dyn represents
|
|
3145
|
-
// undefined directly (the unit path yields the undefined dyn value).
|
|
3146
|
-
if (body.type.kind === "dyn") {
|
|
3147
|
-
return { kind: "optChain", id, receiver, body, type: DYN, loc };
|
|
3148
|
-
}
|
|
3149
|
-
let type = lowerer.irTypeOf(expr);
|
|
3150
|
-
const hiddenOptionalResult = (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) &&
|
|
3151
|
-
receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 &&
|
|
3152
|
-
body.type.kind !== "generator" && body.type.kind !== "jsval"
|
|
3153
|
-
? lowerer.withUndefinedArmOf(body.type)
|
|
3154
|
-
: null;
|
|
3155
|
-
if (hiddenOptionalResult)
|
|
3156
|
-
type = hiddenOptionalResult;
|
|
3157
|
-
if (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) {
|
|
3158
|
-
lowerer.unsupported("SC1090", expr, `'?.' where the result '${lowerer.fmt(type)}' has no undefined arm (narrow the receiver with 'if (x !== undefined)' instead)`);
|
|
3159
|
-
}
|
|
3160
|
-
const wrapped = lowerer.coerceToExpected(body, type);
|
|
3161
|
-
if (!typeEquals(wrapped.type, type))
|
|
3162
|
-
lowerer.badType(expr, lowerer.typeOf(expr));
|
|
3163
|
-
return { kind: "optChain", id, receiver, body: wrapped, type, loc };
|
|
3164
|
-
}
|
|
3165
2828
|
/** A CONDITION-position expression: the result is consumed as a bool
|
|
3166
2829
|
* only, so `&&`/`||` descend recursively over ToBoolean'd operands —
|
|
3167
2830
|
* JS-exact (`ToBoolean(a && b)` ≡ `ToBoolean(a) && ToBoolean(b)`), still
|
|
@@ -3171,6 +2834,11 @@ export function lowerCondition(lowerer, expr) {
|
|
|
3171
2834
|
let e = expr;
|
|
3172
2835
|
while (ts.isParenthesizedExpression(e))
|
|
3173
2836
|
e = e.expression;
|
|
2837
|
+
if (ts.isPropertyAccessExpression(e)) {
|
|
2838
|
+
const presence = lowerPromiseThenPresence(lowerer, e, { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) });
|
|
2839
|
+
if (presence !== null)
|
|
2840
|
+
return presence;
|
|
2841
|
+
}
|
|
3174
2842
|
if (ts.isBinaryExpression(e)) {
|
|
3175
2843
|
const op = e.operatorToken.kind;
|
|
3176
2844
|
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
|
|
@@ -3204,6 +2872,47 @@ export function lowerCondition(lowerer, expr) {
|
|
|
3204
2872
|
}
|
|
3205
2873
|
return lowerer.ensureBool(lowerAbsenceProbe(lowerer, expr) ?? lowerer.lowerExpr(expr), expr);
|
|
3206
2874
|
}
|
|
2875
|
+
function lowerPromiseThenPresence(lowerer, access, result) {
|
|
2876
|
+
if (access.name.text !== "then")
|
|
2877
|
+
return null;
|
|
2878
|
+
const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
|
2879
|
+
const promiseType = mapped?.kind === "promise"
|
|
2880
|
+
? mapped
|
|
2881
|
+
: mapped?.kind === "union"
|
|
2882
|
+
? (() => {
|
|
2883
|
+
const arms = lowerer.unions.get(mapped.unionId)?.arms ?? [];
|
|
2884
|
+
const promises = arms.filter((arm) => arm.kind === "promise");
|
|
2885
|
+
return promises.length === 1 && arms.every((arm) => arm.kind === "promise" || isUnitType(arm))
|
|
2886
|
+
? promises[0]
|
|
2887
|
+
: null;
|
|
2888
|
+
})()
|
|
2889
|
+
: null;
|
|
2890
|
+
if (promiseType === null)
|
|
2891
|
+
return null;
|
|
2892
|
+
let receiver = lowerer.lowerExpr(access.expression);
|
|
2893
|
+
if (receiver.type.kind === "union") {
|
|
2894
|
+
const optional = lowerer.runtimeOptionalPropertyReceiver(access.expression, receiver, promiseType, access.name.text);
|
|
2895
|
+
if (optional !== null)
|
|
2896
|
+
receiver = optional;
|
|
2897
|
+
else {
|
|
2898
|
+
const helper = lowerer.narrowedArmHelper(receiver.type.unionId, promiseType, receiver.loc);
|
|
2899
|
+
if (helper === null)
|
|
2900
|
+
return null;
|
|
2901
|
+
receiver = { kind: "call", callee: helper, args: [receiver], type: promiseType, loc: receiver.loc };
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
if (receiver.type.kind !== "promise")
|
|
2905
|
+
return null;
|
|
2906
|
+
if (isSafeToDiscard(receiver))
|
|
2907
|
+
return result;
|
|
2908
|
+
return {
|
|
2909
|
+
kind: "seqExpr",
|
|
2910
|
+
stmts: [{ kind: "exprStmt", expr: receiver, loc: receiver.loc }],
|
|
2911
|
+
result,
|
|
2912
|
+
type: result.type,
|
|
2913
|
+
loc: result.loc,
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
3207
2916
|
function runtimeOptionalLocalOf(lowerer, node) {
|
|
3208
2917
|
let expr = node;
|
|
3209
2918
|
while (ts.isParenthesizedExpression(expr))
|
|
@@ -3729,6 +3438,27 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
3729
3438
|
}
|
|
3730
3439
|
}
|
|
3731
3440
|
}
|
|
3441
|
+
// An array literal inside an npm-static implicit-any instance can
|
|
3442
|
+
// inherit the checker's poisoned any[] context even after the
|
|
3443
|
+
// active binding gives every element one concrete lowering. Recover a
|
|
3444
|
+
// static array only when every spelled element lowers to the same
|
|
3445
|
+
// supported type. An explicit checked-dynamic destination still owns
|
|
3446
|
+
// the later slot coercion; heterogeneous literals, holes, and spreads
|
|
3447
|
+
// keep the checked-dynamic fallback below.
|
|
3448
|
+
if (lowerer.implicitParamTypes !== null &&
|
|
3449
|
+
npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
|
|
3450
|
+
(mapped === null || mapped.kind === "dyn" ||
|
|
3451
|
+
(mapped.kind === "array" && mapped.elem.kind === "dyn")) &&
|
|
3452
|
+
expr.elements.length > 0 &&
|
|
3453
|
+
expr.elements.every((element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element))) {
|
|
3454
|
+
const elements = expr.elements.map((element) => tryLowerExpression(lowerer, element));
|
|
3455
|
+
const first = elements[0];
|
|
3456
|
+
if (first !== undefined && first !== null &&
|
|
3457
|
+
isSupportedArrayElem(first.type) &&
|
|
3458
|
+
elements.every((element) => element !== null && typeEquals(element.type, first.type))) {
|
|
3459
|
+
mapped = arrayOf(first.type);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3732
3462
|
// An EMPTY literal under a CONST ASSERTION whose type queries panicked
|
|
3733
3463
|
// (tsgo's readonly-[] TupleType conversion — the facade answers `any`,
|
|
3734
3464
|
// which maps to nothing statically and to an island value under
|
|
@@ -3810,7 +3540,7 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
3810
3540
|
}
|
|
3811
3541
|
lowerer.badType(expr, tsType);
|
|
3812
3542
|
}
|
|
3813
|
-
|
|
3543
|
+
const type = mapped;
|
|
3814
3544
|
// Keep the array payload type fixed. Optional reads are stored through
|
|
3815
3545
|
// arrayValueStore, which records UNDEFINED in the state byte while the
|
|
3816
3546
|
// payload remains number/string/etc.; widening the array element here
|
|
@@ -3823,7 +3553,7 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
3823
3553
|
const candidate = expr.elements[i];
|
|
3824
3554
|
if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate))
|
|
3825
3555
|
continue;
|
|
3826
|
-
const probe =
|
|
3556
|
+
const probe = tryLowerExpression(lowerer, candidate);
|
|
3827
3557
|
if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) {
|
|
3828
3558
|
runtimeOptionalElements.add(i);
|
|
3829
3559
|
needsStateBuild = true;
|
|
@@ -4019,315 +3749,6 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
4019
3749
|
loc,
|
|
4020
3750
|
};
|
|
4021
3751
|
}
|
|
4022
|
-
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
|
|
4023
|
-
* contextual type when tsc has one (annotated declarations, arguments,
|
|
4024
|
-
* nested literals) and from the literal's own type otherwise — both intern
|
|
4025
|
-
* to the same shapeId unless width subtyping is in play (an `as` cast can
|
|
4026
|
-
* smuggle a wider/narrower literal past tsc's freshness check), which the
|
|
4027
|
-
* exact-shape checks below reject with SC2002. Fields lower IN SOURCE
|
|
4028
|
-
* ORDER: JS evaluates property values in source order. */
|
|
4029
|
-
/** The dropped-field names of the PromiseSettledResult honest subset
|
|
4030
|
-
* (SEMANTICS.md 46): when the literal's target type is (a union
|
|
4031
|
-
* containing) the lib's PromiseFulfilledResult / PromiseRejectedResult,
|
|
4032
|
-
* the record mapping kept only `status` — `value` and `reason` evaluate
|
|
4033
|
-
* for effect and are not stored. Null for every other target. */
|
|
4034
|
-
function settledDropNames(lowerer, tsType) {
|
|
4035
|
-
let names = null;
|
|
4036
|
-
const parts = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
|
|
4037
|
-
for (const part of parts) {
|
|
4038
|
-
const sym = part.getSymbol();
|
|
4039
|
-
if (!sym ||
|
|
4040
|
-
!lowerer.checker.declarationsOf(sym).some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))) {
|
|
4041
|
-
continue;
|
|
4042
|
-
}
|
|
4043
|
-
if (sym.name === "PromiseFulfilledResult")
|
|
4044
|
-
(names ??= new Set()).add("value");
|
|
4045
|
-
if (sym.name === "PromiseRejectedResult")
|
|
4046
|
-
(names ??= new Set()).add("reason");
|
|
4047
|
-
}
|
|
4048
|
-
return names;
|
|
4049
|
-
}
|
|
4050
|
-
/** `{ [KEY]: v }` where KEY is a compile-time-known string or number:
|
|
4051
|
-
* the key folds into an ordinary (spelled) property name — the record
|
|
4052
|
-
* shape is exactly what tsc computed for the literal (the late-bound
|
|
4053
|
-
* name IS the checker's literal type), so the fold is just spelling.
|
|
4054
|
-
* PURE key forms only, so skipping the key's evaluation is exact (JS
|
|
4055
|
-
* evaluates the key before the value; effectful expressions keep the
|
|
4056
|
-
* fence): any side-effect-free expression — identifiers, property
|
|
4057
|
-
* chains with no accessor on them (enum members, `other.name` reads),
|
|
4058
|
-
* literals, parenthesized/as-wrapped forms, operators over those —
|
|
4059
|
-
* whose checker type is ONE string or number literal (or a union whose
|
|
4060
|
-
* arms all spell the same name — `E1.x || E2.x` where both are 0).
|
|
4061
|
-
* Number keys take JS's canonical spelling (`{ [E.member]: v }` stores
|
|
4062
|
-
* "0" — ToPropertyKey), exactly the name tsc late-bound. Templates
|
|
4063
|
-
* fold structurally span by span as well (a template of literals whose
|
|
4064
|
-
* checker type widened still spells one string). Symbol-typed and
|
|
4065
|
-
* runtime-valued keys stay out. */
|
|
4066
|
-
function literalComputedKey(lowerer, name) {
|
|
4067
|
-
return foldedStringKeyOf(lowerer, name.expression);
|
|
4068
|
-
}
|
|
4069
|
-
export function foldedStringKeyOf(lowerer, expr) {
|
|
4070
|
-
if (ts.isParenthesizedExpression(expr))
|
|
4071
|
-
return foldedStringKeyOf(lowerer, expr.expression);
|
|
4072
|
-
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
|
|
4073
|
-
return expr.text;
|
|
4074
|
-
if (ts.isTemplateExpression(expr)) {
|
|
4075
|
-
// Structural fold first — exact even where the checker widened the
|
|
4076
|
-
// template's own type; the general type-directed fold below is the
|
|
4077
|
-
// fallback (a template the checker DID type as one literal).
|
|
4078
|
-
let out = expr.head.text;
|
|
4079
|
-
let folded = true;
|
|
4080
|
-
for (const span of expr.templateSpans) {
|
|
4081
|
-
const part = foldedStringKeyOf(lowerer, span.expression);
|
|
4082
|
-
if (part === null) {
|
|
4083
|
-
folded = false;
|
|
4084
|
-
break;
|
|
4085
|
-
}
|
|
4086
|
-
out += part + span.literal.text;
|
|
4087
|
-
}
|
|
4088
|
-
if (folded)
|
|
4089
|
-
return out;
|
|
4090
|
-
}
|
|
4091
|
-
// The general fold: a PURE expression whose checker type spells one
|
|
4092
|
-
// property name. An as-cast folds by its OWN checker type — that is
|
|
4093
|
-
// the name tsc late-bound the property under, so shape and storage
|
|
4094
|
-
// stay one name (the standard trust-the-checker bet); a cast that
|
|
4095
|
-
// WIDENS to string un-late-binds the property, its type is no literal,
|
|
4096
|
-
// and the fence stays.
|
|
4097
|
-
if (!pureKeyExpr(lowerer, expr))
|
|
4098
|
-
return null;
|
|
4099
|
-
return literalKeySpellingOf(lowerer.typeOf(expr));
|
|
4100
|
-
}
|
|
4101
|
-
/** The property-name spelling of a single-literal checker type: string
|
|
4102
|
-
* literals directly, number literals in JS's canonical ToString spelling
|
|
4103
|
-
* (enum members included — their literal flags carry the value), and
|
|
4104
|
-
* unions whose arms all spell the SAME name (`E1.x || E2.x` — two enum
|
|
4105
|
-
* literal types, one value). Null for everything else. */
|
|
4106
|
-
function literalKeySpellingOf(t) {
|
|
4107
|
-
if (t.isStringLiteralType())
|
|
4108
|
-
return t.value;
|
|
4109
|
-
if (t.isNumberLiteralType())
|
|
4110
|
-
return String(t.value);
|
|
4111
|
-
if (t.isUnionType()) {
|
|
4112
|
-
let out = null;
|
|
4113
|
-
for (const arm of ts.constituentTypes(t)) {
|
|
4114
|
-
const s = arm.isStringLiteralType() ? arm.value : arm.isNumberLiteralType() ? String(arm.value) : null;
|
|
4115
|
-
if (s === null || (out !== null && s !== out))
|
|
4116
|
-
return null;
|
|
4117
|
-
out = s;
|
|
4118
|
-
}
|
|
4119
|
-
return out;
|
|
4120
|
-
}
|
|
4121
|
-
return null;
|
|
4122
|
-
}
|
|
4123
|
-
/** True iff evaluating `expr` can have no observable effect, so a fold
|
|
4124
|
-
* may skip it: literals, identifier reads, property chains whose every
|
|
4125
|
-
* member is a plain field/enum member (an accessor anywhere on the chain
|
|
4126
|
-
* is a call), operators and casts over those. Calls, `new`, assignments,
|
|
4127
|
-
* and element accesses stay impure. */
|
|
4128
|
-
function pureKeyExpr(lowerer, expr) {
|
|
4129
|
-
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr))
|
|
4130
|
-
return pureKeyExpr(lowerer, expr.expression);
|
|
4131
|
-
if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr))
|
|
4132
|
-
return pureKeyExpr(lowerer, expr.expression);
|
|
4133
|
-
if (ts.isIdentifier(expr))
|
|
4134
|
-
return true;
|
|
4135
|
-
if (ts.isStringLiteralLike(expr) || ts.isNumericLiteral(expr))
|
|
4136
|
-
return true;
|
|
4137
|
-
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword ||
|
|
4138
|
-
expr.kind === ts.SyntaxKind.NullKeyword)
|
|
4139
|
-
return true;
|
|
4140
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
4141
|
-
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
|
|
4142
|
-
if (sym && sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))
|
|
4143
|
-
return false;
|
|
4144
|
-
return pureKeyExpr(lowerer, expr.expression);
|
|
4145
|
-
}
|
|
4146
|
-
if (ts.isPrefixUnaryExpression(expr) &&
|
|
4147
|
-
(expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken ||
|
|
4148
|
-
expr.operator === ts.SyntaxKind.ExclamationToken || expr.operator === ts.SyntaxKind.TildeToken)) {
|
|
4149
|
-
return pureKeyExpr(lowerer, expr.operand);
|
|
4150
|
-
}
|
|
4151
|
-
if (ts.isBinaryExpression(expr)) {
|
|
4152
|
-
const op = expr.operatorToken.kind;
|
|
4153
|
-
const pureOp = op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken ||
|
|
4154
|
-
op === ts.SyntaxKind.QuestionQuestionToken ||
|
|
4155
|
-
(op >= ts.SyntaxKind.LessThanToken && op <= ts.SyntaxKind.CaretToken &&
|
|
4156
|
-
op !== ts.SyntaxKind.InstanceOfKeyword && op !== ts.SyntaxKind.InKeyword);
|
|
4157
|
-
return pureOp && pureKeyExpr(lowerer, expr.left) && pureKeyExpr(lowerer, expr.right);
|
|
4158
|
-
}
|
|
4159
|
-
if (ts.isConditionalExpression(expr)) {
|
|
4160
|
-
return pureKeyExpr(lowerer, expr.condition) && pureKeyExpr(lowerer, expr.whenTrue) && pureKeyExpr(lowerer, expr.whenFalse);
|
|
4161
|
-
}
|
|
4162
|
-
if (ts.isTemplateExpression(expr))
|
|
4163
|
-
return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression));
|
|
4164
|
-
return false;
|
|
4165
|
-
}
|
|
4166
|
-
/** A property's field name: identifier/string-literal keys directly,
|
|
4167
|
-
* foldable computed keys through literalComputedKey (the syntax pass
|
|
4168
|
-
* fenced everything else). */
|
|
4169
|
-
function propNameText(lowerer, name) {
|
|
4170
|
-
if (ts.isComputedPropertyName(name)) {
|
|
4171
|
-
const k = literalComputedKey(lowerer, name);
|
|
4172
|
-
if (k === null)
|
|
4173
|
-
throw new InternalCompilerError("lowerer bug: unfoldable computed key past the fence");
|
|
4174
|
-
return k;
|
|
4175
|
-
}
|
|
4176
|
-
// Numeric literals name their canonical string key (JS's ToPropertyKey
|
|
4177
|
-
// — the same name the checker gives the property symbol).
|
|
4178
|
-
if (ts.isNumericLiteral(name))
|
|
4179
|
-
return String(Number(name.text));
|
|
4180
|
-
return name.text;
|
|
4181
|
-
}
|
|
4182
|
-
/** The runtime-keyed JS object literal (a computed key that doesn't fold):
|
|
4183
|
-
* builds a dyn object member-by-member. Keys evaluate before their values,
|
|
4184
|
-
* properties in source order — JS's object-literal evaluation exactly.
|
|
4185
|
-
* Identifier/string keys are compile-time strings; numeric keys take
|
|
4186
|
-
* their canonical number string (`{ 0x10: v }` stores "16" — JS's
|
|
4187
|
-
* ToPropertyKey); computed keys evaluate and pass through ToString (the
|
|
4188
|
-
* dyn's String() for dyn operands — `{ [field]: v }` where field is a
|
|
4189
|
-
* checked-dynamic param). Values convert through the usual dyn boundary
|
|
4190
|
-
* (dynFrom's domain, functions box); a value with no dyn representation
|
|
4191
|
-
* fences per property. Spreads copy through dyn.assign in source order;
|
|
4192
|
-
* accessors stay fenced. */
|
|
4193
|
-
export function lowerDynObjectLiteral(lowerer, expr, boxValue) {
|
|
4194
|
-
const loc = locOf(expr);
|
|
4195
|
-
let fields = [];
|
|
4196
|
-
let acc = null;
|
|
4197
|
-
const flushFields = () => {
|
|
4198
|
-
if (fields.length === 0)
|
|
4199
|
-
return;
|
|
4200
|
-
const chunk = { kind: "dynObjLit", fields, type: DYN, loc };
|
|
4201
|
-
acc = acc === null
|
|
4202
|
-
? chunk
|
|
4203
|
-
: { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc };
|
|
4204
|
-
fields = [];
|
|
4205
|
-
};
|
|
4206
|
-
for (const prop of expr.properties) {
|
|
4207
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
4208
|
-
flushFields();
|
|
4209
|
-
const raw = lowerer.lowerExpr(prop.expression);
|
|
4210
|
-
const source = boxValue
|
|
4211
|
-
? boxValue(prop.expression, raw)
|
|
4212
|
-
: lowerer.coerceToExpected(raw, DYN);
|
|
4213
|
-
if (source.type.kind !== "dyn") {
|
|
4214
|
-
lowerer.unsupported("SC1101", prop.expression, `spreading '${lowerer.fmt(source.type)}' into a checked-dynamic object literal`);
|
|
4215
|
-
}
|
|
4216
|
-
acc ??= { kind: "dynObjLit", fields: [], type: DYN, loc };
|
|
4217
|
-
acc = { kind: "libCall", fn: "dyn.assign", args: [acc, source], type: DYN, loc: locOf(prop) };
|
|
4218
|
-
continue;
|
|
4219
|
-
}
|
|
4220
|
-
if (!ts.isPropertyAssignment(prop) &&
|
|
4221
|
-
!ts.isShorthandPropertyAssignment(prop) &&
|
|
4222
|
-
!ts.isMethodDeclaration(prop)) {
|
|
4223
|
-
lowerer.unsupported("SC1090", prop, "accessors in a runtime-keyed (computed-key) object literal");
|
|
4224
|
-
}
|
|
4225
|
-
const name = prop.name;
|
|
4226
|
-
let key;
|
|
4227
|
-
if (ts.isComputedPropertyName(name)) {
|
|
4228
|
-
const folded = literalComputedKey(lowerer, name);
|
|
4229
|
-
if (folded !== null) {
|
|
4230
|
-
key = { kind: "strLit", value: folded, type: STRING, loc: locOf(name) };
|
|
4231
|
-
}
|
|
4232
|
-
else {
|
|
4233
|
-
let k = lowerer.lowerExpr(name.expression);
|
|
4234
|
-
if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
|
|
4235
|
-
k = { kind: "toString", operand: k, type: STRING, loc: locOf(name) };
|
|
4236
|
-
}
|
|
4237
|
-
if (k.type.kind !== "string") {
|
|
4238
|
-
lowerer.unsupported("SC1090", name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
|
|
4239
|
-
}
|
|
4240
|
-
key = k;
|
|
4241
|
-
}
|
|
4242
|
-
}
|
|
4243
|
-
else if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
4244
|
-
key = { kind: "strLit", value: name.text, type: STRING, loc: locOf(name) };
|
|
4245
|
-
}
|
|
4246
|
-
else if (ts.isNumericLiteral(name)) {
|
|
4247
|
-
key = { kind: "strLit", value: String(Number(name.text)), type: STRING, loc: locOf(name) };
|
|
4248
|
-
}
|
|
4249
|
-
else {
|
|
4250
|
-
lowerer.unsupported("SC1090", prop, "non-identifier property names");
|
|
4251
|
-
}
|
|
4252
|
-
const valueExpr = ts.isMethodDeclaration(prop)
|
|
4253
|
-
? prop
|
|
4254
|
-
: ts.isShorthandPropertyAssignment(prop)
|
|
4255
|
-
? prop.name
|
|
4256
|
-
: prop.initializer;
|
|
4257
|
-
// Lambda values that fail to lower become trap closures (the
|
|
4258
|
-
// per-call fence granularity — fenceClosureProbe) so the object
|
|
4259
|
-
// still builds; everything else keeps the per-property fence below.
|
|
4260
|
-
let raw;
|
|
4261
|
-
const propDiagsBefore = lowerer.diags.length;
|
|
4262
|
-
try {
|
|
4263
|
-
const lowerValue = () => ts.isMethodDeclaration(prop)
|
|
4264
|
-
? (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop))
|
|
4265
|
-
: lowerer.lowerExpr(valueExpr);
|
|
4266
|
-
raw =
|
|
4267
|
-
fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
|
|
4268
|
-
lowerValue();
|
|
4269
|
-
}
|
|
4270
|
-
catch (err) {
|
|
4271
|
-
// A PURE member read a JS file cannot lower (a namespace object
|
|
4272
|
-
// in an export aggregate): the slot takes a boxed fence closure —
|
|
4273
|
-
// the diagnostics defer to the runtime-fence ledger, the object
|
|
4274
|
-
// builds, and only USING the member stops the run.
|
|
4275
|
-
const pureMember = ts.isIdentifier(valueExpr);
|
|
4276
|
-
if (!(err instanceof PoisonError) || !pureMember)
|
|
4277
|
-
throw err;
|
|
4278
|
-
const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, {
|
|
4279
|
-
kind: "closure",
|
|
4280
|
-
name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
|
|
4281
|
-
returnType: VOID,
|
|
4282
|
-
type: { kind: "func", params: [], ret: VOID },
|
|
4283
|
-
});
|
|
4284
|
-
if (!fence)
|
|
4285
|
-
throw err;
|
|
4286
|
-
raw = fence;
|
|
4287
|
-
}
|
|
4288
|
-
let v = boxValue
|
|
4289
|
-
? boxValue(valueExpr, raw)
|
|
4290
|
-
: lowerer.coerceToExpected(raw, DYN);
|
|
4291
|
-
if (v.type.kind !== "dyn") {
|
|
4292
|
-
const convDiagsBefore = lowerer.diags.length;
|
|
4293
|
-
try {
|
|
4294
|
-
lowerer.unsupported("SC1101", valueExpr, `holding '${lowerer.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
|
|
4295
|
-
}
|
|
4296
|
-
catch (err) {
|
|
4297
|
-
// A member value the checked-dynamic tree cannot hold — a func whose signature
|
|
4298
|
-
// cannot box, an island ('any') handle, a record carrying func
|
|
4299
|
-
// fields (the export aggregate's typed utilities and npm-handle
|
|
4300
|
-
// members): the same call-time fence deferral as an unlowerable
|
|
4301
|
-
// member — the slot takes a boxed fence closure; only USING it
|
|
4302
|
-
// stops the run. Probe mode and ICEs keep the poison.
|
|
4303
|
-
if (!(err instanceof PoisonError))
|
|
4304
|
-
throw err;
|
|
4305
|
-
const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, {
|
|
4306
|
-
kind: "closure",
|
|
4307
|
-
name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
|
|
4308
|
-
returnType: VOID,
|
|
4309
|
-
type: { kind: "func", params: [], ret: VOID },
|
|
4310
|
-
});
|
|
4311
|
-
if (!fence)
|
|
4312
|
-
throw err;
|
|
4313
|
-
v = lowerer.coerceToExpected(fence, DYN);
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
|
-
fields.push({ key, value: v });
|
|
4317
|
-
}
|
|
4318
|
-
flushFields();
|
|
4319
|
-
return acc ?? { kind: "dynObjLit", fields: [], type: DYN, loc };
|
|
4320
|
-
}
|
|
4321
|
-
/** Spreading a source whose shape carries accessor slots: Node's copy
|
|
4322
|
-
* invokes each getter exactly once in insertion order — even for keys a
|
|
4323
|
-
* later contributor overrides — while the field-copy desugar assumes pure
|
|
4324
|
-
* reads it may drop or reorder. Getter-call counts would silently diverge,
|
|
4325
|
-
* so accessor-carrying sources fence by name. */
|
|
4326
|
-
function fenceAccessorSpreadSource(lowerer, prop, srcShape) {
|
|
4327
|
-
if (srcShape && shapeHasAccessorSlots(srcShape)) {
|
|
4328
|
-
lowerer.unsupported("SC1090", prop, "object spread of sources carrying get/set accessor properties (Node invokes each getter once during the copy — the field-copy desugar cannot model the calls; bind the reads to consts first)");
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
3752
|
/** Convert an undefined-armed numeric value at an arithmetic use. Ordinary
|
|
4332
3753
|
* reads preserve their tagged value; JavaScript's ToNumber(undefined) is NaN,
|
|
4333
3754
|
* so compounds and binary arithmetic use this checked one-time extraction. */
|
|
@@ -4347,7 +3768,7 @@ export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
|
|
|
4347
3768
|
return operand;
|
|
4348
3769
|
let stable = operand;
|
|
4349
3770
|
let prefix = [];
|
|
4350
|
-
if (!
|
|
3771
|
+
if (!isSafeToRepeat(operand)) {
|
|
4351
3772
|
const tmp = lowerer.declareHiddenLocal("%arith", operand.type);
|
|
4352
3773
|
stable = varRef(tmp.id, operand.type, loc);
|
|
4353
3774
|
prefix = [{ kind: "varDecl", localId: tmp.id, init: operand, loc }];
|
|
@@ -4364,1499 +3785,6 @@ export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
|
|
|
4364
3785
|
};
|
|
4365
3786
|
return prefix.length === 0 ? value : { kind: "seqExpr", stmts: prefix, result: value, type: F64, loc };
|
|
4366
3787
|
}
|
|
4367
|
-
/** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
|
|
4368
|
-
* lambda whose body fails to lower inside a JS object literal becomes a
|
|
4369
|
-
* closure of the field's exact func type whose body is the runtime fence —
|
|
4370
|
-
* the object still BUILDS (commander's `_outputConfiguration` table: the
|
|
4371
|
-
* driven writeOut/writeErr entries work; an unloweraable color probe traps
|
|
4372
|
-
* only if something CALLS it). The failed lambda's diagnostics move to the
|
|
4373
|
-
* runtime-fence inventory, exactly like a fenced JS statement. Null when
|
|
4374
|
-
* the fallback does not apply (not a JS file, not a lambda, no func slot)
|
|
4375
|
-
* — the caller lowers normally. ICEs (SC9001) never convert. */
|
|
4376
|
-
function fenceClosureProbe(lowerer, node, slotType, attempt) {
|
|
4377
|
-
if (!isJsSourceFile(node.getSourceFile()))
|
|
4378
|
-
return null;
|
|
4379
|
-
let inner = node;
|
|
4380
|
-
while (ts.isParenthesizedExpression(inner))
|
|
4381
|
-
inner = inner.expression;
|
|
4382
|
-
if (!ts.isArrowFunction(inner) && !ts.isFunctionExpression(inner) && !ts.isMethodDeclaration(inner)) {
|
|
4383
|
-
return null;
|
|
4384
|
-
}
|
|
4385
|
-
// The trap's ABI: the field's func slot when one exists; otherwise (the
|
|
4386
|
-
// dyn-object literal path — the value boxes into dyn) the all-dyn
|
|
4387
|
-
// signature of the lambda's own arity, which canBoxFuncIntoDyn always
|
|
4388
|
-
// admits.
|
|
4389
|
-
const fieldType = slotType !== undefined && slotType.kind === "func"
|
|
4390
|
-
? slotType
|
|
4391
|
-
: { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN };
|
|
4392
|
-
if (slotType !== undefined && slotType.kind !== "func")
|
|
4393
|
-
return null;
|
|
4394
|
-
const diagsBefore = lowerer.diags.length;
|
|
4395
|
-
try {
|
|
4396
|
-
return attempt();
|
|
4397
|
-
}
|
|
4398
|
-
catch (e) {
|
|
4399
|
-
if (!(e instanceof PoisonError))
|
|
4400
|
-
throw e;
|
|
4401
|
-
const params = fieldType.params.map((t, i) => ({ localId: `p.${i}`, name: `p${i}`, type: t }));
|
|
4402
|
-
const fence = lowerer.deferToRuntimeFence(diagsBefore, node, {
|
|
4403
|
-
kind: "closure",
|
|
4404
|
-
name: () => `%fence.fn.${lowerer.liftedFns.length}`,
|
|
4405
|
-
params,
|
|
4406
|
-
returnType: fieldType.ret,
|
|
4407
|
-
paramsMutable: true,
|
|
4408
|
-
type: fieldType,
|
|
4409
|
-
fallback: { code: "SC1090", message: "this function's body has no static lowering" },
|
|
4410
|
-
bareMessage: true,
|
|
4411
|
-
allowDiagSink: true,
|
|
4412
|
-
});
|
|
4413
|
-
if (!fence)
|
|
4414
|
-
throw e;
|
|
4415
|
-
return fence;
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
|
-
/** The union arm an object LITERAL inhabits when its fields must widen
|
|
4419
|
-
* PER FIELD into exactly one record arm — the reducer-action pattern
|
|
4420
|
-
* (`{ kind: "a", parsed: 1 }` into `{ kind: "a"; parsed: number | null }
|
|
4421
|
-
* | { kind: "b" }`). The IR shapes erased the literal types tsc
|
|
4422
|
-
* discriminated on, so the probe runs against the contextual union's
|
|
4423
|
-
* CHECKER members: every literal field must exist on the member
|
|
4424
|
-
* (excess-property freshness — tsc already enforced it), every arm field
|
|
4425
|
-
* missing from the literal must be optional-flavored (an undefined-armed
|
|
4426
|
-
* union, or a dyn slot — the absent-completion rule), and each present
|
|
4427
|
-
* field's LITERAL type must fit the member's field type — literal against
|
|
4428
|
-
* literal decides by VALUE (the discriminant), everything else by the
|
|
4429
|
-
* widened IR pair under the width-lift relation. Exactly ONE fitting arm
|
|
4430
|
-
* answers it; zero or several answer null and the caller keeps its
|
|
4431
|
-
* fences. Plain property-assignment/shorthand literals only — spreads,
|
|
4432
|
-
* accessors, methods, and unfoldable computed keys keep their own paths. */
|
|
4433
|
-
function literalUnionArmOf(lowerer, expr, tsType, recordArms) {
|
|
4434
|
-
if (!tsType.isUnionType())
|
|
4435
|
-
return null;
|
|
4436
|
-
const props = [];
|
|
4437
|
-
for (const p of expr.properties) {
|
|
4438
|
-
if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
|
|
4439
|
-
props.push({ name: propNameText(lowerer, p.name), node: p.initializer });
|
|
4440
|
-
}
|
|
4441
|
-
else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) {
|
|
4442
|
-
props.push({ name: p.name.text, node: p.name });
|
|
4443
|
-
}
|
|
4444
|
-
else {
|
|
4445
|
-
return null;
|
|
4446
|
-
}
|
|
4447
|
-
}
|
|
4448
|
-
/** litT fits ftT: unions per arm; literal-vs-literal by value; unit
|
|
4449
|
-
* types only into their own unit; otherwise the widened IR pair must be
|
|
4450
|
-
* equal or width-liftable. */
|
|
4451
|
-
const fits = (litT, ftT) => {
|
|
4452
|
-
if (ftT.isUnionType())
|
|
4453
|
-
return ts.constituentTypes(ftT).some((a) => fits(litT, a));
|
|
4454
|
-
if (ftT.isStringLiteralType())
|
|
4455
|
-
return litT.isStringLiteralType() && litT.value === ftT.value;
|
|
4456
|
-
if (ftT.isNumberLiteralType())
|
|
4457
|
-
return litT.isNumberLiteralType() && litT.value === ftT.value;
|
|
4458
|
-
if (ftT.flags & ts.TypeFlags.BooleanLiteral) {
|
|
4459
|
-
return (litT.flags & ts.TypeFlags.BooleanLiteral) !== 0 && lowerer.checker.typeToString(litT) === lowerer.checker.typeToString(ftT);
|
|
4460
|
-
}
|
|
4461
|
-
if (ftT.flags & ts.TypeFlags.Null)
|
|
4462
|
-
return (litT.flags & ts.TypeFlags.Null) !== 0;
|
|
4463
|
-
if (ftT.flags & ts.TypeFlags.Undefined)
|
|
4464
|
-
return (litT.flags & ts.TypeFlags.Undefined) !== 0;
|
|
4465
|
-
if (litT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined))
|
|
4466
|
-
return false;
|
|
4467
|
-
const li = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(litT));
|
|
4468
|
-
const fi = lowerer.mapTypeOf(ftT);
|
|
4469
|
-
if (!li || !fi)
|
|
4470
|
-
return false;
|
|
4471
|
-
return typeEquals(li, fi) || lowerer.widthLiftPlan(li, fi) !== null;
|
|
4472
|
-
};
|
|
4473
|
-
const armShapeIds = new Set(recordArms.map((a) => a.shapeId));
|
|
4474
|
-
const candidates = new Set();
|
|
4475
|
-
for (const member of ts.constituentTypes(tsType)) {
|
|
4476
|
-
const mMapped = lowerer.mapTypeOf(member);
|
|
4477
|
-
if (mMapped?.kind !== "record" || !armShapeIds.has(mMapped.shapeId) || candidates.has(mMapped.shapeId))
|
|
4478
|
-
continue;
|
|
4479
|
-
const shape = lowerer.shapes.get(mMapped.shapeId);
|
|
4480
|
-
if (!shape || shape.tuple || shape.indexValue)
|
|
4481
|
-
continue;
|
|
4482
|
-
// Excess-property freshness: every literal field must exist on the
|
|
4483
|
-
// member (tsc rejected the others for a fresh literal).
|
|
4484
|
-
if (!props.every((p) => lowerer.checker.getPropertyOfType(member, p.name) !== undefined))
|
|
4485
|
-
continue;
|
|
4486
|
-
// Arm fields the literal leaves unset must be optional-flavored (the
|
|
4487
|
-
// absent-completion rule: an undefined-armed union or a dyn slot).
|
|
4488
|
-
const names = new Set(props.map((p) => p.name));
|
|
4489
|
-
const absentOk = shape.fields.every((f) => {
|
|
4490
|
-
if (names.has(f.name))
|
|
4491
|
-
return true;
|
|
4492
|
-
if (f.type.kind === "dyn")
|
|
4493
|
-
return true;
|
|
4494
|
-
if (f.type.kind !== "union")
|
|
4495
|
-
return false;
|
|
4496
|
-
return lowerer.unions.get(f.type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false;
|
|
4497
|
-
});
|
|
4498
|
-
if (!absentOk)
|
|
4499
|
-
continue;
|
|
4500
|
-
const fieldsFit = props.every((p) => {
|
|
4501
|
-
const sym = lowerer.checker.getPropertyOfType(member, p.name);
|
|
4502
|
-
if (!sym)
|
|
4503
|
-
return false;
|
|
4504
|
-
return fits(lowerer.typeOf(p.node), lowerer.checker.getTypeOfSymbol(sym));
|
|
4505
|
-
});
|
|
4506
|
-
if (fieldsFit)
|
|
4507
|
-
candidates.add(mMapped.shapeId);
|
|
4508
|
-
}
|
|
4509
|
-
if (candidates.size !== 1)
|
|
4510
|
-
return null;
|
|
4511
|
-
const [only] = candidates;
|
|
4512
|
-
return recordArms.find((a) => a.shapeId === only) ?? null;
|
|
4513
|
-
}
|
|
4514
|
-
/** Object spread treats nullish sources as empty. Preserve evaluation once,
|
|
4515
|
-
* then turn `Record<K, V> | undefined` into either that record or a fresh
|
|
4516
|
-
* empty record of the same represented shape. */
|
|
4517
|
-
function lowerOptionalIndexSpreadSource(lowerer, source, loc) {
|
|
4518
|
-
if (source.type.kind !== "union")
|
|
4519
|
-
return null;
|
|
4520
|
-
const def = lowerer.unions.get(source.type.unionId);
|
|
4521
|
-
if (!def || def.arms.length !== 2)
|
|
4522
|
-
return null;
|
|
4523
|
-
const undefinedTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
|
|
4524
|
-
const recordTag = def.arms.findIndex((arm) => arm.kind === "record");
|
|
4525
|
-
if (undefinedTag < 0 || recordTag < 0)
|
|
4526
|
-
return null;
|
|
4527
|
-
const recordType = def.arms[recordTag];
|
|
4528
|
-
if (recordType.kind !== "record")
|
|
4529
|
-
return null;
|
|
4530
|
-
const prefix = [];
|
|
4531
|
-
let stable = source;
|
|
4532
|
-
if (!pureReemittable(source)) {
|
|
4533
|
-
const local = lowerer.declareHiddenLocal("%spread", source.type);
|
|
4534
|
-
prefix.push({ kind: "varDecl", localId: local.id, init: source, loc });
|
|
4535
|
-
stable = varRef(local.id, source.type, loc);
|
|
4536
|
-
}
|
|
4537
|
-
const selected = {
|
|
4538
|
-
kind: "ternary",
|
|
4539
|
-
cond: { kind: "unionIsTag", unionId: source.type.unionId, tag: undefinedTag, negated: false, value: stable, type: BOOL, loc },
|
|
4540
|
-
then: { kind: "recordLit", fields: [], type: recordType, loc },
|
|
4541
|
-
else_: { kind: "unionNarrow", unionId: source.type.unionId, tag: recordTag, value: stable, type: recordType, loc },
|
|
4542
|
-
type: recordType,
|
|
4543
|
-
loc,
|
|
4544
|
-
};
|
|
4545
|
-
return prefix.length === 0 ? selected : { kind: "seqExpr", stmts: prefix, result: selected, type: recordType, loc };
|
|
4546
|
-
}
|
|
4547
|
-
export function lowerObjectLiteral(lowerer, expr) {
|
|
4548
|
-
const loc = locOf(expr);
|
|
4549
|
-
// The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
|
|
4550
|
-
// compile-time string means the literal's shape is not a compile-time
|
|
4551
|
-
// fact — no record shape can hold it. The whole literal builds as a dyn
|
|
4552
|
-
// object instead (`{ [field]: criteria, actual: 0, ... }` —
|
|
4553
|
-
// test/common's _mustCallInner context), where keys are runtime string
|
|
4554
|
-
// values. TypeScript keeps the record world and its fence: this shape
|
|
4555
|
-
// only arises in checked-dynamic JS.
|
|
4556
|
-
if (isJsSourceFile(expr.getSourceFile()) &&
|
|
4557
|
-
expr.properties.some((p) => {
|
|
4558
|
-
const n = ts.isSpreadAssignment(p) ? undefined : p.name;
|
|
4559
|
-
return n !== undefined && ts.isComputedPropertyName(n) && literalComputedKey(lowerer, n) === null;
|
|
4560
|
-
})) {
|
|
4561
|
-
return lowerDynObjectLiteral(lowerer, expr);
|
|
4562
|
-
}
|
|
4563
|
-
// Syntax fence FIRST: unsupported member forms get their specific
|
|
4564
|
-
// message even when they also make the literal's type unmappable
|
|
4565
|
-
// (a getter or a `this`-returning method would otherwise surface as an
|
|
4566
|
-
// opaque type fence on the whole literal).
|
|
4567
|
-
for (const prop of expr.properties) {
|
|
4568
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
4569
|
-
// Supported shapes: FULL spreads of known record shapes read as
|
|
4570
|
-
// identifiers or other side-effect-free reads, all BEFORE any
|
|
4571
|
-
// explicit property (the field-copy desugar reads spread fields
|
|
4572
|
-
// first, exactly JS's eager copy order); and CONDITIONAL spreads
|
|
4573
|
-
// `...(c ? {k: v} : {})` — the optional-field idiom tsc types as
|
|
4574
|
-
// `k?: ...` — which desugar to one conditional field at the
|
|
4575
|
-
// spread's own position (any position: they introduce one fresh
|
|
4576
|
-
// name, collision-fenced below). Everything else stays fenced.
|
|
4577
|
-
const cs = conditionalSpreadOf(prop.expression);
|
|
4578
|
-
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
|
4579
|
-
lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)");
|
|
4580
|
-
}
|
|
4581
|
-
// Spread ORDER is fenced in the field-by-field desugar below, not
|
|
4582
|
-
// here: the index-signature merge path supports any order (keyed
|
|
4583
|
-
// last-write-wins writes), so `{ K: v, ...extra }` into a pure
|
|
4584
|
-
// Record shape compiles — the buildServiceEnv pattern.
|
|
4585
|
-
continue;
|
|
4586
|
-
}
|
|
4587
|
-
if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
|
|
4588
|
-
// JavaScript CJS-export tables reach reads through the lifted-
|
|
4589
|
-
// accessor path (cjsExportAccessorRead) — the literal VALUE
|
|
4590
|
-
// narrows to its plain fields below (accessor names are not
|
|
4591
|
-
// record storage; SEMANTICS.md documents the enumeration
|
|
4592
|
-
// divergence). TypeScript accessors lower into the shape's
|
|
4593
|
-
// reserved closure slots (%get:/%set: — see accessorSlotProp);
|
|
4594
|
-
// `this` in the body is fenced up front: the closure slot has no
|
|
4595
|
-
// receiver (capturing the record under construction would be an
|
|
4596
|
-
// RC cycle), and the generic lexical-this walk would silently
|
|
4597
|
-
// capture an ENCLOSING method's `this` — the object-method rule.
|
|
4598
|
-
if (!isJsSourceFile(expr.getSourceFile())) {
|
|
4599
|
-
rejectThisInObjectAccessor(lowerer, prop.body ?? prop);
|
|
4600
|
-
}
|
|
4601
|
-
}
|
|
4602
|
-
// Unfoldable computed keys are NOT fenced here: an index-signature
|
|
4603
|
-
// target lowers them as runtime keyed writes (the merge path below);
|
|
4604
|
-
// every other target re-fences them after the merge path declined
|
|
4605
|
-
// (fenceUnfoldableComputedKeys).
|
|
4606
|
-
// Identifier keys, STRING-LITERAL keys (`"content-type": v` —
|
|
4607
|
-
// record field names are data, never C identifiers; the mangler
|
|
4608
|
-
// encodes what C can't spell), NUMERIC-LITERAL keys in their
|
|
4609
|
-
// canonical string spelling (`{ 0: v }` stores "0", `{ 0x10: v }`
|
|
4610
|
-
// stores "16" — JS's ToPropertyKey; the checker names the property
|
|
4611
|
-
// symbol the same way), and FOLDABLE computed keys (above).
|
|
4612
|
-
if (!prop.name ||
|
|
4613
|
-
!(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ||
|
|
4614
|
-
ts.isNumericLiteral(prop.name) || ts.isComputedPropertyName(prop.name))) {
|
|
4615
|
-
lowerer.unsupported("SC1090", prop, "non-identifier property names");
|
|
4616
|
-
}
|
|
4617
|
-
// Shorthand methods are just closure-valued fields — but their `this`
|
|
4618
|
-
// is dynamically bound (typed as the literal by tsc, so noImplicitThis
|
|
4619
|
-
// lets it through) and records don't model it; the generic
|
|
4620
|
-
// lexical-this walk would silently capture an ENCLOSING method's
|
|
4621
|
-
// `this`, so any `this` in the body is rejected up front.
|
|
4622
|
-
if (ts.isMethodDeclaration(prop))
|
|
4623
|
-
lowerer.rejectThisInObjectMethod(prop.body ?? prop);
|
|
4624
|
-
}
|
|
4625
|
-
let tsType = lowerer.checker.getContextualType(expr) ?? lowerer.typeOf(expr);
|
|
4626
|
-
// `lit satisfies T` is TYPE-LEVEL only: the expression's checker type —
|
|
4627
|
-
// and therefore the shape every downstream consumer sees — is the
|
|
4628
|
-
// literal's OWN type (T still contextually types members, so inferred
|
|
4629
|
-
// parameter types flow). Building at T would reshape the value tsc
|
|
4630
|
-
// says has the literal's type: `{...} satisfies Movable &
|
|
4631
|
-
// Record<string, unknown>` must NOT become an index-signature record.
|
|
4632
|
-
// Own type wins whenever it maps to a record; an unmappable own type
|
|
4633
|
-
// keeps the contextual fallback (a bare-null field whose satisfies
|
|
4634
|
-
// target names the wider slot type).
|
|
4635
|
-
{
|
|
4636
|
-
let p = expr.parent;
|
|
4637
|
-
while (ts.isParenthesizedExpression(p))
|
|
4638
|
-
p = p.parent;
|
|
4639
|
-
if (ts.isSatisfiesExpression(p)) {
|
|
4640
|
-
const own = lowerer.typeOf(expr);
|
|
4641
|
-
if (lowerer.mapTypeOf(own)?.kind === "record")
|
|
4642
|
-
tsType = own;
|
|
4643
|
-
}
|
|
4644
|
-
}
|
|
4645
|
-
// An async function's return position types the literal
|
|
4646
|
-
// `T | PromiseLike<T>` (the lib's await-unwrapping contract). The
|
|
4647
|
-
// PromiseLike arm never maps, and the record the return slot actually
|
|
4648
|
-
// holds is exactly the checker's awaited type — strip to it BEFORE the
|
|
4649
|
-
// own-type fallback below: the awaited contextual type carries the
|
|
4650
|
-
// slot's field types (`lanIp: string | null`), which the literal's own
|
|
4651
|
-
// type narrows away (a field written as `lanIp: null` types as bare
|
|
4652
|
-
// `null`, which maps to nothing on its own).
|
|
4653
|
-
if (tsType.isUnionType() && ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike")) {
|
|
4654
|
-
tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
|
|
4655
|
-
}
|
|
4656
|
-
let mapped = lowerer.mapTypeOf(tsType);
|
|
4657
|
-
// An EMPTY-record context under a NON-empty literal (`Object.keys({
|
|
4658
|
-
// ...process.env })` — the lib's `{}`-typed parameters admit every
|
|
4659
|
-
// object): `{}` carries no shape information, so the literal builds as
|
|
4660
|
-
// its OWN type, exactly the unmappable-context fallback below. An
|
|
4661
|
-
// empty LITERAL keeps the context (the shapes agree).
|
|
4662
|
-
if (mapped?.kind === "record" && expr.properties.length > 0) {
|
|
4663
|
-
const ctxShape = lowerer.shapes.get(mapped.shapeId);
|
|
4664
|
-
if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) {
|
|
4665
|
-
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
|
|
4666
|
-
}
|
|
4667
|
-
}
|
|
4668
|
-
// A literal with a property the contextual TYPE ITSELF lacks — only
|
|
4669
|
-
// reachable through type assertions and satisfies (fresh-literal
|
|
4670
|
-
// excess-property checks reject the direct spelling): the context
|
|
4671
|
-
// cannot hold the value, so the literal builds at its OWN type and the
|
|
4672
|
-
// slot's width coercion narrows it (divergence 36's copy stance — the
|
|
4673
|
-
// extra fields drop in the copy). The probe is against the CHECKER's
|
|
4674
|
-
// contextual type, not the mapped shape: a property the contextual
|
|
4675
|
-
// type carries but the shape dropped (settled value/reason,
|
|
4676
|
-
// generic-callable members) belongs to the drop paths below, and
|
|
4677
|
-
// index-signature contexts keep every key (overflow capture).
|
|
4678
|
-
if (mapped?.kind === "record" && expr.properties.length > 0) {
|
|
4679
|
-
const ctxShape = lowerer.shapes.get(mapped.shapeId);
|
|
4680
|
-
if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) {
|
|
4681
|
-
const names = new Set(ctxShape.fields.map((f) => f.name));
|
|
4682
|
-
const ctxHasProp = (name) => {
|
|
4683
|
-
const members = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
|
|
4684
|
-
return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined);
|
|
4685
|
-
};
|
|
4686
|
-
const extraOf = (text) => !names.has(text) && !ctxHasProp(text);
|
|
4687
|
-
const extra = expr.properties.some((p) => {
|
|
4688
|
-
if (ts.isSpreadAssignment(p) || !p.name)
|
|
4689
|
-
return false;
|
|
4690
|
-
if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
|
|
4691
|
-
return extraOf(p.name.text);
|
|
4692
|
-
if (ts.isNumericLiteral(p.name))
|
|
4693
|
-
return extraOf(String(Number(p.name.text)));
|
|
4694
|
-
return false; // computed keys keep their existing paths
|
|
4695
|
-
});
|
|
4696
|
-
if (extra)
|
|
4697
|
-
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
|
|
4698
|
-
}
|
|
4699
|
-
}
|
|
4700
|
-
// A union-typed slot (`const r: Res = { kind: "ok", ... }`) contextually
|
|
4701
|
-
// types the literal as the WHOLE union; build the literal as its own
|
|
4702
|
-
// (arm) shape and let the slot's coercion wrap it into the union. An
|
|
4703
|
-
// unknown-typed slot (`JSON.stringify({ a: 1 })`) likewise. An
|
|
4704
|
-
// UNMAPPABLE context falls back the same way — the literal's own type
|
|
4705
|
-
// is the plain record the slot coerces. A CLASS-INSTANCE context
|
|
4706
|
-
// (`const p: Point = { x, y }` — tsc's structural view of data
|
|
4707
|
-
// classes) falls back too: the literal builds as its own record and
|
|
4708
|
-
// the slot's width coercion constructs through the trivial
|
|
4709
|
-
// parameter-property constructor (recordToClassPlan), or fences with
|
|
4710
|
-
// the record-shape story.
|
|
4711
|
-
// A jsval-mapped context reaches here only when lowerExpr's island
|
|
4712
|
-
// gate DECLINED it (a project-declared typedef that absorbed to the
|
|
4713
|
-
// island through checker-`any` field residue): the literal builds at
|
|
4714
|
-
// its own type like every unmappable context.
|
|
4715
|
-
if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object" || mapped.kind === "jsval") {
|
|
4716
|
-
const ctxUnion = mapped?.kind === "union" ? mapped : null;
|
|
4717
|
-
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
|
|
4718
|
-
// A literal whose own shape re-tags into NO arm of the contextual
|
|
4719
|
-
// union, where the union has exactly ONE record arm: build AS that
|
|
4720
|
-
// arm — there is no ambiguity (tsc already checked the literal
|
|
4721
|
-
// against the union, and the record arm is the only shape it can
|
|
4722
|
-
// inhabit), and the arm's field types drive every property's
|
|
4723
|
-
// coercion (`{ env: {...spread...}, onCleanup }` against an
|
|
4724
|
-
// optional options param — the env value builds by ITS contextual
|
|
4725
|
-
// index-signature type and wraps into the arm's `| undefined`
|
|
4726
|
-
// field, where the literal's own inferred width would mismatch).
|
|
4727
|
-
// Empty literals (`_env = {}` — the fieldless own shape) and
|
|
4728
|
-
// literals against PURE index-signature arms (`{ OPENSSL_CONF:
|
|
4729
|
-
// candidate }` — keys become overflow entries) are the same rule's
|
|
4730
|
-
// simplest cases. The empty-array-in-union rule, record form.
|
|
4731
|
-
if (ctxUnion) {
|
|
4732
|
-
const def = lowerer.unions.get(ctxUnion.unionId);
|
|
4733
|
-
const recordArms = def?.arms.filter((a) => a.kind === "record") ?? [];
|
|
4734
|
-
if (recordArms.length === 1) {
|
|
4735
|
-
const armShape = lowerer.shapes.get(recordArms[0].shapeId);
|
|
4736
|
-
if ((mapped?.kind !== "record" || mapped.shapeId !== recordArms[0].shapeId) &&
|
|
4737
|
-
!armShape?.tuple) {
|
|
4738
|
-
mapped = recordArms[0];
|
|
4739
|
-
}
|
|
4740
|
-
}
|
|
4741
|
-
else if (recordArms.length > 1) {
|
|
4742
|
-
const ownShapeId = mapped?.kind === "record" ? mapped.shapeId : null;
|
|
4743
|
-
// SEVERAL record arms (the reducer-action / discriminated-message
|
|
4744
|
-
// pattern): the literal's own inferred shape re-tags into no arm
|
|
4745
|
-
// — its field types widened per field (`parsed: 1` against
|
|
4746
|
-
// `parsed: number | null`), so the IR-level candidate probe is
|
|
4747
|
-
// ambiguous (a narrower arm also admits the literal by dropping
|
|
4748
|
-
// fields). The LITERAL types tsc checked carry the discriminant
|
|
4749
|
-
// the shapes erased: match the literal's fields against each
|
|
4750
|
-
// union member's CHECKER types (literal-vs-literal field pairs
|
|
4751
|
-
// decide by value — the `kind: "a"` discriminant), and when
|
|
4752
|
-
// exactly ONE member fits, build AS that arm — its field types
|
|
4753
|
-
// drive every property's coercion, exactly the single-record-arm
|
|
4754
|
-
// rule above. Ambiguous literals keep the SC2003 fence.
|
|
4755
|
-
if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId)) {
|
|
4756
|
-
const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms);
|
|
4757
|
-
if (arm)
|
|
4758
|
-
mapped = arm;
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
}
|
|
4763
|
-
// The CJS EXPORT-TABLE literal in VALUE position (JS): importers reach
|
|
4764
|
-
// every member through alias plumbing and accessor lifts — the record
|
|
4765
|
-
// VALUE exists for the module's own reads (Object.keys, internal
|
|
4766
|
-
// member reads), so it keeps exactly the plain fields whose values
|
|
4767
|
-
// lower; accessor entries and members with no value representation
|
|
4768
|
-
// (rest-param function types) narrow away. SEMANTICS.md documents the
|
|
4769
|
-
// enumeration divergence.
|
|
4770
|
-
if (isJsSourceFile(expr.getSourceFile()) &&
|
|
4771
|
-
(!mapped || expr.properties.some((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))) &&
|
|
4772
|
-
isCjsExportTableLiteral(expr)) {
|
|
4773
|
-
const fields = [];
|
|
4774
|
-
for (const prop of expr.properties) {
|
|
4775
|
-
if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop))
|
|
4776
|
-
continue;
|
|
4777
|
-
const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name;
|
|
4778
|
-
if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName)))
|
|
4779
|
-
continue;
|
|
4780
|
-
const name = propName.text;
|
|
4781
|
-
let v = null;
|
|
4782
|
-
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
4783
|
-
v = probeLower(lowerer, propName);
|
|
4784
|
-
}
|
|
4785
|
-
else if (ts.isPropertyAssignment(prop)) {
|
|
4786
|
-
v = probeLower(lowerer, prop.initializer);
|
|
4787
|
-
}
|
|
4788
|
-
if (!v || v.type.kind === "void" || v.type.kind === "caught" || v.type.kind === "jsval")
|
|
4789
|
-
continue;
|
|
4790
|
-
if (isUnitType(v.type))
|
|
4791
|
-
continue;
|
|
4792
|
-
fields.push({ name, value: v });
|
|
4793
|
-
}
|
|
4794
|
-
// Canonical (sorted) field order — the shape registry's invariant;
|
|
4795
|
-
// the dropped reads are all pure, so reordering loses nothing.
|
|
4796
|
-
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
4797
|
-
const shapeId = lowerer.shapes.intern(fields.map((f) => ({ name: f.name, type: f.value.type })));
|
|
4798
|
-
return { kind: "recordLit", fields, type: { kind: "record", shapeId }, loc };
|
|
4799
|
-
}
|
|
4800
|
-
// The JS declaration fallback, literal-side (the checked-dynamic tree-array rule's
|
|
4801
|
-
// object form): a literal whose shape has no static home — an
|
|
4802
|
-
// unmappable contextual type over unmappable own fields (the
|
|
4803
|
-
// PropertyDescriptorMap argument of Object.defineProperties, nested
|
|
4804
|
-
// descriptor records with `any` values) — builds as a dyn OBJECT:
|
|
4805
|
-
// each field converts through the usual dyn boundary, and dynamic
|
|
4806
|
-
// consumers ride the keyed-dyn paths. TypeScript keeps the fence.
|
|
4807
|
-
if ((!mapped || mapped.kind === "dyn") && isJsSourceFile(expr.getSourceFile())) {
|
|
4808
|
-
return lowerDynObjectLiteral(lowerer, expr);
|
|
4809
|
-
}
|
|
4810
|
-
if (!mapped || mapped.kind !== "record")
|
|
4811
|
-
lowerer.badType(expr, tsType);
|
|
4812
|
-
let type = mapped;
|
|
4813
|
-
let shape = lowerer.shapes.get(type.shapeId);
|
|
4814
|
-
// ACCESSOR properties, JS literals only (TS accessors fill the shape's
|
|
4815
|
-
// %get:/%set: closure slots below): no record storage exists for them,
|
|
4816
|
-
// so the literal's shape NARROWS to its plain fields (reads resolve
|
|
4817
|
-
// through the lifted accessors; Object.keys over the value omits
|
|
4818
|
-
// accessor names — the documented divergence).
|
|
4819
|
-
if (isJsSourceFile(expr.getSourceFile())) {
|
|
4820
|
-
const accessorNames = new Set(expr.properties
|
|
4821
|
-
.filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
|
|
4822
|
-
.map((p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : ""))
|
|
4823
|
-
.filter((n) => n !== ""));
|
|
4824
|
-
if (accessorNames.size > 0) {
|
|
4825
|
-
const narrowed = shape.fields.filter((f) => !accessorNames.has(f.name));
|
|
4826
|
-
const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
|
|
4827
|
-
type = { kind: "record", shapeId: narrowedId };
|
|
4828
|
-
shape = lowerer.shapes.get(narrowedId);
|
|
4829
|
-
}
|
|
4830
|
-
}
|
|
4831
|
-
const fieldTypes = new Map(shape.fields.map((f) => [f.name, f.type]));
|
|
4832
|
-
// File-scope JavaScript object bindings live in the checked-dynamic tree
|
|
4833
|
-
// to preserve open writes and identity. Build an optional-field literal
|
|
4834
|
-
// from its actually written keys so Object.hasOwn can distinguish an
|
|
4835
|
-
// omitted property from a present property whose value is undefined.
|
|
4836
|
-
const topLevelJsDecl = ts.isVariableDeclaration(expr.parent) &&
|
|
4837
|
-
expr.parent.initializer === expr &&
|
|
4838
|
-
ts.isVariableStatement(expr.parent.parent.parent) &&
|
|
4839
|
-
ts.isSourceFile(expr.parent.parent.parent.parent);
|
|
4840
|
-
const assignedProperties = expr.properties.filter((prop) => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop));
|
|
4841
|
-
if (topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) &&
|
|
4842
|
-
assignedProperties.length === expr.properties.length) {
|
|
4843
|
-
const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name)));
|
|
4844
|
-
const omittedOptional = shape.fields.some((field) => !provided.has(field.name) && field.type.kind === "union" &&
|
|
4845
|
-
lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0);
|
|
4846
|
-
if (omittedOptional)
|
|
4847
|
-
return lowerDynObjectLiteral(lowerer, expr);
|
|
4848
|
-
}
|
|
4849
|
-
// The clone probe may lower the leading spread before declining (a
|
|
4850
|
-
// static `as` cast can make the checker-visible shape match while its
|
|
4851
|
-
// erased IR value keeps a wider shape). Reuse that exact value in the
|
|
4852
|
-
// ordinary spread path so lowering still happens once.
|
|
4853
|
-
let leadingSpreadLowered = null;
|
|
4854
|
-
// The overwhelmingly common immutable-update form over a record:
|
|
4855
|
-
// `{ ...model, changed, other: value }`. The historic lowering expanded
|
|
4856
|
-
// the spread into one recordGet per untouched field at EVERY site, then
|
|
4857
|
-
// the backends inlined all of those retains and stores into the caller.
|
|
4858
|
-
// A 178-field model updated hundreds of times consequently produced a
|
|
4859
|
-
// half-million-line LLVM function. Keep the same evaluation/ownership
|
|
4860
|
-
// semantics in one compact IR node: source first, explicit overrides in
|
|
4861
|
-
// source order, and one backend clone helper per shape.
|
|
4862
|
-
//
|
|
4863
|
-
// Stay deliberately narrow. Tuple/index/accessor shapes, conditional or
|
|
4864
|
-
// multiple spreads, spread-after-explicit order, and shape-changing width
|
|
4865
|
-
// copies retain their existing lowering and diagnostics.
|
|
4866
|
-
if (!isJsSourceFile(expr.getSourceFile()) &&
|
|
4867
|
-
!shape.indexValue &&
|
|
4868
|
-
!shape.tuple &&
|
|
4869
|
-
!shapeHasAccessorSlots(shape) &&
|
|
4870
|
-
expr.properties.length >= 2 &&
|
|
4871
|
-
ts.isSpreadAssignment(expr.properties[0]) &&
|
|
4872
|
-
!conditionalSpreadOf(expr.properties[0].expression) &&
|
|
4873
|
-
expr.properties.slice(1).every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
|
4874
|
-
p.name !== undefined &&
|
|
4875
|
-
(ts.isIdentifier(p.name) ||
|
|
4876
|
-
ts.isStringLiteral(p.name) ||
|
|
4877
|
-
ts.isNumericLiteral(p.name) ||
|
|
4878
|
-
(ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)))) {
|
|
4879
|
-
const spread = expr.properties[0];
|
|
4880
|
-
const props = expr.properties.slice(1);
|
|
4881
|
-
const names = props.map((p) => propNameText(lowerer, p.name));
|
|
4882
|
-
const unique = new Set(names);
|
|
4883
|
-
const sourceType = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
|
|
4884
|
-
if (sourceType?.kind === "record" &&
|
|
4885
|
-
sourceType.shapeId === type.shapeId &&
|
|
4886
|
-
unique.size === names.length &&
|
|
4887
|
-
names.every((name) => fieldTypes.has(name))) {
|
|
4888
|
-
const source = lowerer.lowerExpr(spread.expression);
|
|
4889
|
-
if (source.type.kind === "record" && source.type.shapeId === type.shapeId) {
|
|
4890
|
-
const overrides = [];
|
|
4891
|
-
for (let i = 0; i < props.length; i++) {
|
|
4892
|
-
const p = props[i];
|
|
4893
|
-
const name = names[i];
|
|
4894
|
-
const fieldType = fieldTypes.get(name);
|
|
4895
|
-
const valueNode = ts.isPropertyAssignment(p) ? p.initializer : p;
|
|
4896
|
-
let value = ts.isPropertyAssignment(p)
|
|
4897
|
-
? lowerer.lowerExpr(p.initializer)
|
|
4898
|
-
: lowerer.lowerShorthandValue(p);
|
|
4899
|
-
value = lowerer.coerceInto(valueNode, value, fieldType);
|
|
4900
|
-
if (!typeEquals(value.type, fieldType))
|
|
4901
|
-
lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
|
4902
|
-
overrides.push({ name, value });
|
|
4903
|
-
}
|
|
4904
|
-
return { kind: "recordClone", source, overrides, type, loc };
|
|
4905
|
-
}
|
|
4906
|
-
leadingSpreadLowered = source;
|
|
4907
|
-
}
|
|
4908
|
-
}
|
|
4909
|
-
// A PURE index-signature target with spreads — `{ ...process.env }`,
|
|
4910
|
-
// `{ ...process.env, ...extraEnv }`, `{ ...env, PATH: p }` (the
|
|
4911
|
-
// spawn-env pattern): the field-by-field desugar below cannot
|
|
4912
|
-
// enumerate runtime overflow keys, so the literal lowers as ONE
|
|
4913
|
-
// interned merge-helper call — contributors apply in literal order
|
|
4914
|
-
// with keyed writes (JS last-write-wins), sources and values evaluate
|
|
4915
|
-
// once each in source order (the call's argument order). A CONDITIONAL
|
|
4916
|
-
// spread `...(c ? { k: v } : {})` contributes its one key as a ternary
|
|
4917
|
-
// — cond once, v lazily, the empty arm holding the value slot's
|
|
4918
|
-
// undefined arm (the explicit-undefined-is-absent stance: JSON and
|
|
4919
|
-
// child-env builders drop it, exactly Node's absent key); targets
|
|
4920
|
-
// with declared fields keep the historic desugar below.
|
|
4921
|
-
const isRuntimeComputedKey = (p) => !ts.isSpreadAssignment(p) &&
|
|
4922
|
-
p.name !== undefined &&
|
|
4923
|
-
ts.isComputedPropertyName(p.name) &&
|
|
4924
|
-
literalComputedKey(lowerer, p.name) === null;
|
|
4925
|
-
if (shape.indexValue &&
|
|
4926
|
-
shape.fields.length === 0 &&
|
|
4927
|
-
!shape.tuple &&
|
|
4928
|
-
(expr.properties.some((p) => ts.isSpreadAssignment(p)) || expr.properties.some(isRuntimeComputedKey))) {
|
|
4929
|
-
const contributors = [];
|
|
4930
|
-
let mergeable = true;
|
|
4931
|
-
for (const prop of expr.properties) {
|
|
4932
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
4933
|
-
const cs = conditionalSpreadOf(prop.expression);
|
|
4934
|
-
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
|
4935
|
-
lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm)");
|
|
4936
|
-
}
|
|
4937
|
-
if (cs) {
|
|
4938
|
-
const absent = lowerer.wrappedUndefined(shape.indexValue, locOf(prop));
|
|
4939
|
-
if (!absent) {
|
|
4940
|
-
lowerer.unsupported("SC1090", prop, `conditional spreads into '${lowerer.fmt(shape.indexValue)}'-valued index-signature keys (the empty arm needs an undefined arm to hold — write the key in an if statement instead)`);
|
|
4941
|
-
}
|
|
4942
|
-
const csProp = cs.props[0];
|
|
4943
|
-
const cond = lowerer.lowerCondition(cs.cond);
|
|
4944
|
-
const vNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
|
|
4945
|
-
const v = lowerer.intoIndexValueSlot(ts.isPropertyAssignment(csProp) ? lowerer.lowerExpr(csProp.initializer) : lowerer.lowerShorthandValue(csProp), shape.indexValue, vNode);
|
|
4946
|
-
contributors.push({
|
|
4947
|
-
kind: "field",
|
|
4948
|
-
name: csProp.name.text,
|
|
4949
|
-
value: {
|
|
4950
|
-
kind: "ternary",
|
|
4951
|
-
cond,
|
|
4952
|
-
then: cs.whenTrue ? v : absent,
|
|
4953
|
-
else_: cs.whenTrue ? absent : v,
|
|
4954
|
-
type: shape.indexValue,
|
|
4955
|
-
loc: locOf(prop),
|
|
4956
|
-
},
|
|
4957
|
-
});
|
|
4958
|
-
continue;
|
|
4959
|
-
}
|
|
4960
|
-
let src = lowerer.lowerExpr(prop.expression);
|
|
4961
|
-
src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src;
|
|
4962
|
-
if (src.type.kind !== "record") {
|
|
4963
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(src.type)}' sources into an index-signature shape (only index-signature records merge — ${NARROW_FIRST})`);
|
|
4964
|
-
}
|
|
4965
|
-
fenceAccessorSpreadSource(lowerer, prop, lowerer.shapes.get(src.type.shapeId));
|
|
4966
|
-
contributors.push({ kind: "spread", shapeId: src.type.shapeId, value: src });
|
|
4967
|
-
continue;
|
|
4968
|
-
}
|
|
4969
|
-
if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
|
|
4970
|
-
prop.name &&
|
|
4971
|
-
(ts.isIdentifier(prop.name) ||
|
|
4972
|
-
ts.isStringLiteral(prop.name) ||
|
|
4973
|
-
ts.isNumericLiteral(prop.name) ||
|
|
4974
|
-
(ts.isComputedPropertyName(prop.name) && literalComputedKey(lowerer, prop.name) !== null))) {
|
|
4975
|
-
const value = ts.isPropertyAssignment(prop)
|
|
4976
|
-
? lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer)
|
|
4977
|
-
: lowerer.intoIndexValueSlot(lowerer.lowerShorthandValue(prop), shape.indexValue, prop);
|
|
4978
|
-
contributors.push({ kind: "field", name: propNameText(lowerer, prop.name), value });
|
|
4979
|
-
continue;
|
|
4980
|
-
}
|
|
4981
|
-
// A RUNTIME-keyed property (`{ ...m, ["a" + "b"]: "" }`, `{ [K]:
|
|
4982
|
-
// v }` where K is a runtime string): the key evaluates before its
|
|
4983
|
-
// value (JS's per-property order — both are helper arguments) and
|
|
4984
|
-
// writes through the signature exactly like a spread's keys.
|
|
4985
|
-
// Number/boolean/unknown keys stringify (ToPropertyKey).
|
|
4986
|
-
if (ts.isPropertyAssignment(prop) && ts.isComputedPropertyName(prop.name)) {
|
|
4987
|
-
let k = lowerer.lowerExpr(prop.name.expression);
|
|
4988
|
-
if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
|
|
4989
|
-
k = { kind: "toString", operand: k, type: STRING, loc: locOf(prop.name) };
|
|
4990
|
-
}
|
|
4991
|
-
if (k.type.kind !== "string") {
|
|
4992
|
-
lowerer.unsupported("SC1090", prop.name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
|
|
4993
|
-
}
|
|
4994
|
-
const value = lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer);
|
|
4995
|
-
contributors.push({ kind: "keyedField", key: k, value });
|
|
4996
|
-
continue;
|
|
4997
|
-
}
|
|
4998
|
-
mergeable = false;
|
|
4999
|
-
break;
|
|
5000
|
-
}
|
|
5001
|
-
if (mergeable) {
|
|
5002
|
-
const helper = lowerIndexMergeHelper(lowerer, type.shapeId, contributors, loc);
|
|
5003
|
-
if (helper === null) {
|
|
5004
|
-
lowerer.unsupported("SC1090", expr, `object spread into '${lowerer.fmt(type)}' from these source shapes (spread sources must be index-signature records whose value type is, or lifts into, the target's)`);
|
|
5005
|
-
}
|
|
5006
|
-
return {
|
|
5007
|
-
kind: "call",
|
|
5008
|
-
callee: helper,
|
|
5009
|
-
args: contributors.flatMap((c) => (c.kind === "keyedField" ? [c.key, c.value] : [c.value])),
|
|
5010
|
-
type,
|
|
5011
|
-
loc,
|
|
5012
|
-
};
|
|
5013
|
-
}
|
|
5014
|
-
}
|
|
5015
|
-
// Unfoldable computed keys outside the index-signature merge path:
|
|
5016
|
-
// no record shape can hold a runtime-decided name.
|
|
5017
|
-
for (const prop of expr.properties) {
|
|
5018
|
-
if (isRuntimeComputedKey(prop)) {
|
|
5019
|
-
lowerer.unsupported("SC1090", prop.name, "computed property keys (compile-time-known keys fold — a pure expression whose checker type is one string or number literal: consts, enum members, quoted keys, templates of those — `{ [MARKER]: v }`; runtime string keys write through an index-signature target; symbol keys stay out)");
|
|
5020
|
-
}
|
|
5021
|
-
}
|
|
5022
|
-
// A DECLARED-fields target built ENTIRELY from index-signature spreads —
|
|
5023
|
-
// `{ ...Object.fromEntries(...), ...Object.fromEntries(...) }` typed
|
|
5024
|
-
// AppConfig (the defaults-merge idiom over runtime-keyed sources): the
|
|
5025
|
-
// field-by-field desugar cannot enumerate runtime keys, so the literal
|
|
5026
|
-
// lowers as ONE interned merge-helper call — sources evaluate once each
|
|
5027
|
-
// in source order (the call's argument order — computed sources
|
|
5028
|
-
// included, no re-read), contributors apply in order with per-key
|
|
5029
|
-
// dispatch onto the declared fields (JS last-write-wins). Divergence 68
|
|
5030
|
-
// has the runtime rules (validated collisions, extra keys dropped).
|
|
5031
|
-
if (!shape.indexValue &&
|
|
5032
|
-
!shape.tuple &&
|
|
5033
|
-
shape.fields.length > 0 &&
|
|
5034
|
-
expr.properties.length > 0 &&
|
|
5035
|
-
expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
|
|
5036
|
-
expr.properties.some((p) => {
|
|
5037
|
-
const t = lowerer.mapTypeOf(lowerer.typeOf(p.expression));
|
|
5038
|
-
return t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue;
|
|
5039
|
-
})) {
|
|
5040
|
-
return lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc);
|
|
5041
|
-
}
|
|
5042
|
-
const shapeMismatch = (node) => {
|
|
5043
|
-
const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
5044
|
-
lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), own ? lowerer.fmt(own) : lowerer.checker.typeToString(lowerer.typeOf(expr)), locOf(node), own?.kind === "record" && type.kind === "record"
|
|
5045
|
-
? (lowerer.describeRecordWidthBlocker(own.shapeId, type.shapeId) ?? undefined)
|
|
5046
|
-
: undefined));
|
|
5047
|
-
throw new PoisonError();
|
|
5048
|
-
};
|
|
5049
|
-
const fields = [];
|
|
5050
|
-
// Field names introduced by conditional spreads: their ternary carries
|
|
5051
|
-
// the spread's whole evaluation (cond once, value lazily), so a LATER
|
|
5052
|
-
// contributor overriding one would silently drop that evaluation —
|
|
5053
|
-
// collisions fence in both directions.
|
|
5054
|
-
const conditionalNames = new Set();
|
|
5055
|
-
// Member names DROPPED from the shape (JS deferral — unlowerable pure
|
|
5056
|
-
// member reads narrow away; see the catch in the property loop).
|
|
5057
|
-
const droppedNames = new Set();
|
|
5058
|
-
for (const prop of expr.properties) {
|
|
5059
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
5060
|
-
const cs = conditionalSpreadOf(prop.expression);
|
|
5061
|
-
if (cs && cs !== "unsupported") {
|
|
5062
|
-
// `...(c ? { k: v } : {})` — ONE conditional field at the
|
|
5063
|
-
// spread's own position: cond evaluates exactly once, `v` only
|
|
5064
|
-
// when the non-empty arm is taken (ternary arms are lazy), and
|
|
5065
|
-
// the empty arm holds the interned undefined arm — exactly the
|
|
5066
|
-
// omitted-optional-field representation, which is also why the
|
|
5067
|
-
// target field must be optional (tsc types the idiom that way).
|
|
5068
|
-
const csProp = cs.props[0]; // single-prop-checked in the fence pass
|
|
5069
|
-
const name = csProp.name.text;
|
|
5070
|
-
const fieldType = fieldTypes.get(name);
|
|
5071
|
-
if (!fieldType) {
|
|
5072
|
-
if (shape.indexValue) {
|
|
5073
|
-
lowerer.unsupported("SC1090", prop, "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)");
|
|
5074
|
-
}
|
|
5075
|
-
throw shapeMismatch(prop);
|
|
5076
|
-
}
|
|
5077
|
-
const absent = lowerer.wrappedUndefined(fieldType, locOf(prop));
|
|
5078
|
-
if (!absent) {
|
|
5079
|
-
lowerer.unsupported("SC1090", prop, `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`);
|
|
5080
|
-
}
|
|
5081
|
-
if (fields.some((f) => f.name === name)) {
|
|
5082
|
-
lowerer.unsupported("SC1090", prop, `conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`);
|
|
5083
|
-
}
|
|
5084
|
-
const cond = lowerer.lowerCondition(cs.cond);
|
|
5085
|
-
const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
|
|
5086
|
-
let v = ts.isPropertyAssignment(csProp)
|
|
5087
|
-
? lowerer.lowerExpr(csProp.initializer)
|
|
5088
|
-
: lowerer.lowerShorthandValue(csProp);
|
|
5089
|
-
v = lowerer.coerceInto(valueNode, v, fieldType);
|
|
5090
|
-
if (!typeEquals(v.type, fieldType))
|
|
5091
|
-
lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
|
5092
|
-
conditionalNames.add(name);
|
|
5093
|
-
fields.push({
|
|
5094
|
-
name,
|
|
5095
|
-
value: {
|
|
5096
|
-
kind: "ternary",
|
|
5097
|
-
cond,
|
|
5098
|
-
then: cs.whenTrue ? v : absent,
|
|
5099
|
-
else_: cs.whenTrue ? absent : v,
|
|
5100
|
-
type: fieldType,
|
|
5101
|
-
loc: locOf(prop),
|
|
5102
|
-
},
|
|
5103
|
-
});
|
|
5104
|
-
continue;
|
|
5105
|
-
}
|
|
5106
|
-
// `{ ...base, ... }` — field-by-field copy of a known record
|
|
5107
|
-
// shape. Every source field must land on the target shape with an
|
|
5108
|
-
// equal type (a wider source would silently DROP fields JS keeps —
|
|
5109
|
-
// the width fence, same as literals). Later contributors override
|
|
5110
|
-
// earlier ones (JS last-write-wins; the reads are side-effect-free,
|
|
5111
|
-
// so dropping the earlier read is exact). Identifier sources
|
|
5112
|
-
// re-read per field (historic path); any OTHER source must be a
|
|
5113
|
-
// re-emittable pure read, sharing one lowered node per field.
|
|
5114
|
-
// The desugar's one-entry-per-name list reads spread fields
|
|
5115
|
-
// EAGERLY at the spread's position, so an explicit property
|
|
5116
|
-
// BEFORE a spread would need JS's overwrite — order-fenced (the
|
|
5117
|
-
// index-signature merge path above takes any order).
|
|
5118
|
-
if (expr.properties
|
|
5119
|
-
.slice(0, expr.properties.indexOf(prop))
|
|
5120
|
-
.some((p) => !ts.isSpreadAssignment(p))) {
|
|
5121
|
-
lowerer.unsupported("SC1090", prop, "object spread after explicit properties (spreads must come first — a later spread would overwrite them with JS semantics the desugar does not model)");
|
|
5122
|
-
}
|
|
5123
|
-
let srcNode = prop.expression;
|
|
5124
|
-
while (ts.isParenthesizedExpression(srcNode))
|
|
5125
|
-
srcNode = srcNode.expression;
|
|
5126
|
-
const srcLowered = prop === expr.properties[0] && leadingSpreadLowered !== null
|
|
5127
|
-
? leadingSpreadLowered
|
|
5128
|
-
: ts.isIdentifier(srcNode)
|
|
5129
|
-
? null
|
|
5130
|
-
: lowerer.lowerExpr(srcNode);
|
|
5131
|
-
const srcType = srcLowered ? srcLowered.type : lowerer.mapTypeOf(lowerer.typeOf(srcNode));
|
|
5132
|
-
// `...options.installConfig` — a spread of `Partial<X> | undefined`
|
|
5133
|
-
// (the optional-options merge idiom `{ ...DEFAULTS, ...overrides }`):
|
|
5134
|
-
// JS spreads nothing for the unit arm and copies present keys
|
|
5135
|
-
// otherwise. Per target field the desugar builds
|
|
5136
|
-
// `present ? extracted : earlier` — present tests the source's
|
|
5137
|
-
// record tag AND (optional source fields) the field's own value
|
|
5138
|
-
// arm; absent keeps the earlier contributor's value (both reads
|
|
5139
|
-
// are pure, so the reorder into the ternary is unobservable).
|
|
5140
|
-
if (srcType?.kind === "union") {
|
|
5141
|
-
const def = lowerer.unions.get(srcType.unionId);
|
|
5142
|
-
const recArms = def?.arms.filter((a) => a.kind === "record") ?? [];
|
|
5143
|
-
if (!def ||
|
|
5144
|
-
recArms.length !== 1 ||
|
|
5145
|
-
!def.arms.every((a) => a.kind === "record" || isUnitType(a))) {
|
|
5146
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources (only known record shapes spread — ${NARROW_FIRST})`);
|
|
5147
|
-
}
|
|
5148
|
-
if (srcLowered && !pureReemittable(srcLowered)) {
|
|
5149
|
-
lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
|
|
5150
|
-
}
|
|
5151
|
-
const recArm = recArms[0];
|
|
5152
|
-
const recTag = def.arms.indexOf(recArm);
|
|
5153
|
-
// A checked-dynamic VALUE under the union-mapped checker type
|
|
5154
|
-
// (a JS dyn-holding binding): the present/absent desugar tests
|
|
5155
|
-
// union tags a dyn box does not carry — fence honestly instead
|
|
5156
|
-
// of the validator's ICE.
|
|
5157
|
-
const probedSrc = srcLowered ?? probeLower(lowerer, srcNode);
|
|
5158
|
-
if (probedSrc?.type.kind === "dyn") {
|
|
5159
|
-
lowerer.unsupported("SC1090", prop, `object spread of a checked-dynamic '${lowerer.fmt(srcType)}' source (${NARROW_FIRST})`);
|
|
5160
|
-
}
|
|
5161
|
-
const srcShape = lowerer.shapes.get(recArm.shapeId);
|
|
5162
|
-
if (!srcShape)
|
|
5163
|
-
throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${recArm.shapeId}`);
|
|
5164
|
-
fenceAccessorSpreadSource(lowerer, prop, srcShape);
|
|
5165
|
-
if (srcShape.indexValue || shape.indexValue) {
|
|
5166
|
-
lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
|
|
5167
|
-
}
|
|
5168
|
-
const laterNames = new Set();
|
|
5169
|
-
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
|
5170
|
-
if (ts.isSpreadAssignment(later)) {
|
|
5171
|
-
if (conditionalSpreadOf(later.expression))
|
|
5172
|
-
continue;
|
|
5173
|
-
const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
|
|
5174
|
-
if (lt?.kind === "record") {
|
|
5175
|
-
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
|
|
5176
|
-
laterNames.add(lf.name);
|
|
5177
|
-
}
|
|
5178
|
-
continue;
|
|
5179
|
-
}
|
|
5180
|
-
if (later.name &&
|
|
5181
|
-
(ts.isIdentifier(later.name) ||
|
|
5182
|
-
ts.isStringLiteral(later.name) ||
|
|
5183
|
-
ts.isNumericLiteral(later.name) ||
|
|
5184
|
-
(ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
|
|
5185
|
-
laterNames.add(propNameText(lowerer, later.name));
|
|
5186
|
-
}
|
|
5187
|
-
}
|
|
5188
|
-
const srcRef = () => srcLowered ?? lowerer.lowerExpr(srcNode);
|
|
5189
|
-
for (const f of srcShape.fields) {
|
|
5190
|
-
if (laterNames.has(f.name))
|
|
5191
|
-
continue;
|
|
5192
|
-
const targetType = fieldTypes.get(f.name);
|
|
5193
|
-
// No slot on the target shape: the copy DROPS the field
|
|
5194
|
-
// (divergence 36's stance, same as the plain-record spread).
|
|
5195
|
-
if (!targetType)
|
|
5196
|
-
continue;
|
|
5197
|
-
if (conditionalNames.has(f.name)) {
|
|
5198
|
-
lowerer.unsupported("SC1090", prop, `spread of '${f.name}' over an earlier conditional spread (the desugar keeps one entry per name — restructure so each name has one contributor)`);
|
|
5199
|
-
}
|
|
5200
|
-
const fRead = () => ({
|
|
5201
|
-
kind: "recordGet",
|
|
5202
|
-
obj: { kind: "unionNarrow", unionId: srcType.unionId, tag: recTag, value: srcRef(), type: recArm, loc: locOf(prop) },
|
|
5203
|
-
shapeId: recArm.shapeId,
|
|
5204
|
-
field: f.name,
|
|
5205
|
-
type: f.type,
|
|
5206
|
-
loc: locOf(prop),
|
|
5207
|
-
});
|
|
5208
|
-
let cond = { kind: "unionIsTag", unionId: srcType.unionId, tag: recTag, negated: false, value: srcRef(), type: BOOL, loc: locOf(prop) };
|
|
5209
|
-
let thenVal;
|
|
5210
|
-
if (typeEquals(f.type, targetType)) {
|
|
5211
|
-
thenVal = fRead();
|
|
5212
|
-
}
|
|
5213
|
-
else if (f.type.kind === "union" &&
|
|
5214
|
-
lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
|
|
5215
|
-
typeEquals(lowerer.stripUndefinedArm(f.type), targetType)) {
|
|
5216
|
-
// Optional source field into a required target slot: present
|
|
5217
|
-
// means the source holds the record AND the field its value
|
|
5218
|
-
// arm — the spread-override completion, union-source form.
|
|
5219
|
-
const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
|
|
5220
|
-
cond = {
|
|
5221
|
-
kind: "logical",
|
|
5222
|
-
op: "&&",
|
|
5223
|
-
left: cond,
|
|
5224
|
-
right: { kind: "unionIsTag", unionId: f.type.unionId, tag: ftUndef, negated: true, value: fRead(), type: BOOL, loc: locOf(prop) },
|
|
5225
|
-
type: BOOL,
|
|
5226
|
-
loc: locOf(prop),
|
|
5227
|
-
};
|
|
5228
|
-
const ftDef = lowerer.unions.get(f.type.unionId);
|
|
5229
|
-
if (targetType.kind === "union") {
|
|
5230
|
-
const retag = lowerer.unionRetagHelper(f.type.unionId, targetType.unionId, locOf(prop));
|
|
5231
|
-
if (!retag) {
|
|
5232
|
-
lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(recArm), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot re-tag into '${lowerer.fmt(targetType)}' behind the present-test`));
|
|
5233
|
-
throw new PoisonError();
|
|
5234
|
-
}
|
|
5235
|
-
thenVal = { kind: "call", callee: retag, args: [fRead()], type: targetType, loc: locOf(prop) };
|
|
5236
|
-
}
|
|
5237
|
-
else if (ftDef && ftDef.arms.length === 2 && lowerer.armTag(f.type.unionId, targetType) >= 0) {
|
|
5238
|
-
thenVal = { kind: "unionNarrow", unionId: f.type.unionId, tag: lowerer.armTag(f.type.unionId, targetType), value: fRead(), type: targetType, loc: locOf(prop) };
|
|
5239
|
-
}
|
|
5240
|
-
else {
|
|
5241
|
-
lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(recArm), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot narrow into '${lowerer.fmt(targetType)}' behind the present-test`));
|
|
5242
|
-
throw new PoisonError();
|
|
5243
|
-
}
|
|
5244
|
-
}
|
|
5245
|
-
else {
|
|
5246
|
-
// The width-lift fallback (arm wrap, re-tag, nested
|
|
5247
|
-
// reshape) — the same per-field rule the slot coercion
|
|
5248
|
-
// applies. Runs AFTER the optional-completion branch: a
|
|
5249
|
-
// present-test has its own semantics a re-tag's stranded
|
|
5250
|
-
// undefined trap must not shadow.
|
|
5251
|
-
const lift = lowerer.widthLiftPlan(f.type, targetType);
|
|
5252
|
-
if (!lift) {
|
|
5253
|
-
lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(recArm), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' does not lift into '${lowerer.fmt(targetType)}'`));
|
|
5254
|
-
throw new PoisonError();
|
|
5255
|
-
}
|
|
5256
|
-
thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
|
|
5257
|
-
}
|
|
5258
|
-
const at = fields.findIndex((x) => x.name === f.name && !x.drop);
|
|
5259
|
-
const elseVal = at >= 0 ? fields[at].value : lowerer.wrappedUndefined(targetType, locOf(prop));
|
|
5260
|
-
if (!elseVal) {
|
|
5261
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources where '${f.name}' has no earlier contributor (the absent arm leaves the required field unset — spread defaults first: { ...defaults, ...overrides })`);
|
|
5262
|
-
}
|
|
5263
|
-
const merged = { kind: "ternary", cond, then: thenVal, else_: elseVal, type: targetType, loc: locOf(prop) };
|
|
5264
|
-
if (at >= 0)
|
|
5265
|
-
fields[at] = { name: f.name, value: merged };
|
|
5266
|
-
else
|
|
5267
|
-
fields.push({ name: f.name, value: merged });
|
|
5268
|
-
}
|
|
5269
|
-
continue;
|
|
5270
|
-
}
|
|
5271
|
-
if (srcType?.kind !== "record") {
|
|
5272
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${srcType ? lowerer.fmt(srcType) : lowerer.checker.typeToString(lowerer.typeOf(srcNode))}' sources (only known record shapes spread — ${NARROW_FIRST})`);
|
|
5273
|
-
}
|
|
5274
|
-
if (srcLowered && !pureReemittable(srcLowered)) {
|
|
5275
|
-
lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
|
|
5276
|
-
}
|
|
5277
|
-
const srcShape = lowerer.shapes.get(srcType.shapeId);
|
|
5278
|
-
if (!srcShape)
|
|
5279
|
-
throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${srcType.shapeId}`);
|
|
5280
|
-
fenceAccessorSpreadSource(lowerer, prop, srcShape);
|
|
5281
|
-
// Index-signature shapes carry runtime-keyed overflow entries the
|
|
5282
|
-
// field-by-field desugar cannot enumerate — fenced on either side.
|
|
5283
|
-
if (srcShape.indexValue || shape.indexValue) {
|
|
5284
|
-
lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
|
|
5285
|
-
}
|
|
5286
|
-
// Names a LATER contributor unconditionally defines: copying such a
|
|
5287
|
-
// source field is dead under JS last-write-wins (and spread reads
|
|
5288
|
-
// are side-effect-free), so it neither lowers nor width-checks —
|
|
5289
|
-
// the spread-then-override completion `{ ...config, stateDir }`
|
|
5290
|
-
// narrows an optional source field into a required target slot
|
|
5291
|
-
// exactly like Node does. Conditional spreads don't count (their
|
|
5292
|
-
// empty arm defines nothing) — their own collision fences hold.
|
|
5293
|
-
const laterNames = new Set();
|
|
5294
|
-
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
|
5295
|
-
if (ts.isSpreadAssignment(later)) {
|
|
5296
|
-
if (conditionalSpreadOf(later.expression))
|
|
5297
|
-
continue;
|
|
5298
|
-
const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
|
|
5299
|
-
if (lt?.kind === "record") {
|
|
5300
|
-
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
|
|
5301
|
-
laterNames.add(lf.name);
|
|
5302
|
-
}
|
|
5303
|
-
continue;
|
|
5304
|
-
}
|
|
5305
|
-
if (later.name &&
|
|
5306
|
-
(ts.isIdentifier(later.name) ||
|
|
5307
|
-
ts.isStringLiteral(later.name) ||
|
|
5308
|
-
ts.isNumericLiteral(later.name) ||
|
|
5309
|
-
(ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
|
|
5310
|
-
laterNames.add(propNameText(lowerer, later.name));
|
|
5311
|
-
}
|
|
5312
|
-
}
|
|
5313
|
-
for (const f of srcShape.fields) {
|
|
5314
|
-
if (laterNames.has(f.name))
|
|
5315
|
-
continue;
|
|
5316
|
-
const targetType = fieldTypes.get(f.name);
|
|
5317
|
-
// A source field with NO slot on the target shape: the copy
|
|
5318
|
-
// DROPS it — a spread of a wider record into a narrower literal
|
|
5319
|
-
// is width subtyping in spread clothing, divergence 36's stance
|
|
5320
|
-
// (Node's object would keep the key); the read is pure, so
|
|
5321
|
-
// skipping evaluates nothing.
|
|
5322
|
-
if (!targetType)
|
|
5323
|
-
continue;
|
|
5324
|
-
const lift = typeEquals(f.type, targetType)
|
|
5325
|
-
? null
|
|
5326
|
-
: lowerer.widthLiftPlan(f.type, targetType);
|
|
5327
|
-
if (!typeEquals(f.type, targetType) && !lift) {
|
|
5328
|
-
// Print the SOURCE shape, not the literal's own type: the
|
|
5329
|
-
// checker's own type already has later overrides applied, so
|
|
5330
|
-
// it can render identically to the target while the spread
|
|
5331
|
-
// source (the thing that actually mismatches) differs — an
|
|
5332
|
-
// invisible difference is a diagnostics bug.
|
|
5333
|
-
lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(srcType), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' does not lift into '${lowerer.fmt(targetType)}'`));
|
|
5334
|
-
throw new PoisonError();
|
|
5335
|
-
}
|
|
5336
|
-
const obj = srcLowered ?? lowerer.lowerExpr(srcNode);
|
|
5337
|
-
// A record-mapped CHECKER type whose VALUE lives in the checked-dynamic tree (a
|
|
5338
|
-
// JS file-scope object-literal global): read each field from
|
|
5339
|
-
// the checked-dynamic tree (dynKeyGet) and VALIDATE it into the source shape's
|
|
5340
|
-
// field type (dynCheck) — the checked-dynamic member-read
|
|
5341
|
-
// discipline. A missing key answers the dyn undefined, exactly
|
|
5342
|
-
// the undefined-armed optional's absent case; a mismatched
|
|
5343
|
-
// runtime value throws the catchable TypeError, never a silent
|
|
5344
|
-
// wrong copy. Runtime-ADDED keys drop — width subtyping in
|
|
5345
|
-
// spread clothing, divergence 36's stance.
|
|
5346
|
-
let value;
|
|
5347
|
-
if (obj.type.kind === "dyn") {
|
|
5348
|
-
if (!canDynCheckTo(f.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
|
|
5349
|
-
lowerer.unsupported("SC1100", prop, `object spread of a checked-dynamic source whose field '${f.name}' ('${lowerer.fmt(f.type)}') cannot validate out of the checked-dynamic tree (copy the fields explicitly)`);
|
|
5350
|
-
}
|
|
5351
|
-
value = {
|
|
5352
|
-
kind: "dynCheck",
|
|
5353
|
-
value: {
|
|
5354
|
-
kind: "dynKeyGet",
|
|
5355
|
-
key: { kind: "strLit", value: f.name, type: STRING, loc: locOf(prop) },
|
|
5356
|
-
value: obj,
|
|
5357
|
-
type: DYN,
|
|
5358
|
-
loc: locOf(prop),
|
|
5359
|
-
},
|
|
5360
|
-
type: f.type,
|
|
5361
|
-
loc: locOf(prop),
|
|
5362
|
-
};
|
|
5363
|
-
}
|
|
5364
|
-
else {
|
|
5365
|
-
value = {
|
|
5366
|
-
kind: "recordGet",
|
|
5367
|
-
obj,
|
|
5368
|
-
shapeId: srcType.shapeId,
|
|
5369
|
-
field: f.name,
|
|
5370
|
-
type: f.type,
|
|
5371
|
-
loc: locOf(prop),
|
|
5372
|
-
};
|
|
5373
|
-
}
|
|
5374
|
-
// A liftable field widens into the target slot (arm wrap,
|
|
5375
|
-
// re-tag, nested reshape) — the same per-field rule the slot
|
|
5376
|
-
// coercion applies.
|
|
5377
|
-
if (lift)
|
|
5378
|
-
value = lowerer.applyWidthLift(lift, value, targetType, locOf(prop));
|
|
5379
|
-
if (conditionalNames.has(f.name)) {
|
|
5380
|
-
lowerer.unsupported("SC1090", prop, `spread of '${f.name}' over an earlier conditional spread (the desugar keeps one entry per name — restructure so each name has one contributor)`);
|
|
5381
|
-
}
|
|
5382
|
-
const at = fields.findIndex((x) => x.name === f.name);
|
|
5383
|
-
if (at >= 0)
|
|
5384
|
-
fields[at] = { name: f.name, value };
|
|
5385
|
-
else
|
|
5386
|
-
fields.push({ name: f.name, value });
|
|
5387
|
-
}
|
|
5388
|
-
continue;
|
|
5389
|
-
}
|
|
5390
|
-
if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
|
|
5391
|
-
// JS accessor entries carry no storage (narrowed away above); TS
|
|
5392
|
-
// accessors fill the shape's reserved closure slots — the getter
|
|
5393
|
-
// body lowers as an ordinary zero-arg closure invoked per property
|
|
5394
|
-
// READ, the setter as a one-arg closure invoked per WRITE
|
|
5395
|
-
// (fieldGetExpr/fieldSetStmt dispatch on the slots). Creating the
|
|
5396
|
-
// closures is side-effect-free, so their position among the data
|
|
5397
|
-
// fields' source-order evaluation is unobservable — exactly JS,
|
|
5398
|
-
// where accessor definitions evaluate nothing.
|
|
5399
|
-
if (isJsSourceFile(expr.getSourceFile()))
|
|
5400
|
-
continue;
|
|
5401
|
-
const name = propNameText(lowerer, prop.name); // key-form-checked above
|
|
5402
|
-
const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`;
|
|
5403
|
-
const slotT = fieldTypes.get(slotName);
|
|
5404
|
-
if (!slotT || slotT.kind !== "func") {
|
|
5405
|
-
// The contextual shape stores a DATA value under this name
|
|
5406
|
-
// (`const p: { x: number } = { get x() {...} }` — tsc lets a
|
|
5407
|
-
// live accessor satisfy a data member): the slot would freeze
|
|
5408
|
-
// one getter answer where Node keeps the accessor live.
|
|
5409
|
-
if (fieldTypes.has(name)) {
|
|
5410
|
-
lowerer.unsupported("SC1090", prop, `a get/set accessor satisfying the data property '${name}' of '${lowerer.fmt(type)}' (the record slot stores a plain value — Node would keep the accessor live through this type)`);
|
|
5411
|
-
}
|
|
5412
|
-
throw shapeMismatch(prop);
|
|
5413
|
-
}
|
|
5414
|
-
const closure = lowerer.coerceInto(prop, lowerer.lowerLambda(prop), slotT);
|
|
5415
|
-
if (!typeEquals(closure.type, slotT))
|
|
5416
|
-
lowerer.badType(prop, lowerer.typeOf(prop));
|
|
5417
|
-
fields.push({ name: slotName, value: closure });
|
|
5418
|
-
continue;
|
|
5419
|
-
}
|
|
5420
|
-
const name = propNameText(lowerer, prop.name); // key-form-checked above
|
|
5421
|
-
let fieldType = fieldTypes.get(name);
|
|
5422
|
-
// An undeclared name against an index-signature shape is an OVERFLOW
|
|
5423
|
-
// entry (tsc typechecked it against the signature's value type);
|
|
5424
|
-
// against a plain shape it is the width mismatch it always was —
|
|
5425
|
-
// EXCEPT the PromiseSettledResult honest subset's dropped fields
|
|
5426
|
-
// (value/reason — SEMANTICS.md 46): those evaluate for effect in
|
|
5427
|
-
// their source-order slot and store nothing.
|
|
5428
|
-
// A GENERIC-callable member (a generic method `m<T>(x: T) {...}` or a
|
|
5429
|
-
// generic arrow/function-expression property): excluded from the
|
|
5430
|
-
// record shape (isGenericCallableMemberType — no single closure slot
|
|
5431
|
-
// can hold it), so the literal stores nothing for it. Pure
|
|
5432
|
-
// function-creating forms skip outright (creating a closure has no
|
|
5433
|
-
// side effects; calls resolve statically against this declaration);
|
|
5434
|
-
// a computed initializer would need its evaluation kept — fenced.
|
|
5435
|
-
if (!fieldType && !ts.isSpreadAssignment(prop)) {
|
|
5436
|
-
const memberSym = prop.name && lowerer.checker.getSymbolAtLocation(prop.name);
|
|
5437
|
-
const memberT = memberSym ? lowerer.checker.getTypeOfSymbol(memberSym) : undefined;
|
|
5438
|
-
if (memberT && isGenericCallableMemberType(memberT, lowerer.checker)) {
|
|
5439
|
-
const pureInit = (() => {
|
|
5440
|
-
if (ts.isMethodDeclaration(prop))
|
|
5441
|
-
return true;
|
|
5442
|
-
if (ts.isShorthandPropertyAssignment(prop))
|
|
5443
|
-
return true; // a pure read
|
|
5444
|
-
if (!ts.isPropertyAssignment(prop))
|
|
5445
|
-
return false;
|
|
5446
|
-
let init = prop.initializer;
|
|
5447
|
-
while (ts.isParenthesizedExpression(init))
|
|
5448
|
-
init = init.expression;
|
|
5449
|
-
return ts.isArrowFunction(init) || ts.isFunctionExpression(init) || ts.isIdentifier(init);
|
|
5450
|
-
})();
|
|
5451
|
-
if (!pureInit) {
|
|
5452
|
-
lowerer.unsupported("SC1090", prop, `generic-function-valued properties with computed initializers ('${name}' has no record slot — its evaluation would be dropped; bind the function to a top-level declaration instead)`);
|
|
5453
|
-
}
|
|
5454
|
-
continue;
|
|
5455
|
-
}
|
|
5456
|
-
}
|
|
5457
|
-
if (!fieldType && !shape.indexValue) {
|
|
5458
|
-
if (settledDropNames(lowerer, tsType)?.has(name)) {
|
|
5459
|
-
// Identifier and shorthand initializers are effect-free reads —
|
|
5460
|
-
// skipped outright. The caught `reason` MUST skip: lowering the
|
|
5461
|
-
// read would hit the catch-binding narrowness fence, and the
|
|
5462
|
-
// snapshot it names needs no evaluation.
|
|
5463
|
-
if (ts.isPropertyAssignment(prop) && !ts.isIdentifier(prop.initializer)) {
|
|
5464
|
-
const v = lowerer.lowerExpr(prop.initializer);
|
|
5465
|
-
// Effect-free lowerings (literals, plain reads) drop at
|
|
5466
|
-
// compile time; anything else — the awaited mapper — runs
|
|
5467
|
-
// (and may throw into the enclosing catch) with its result
|
|
5468
|
-
// released by the statement frame.
|
|
5469
|
-
if (v.kind !== "unitLit" && v.kind !== "numLit" && v.kind !== "strLit" &&
|
|
5470
|
-
v.kind !== "boolLit" && v.kind !== "varRef" && v.kind !== "closure") {
|
|
5471
|
-
fields.push({ name, value: v, drop: true });
|
|
5472
|
-
}
|
|
5473
|
-
}
|
|
5474
|
-
continue;
|
|
5475
|
-
}
|
|
5476
|
-
throw shapeMismatch(prop);
|
|
5477
|
-
}
|
|
5478
|
-
let value;
|
|
5479
|
-
let valueNode = prop;
|
|
5480
|
-
const propDiagsBefore = lowerer.diags.length;
|
|
5481
|
-
try {
|
|
5482
|
-
if (ts.isPropertyAssignment(prop)) {
|
|
5483
|
-
valueNode = prop.initializer;
|
|
5484
|
-
// ARRAY-LITERAL initializers route through the expected-type
|
|
5485
|
-
// lowering: a union field with one array-family arm builds the
|
|
5486
|
-
// literal AS that arm (lowerExprExpecting's IR-directed rule —
|
|
5487
|
-
// the option-table `default: [{ value: [] }]` shape), where the
|
|
5488
|
-
// bare lowering would take the JS dyn fallback and fence.
|
|
5489
|
-
let init = prop.initializer;
|
|
5490
|
-
while (ts.isParenthesizedExpression(init))
|
|
5491
|
-
init = init.expression;
|
|
5492
|
-
value =
|
|
5493
|
-
fenceClosureProbe(lowerer, prop.initializer, fieldType, () => lowerer.lowerExpr(prop.initializer)) ??
|
|
5494
|
-
(fieldType !== undefined && ts.isArrayLiteralExpression(init)
|
|
5495
|
-
? lowerer.lowerExprExpecting(prop.initializer, fieldType)
|
|
5496
|
-
: lowerer.lowerExpr(prop.initializer));
|
|
5497
|
-
}
|
|
5498
|
-
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
5499
|
-
value = lowerer.lowerShorthandValue(prop);
|
|
5500
|
-
}
|
|
5501
|
-
else if (ts.isMethodDeclaration(prop)) {
|
|
5502
|
-
value =
|
|
5503
|
-
fenceClosureProbe(lowerer, prop, fieldType, () => lowerer.lowerLambda(prop)) ?? lowerer.lowerLambda(prop);
|
|
5504
|
-
}
|
|
5505
|
-
else {
|
|
5506
|
-
lowerer.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
|
|
5507
|
-
}
|
|
5508
|
-
}
|
|
5509
|
-
catch (err) {
|
|
5510
|
-
// A member VALUE a JS file cannot lower (a namespace object in an
|
|
5511
|
-
// export aggregate — the sharedWithCli `errors` member): the
|
|
5512
|
-
// member NARROWS AWAY like a CJS export-table accessor entry —
|
|
5513
|
-
// the diagnostics defer to the runtime-fence ledger, the shape
|
|
5514
|
-
// drops the field, and each READ of it meets its own per-site
|
|
5515
|
-
// fence. Pure member forms only (identifier/shorthand reads);
|
|
5516
|
-
// TypeScript, probe mode, and ICEs keep the poison.
|
|
5517
|
-
const pureMember = ts.isShorthandPropertyAssignment(prop) ||
|
|
5518
|
-
(ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer));
|
|
5519
|
-
if (!(err instanceof PoisonError) ||
|
|
5520
|
-
!pureMember ||
|
|
5521
|
-
!isJsSourceFile(expr.getSourceFile()) ||
|
|
5522
|
-
lowerer.diagSink !== null ||
|
|
5523
|
-
lowerer.diags.length <= propDiagsBefore ||
|
|
5524
|
-
lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
|
|
5525
|
-
throw err;
|
|
5526
|
-
}
|
|
5527
|
-
lowerer.runtimeFences.push(...lowerer.diags.splice(propDiagsBefore));
|
|
5528
|
-
droppedNames.add(name);
|
|
5529
|
-
continue;
|
|
5530
|
-
}
|
|
5531
|
-
if (!fieldType) {
|
|
5532
|
-
// Overflow entry: the value flows into the index signature's value
|
|
5533
|
-
// slot — dyn slots take a dyn conversion (dynFrom), typed slots the
|
|
5534
|
-
// ordinary coercion path. Later duplicates override (map semantics
|
|
5535
|
-
// are last-write-wins already; a duplicate literal key is a tsc
|
|
5536
|
-
// error anyway).
|
|
5537
|
-
const slotted = lowerer.intoIndexValueSlot(value, shape.indexValue, valueNode);
|
|
5538
|
-
fields.push({ name, value: slotted, overflow: true });
|
|
5539
|
-
continue;
|
|
5540
|
-
}
|
|
5541
|
-
const promotedField = fieldType ? lowerer.runtimeOptionalWidening(value.type, fieldType) : null;
|
|
5542
|
-
if (promotedField && fieldType) {
|
|
5543
|
-
type = lowerer.runtimeOptionalRecordField(type, name, promotedField);
|
|
5544
|
-
if (type.kind === "record")
|
|
5545
|
-
shape = lowerer.shapes.get(type.shapeId);
|
|
5546
|
-
fieldTypes.set(name, promotedField);
|
|
5547
|
-
fieldType = promotedField;
|
|
5548
|
-
}
|
|
5549
|
-
value = lowerer.coerceInto(valueNode, value, fieldType); // union-typed fields wrap arm values
|
|
5550
|
-
if (!typeEquals(value.type, fieldType))
|
|
5551
|
-
lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
|
5552
|
-
// An explicit property overrides a spread-copied field: JS
|
|
5553
|
-
// last-write-wins. The entry moves to the END so explicit property
|
|
5554
|
-
// values keep their source-order evaluation among themselves. A
|
|
5555
|
-
// conditional-spread entry cannot be overridden (its ternary IS the
|
|
5556
|
-
// spread's evaluation; splicing it out would drop that).
|
|
5557
|
-
if (conditionalNames.has(name)) {
|
|
5558
|
-
lowerer.unsupported("SC1090", prop, `'${name}' after a conditional spread of the same name (the desugar keeps one entry per name — restructure so each name has one contributor)`);
|
|
5559
|
-
}
|
|
5560
|
-
const at = fields.findIndex((x) => x.name === name);
|
|
5561
|
-
if (at >= 0)
|
|
5562
|
-
fields.splice(at, 1);
|
|
5563
|
-
fields.push({ name, value });
|
|
5564
|
-
}
|
|
5565
|
-
// Optional fields (undefined-armed union slots) may be omitted: the
|
|
5566
|
-
// absent field holds the interned undefined arm, exactly like writing
|
|
5567
|
-
// `a: undefined` (without exactOptionalPropertyTypes tsc treats the two
|
|
5568
|
-
// the same, and only optional fields may be omitted — tsc rejects
|
|
5569
|
-
// omission of required fields before lowering, undefined-armed or not).
|
|
5570
|
-
// A REQUIRED missing field keeps the shape-mismatch rejection (possible
|
|
5571
|
-
// through `as`: the cast smuggles a narrower literal past freshness).
|
|
5572
|
-
if (droppedNames.size > 0) {
|
|
5573
|
-
const narrowed = shape.fields.filter((f) => !droppedNames.has(f.name));
|
|
5574
|
-
const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
|
|
5575
|
-
type = { kind: "record", shapeId: narrowedId };
|
|
5576
|
-
shape = lowerer.shapes.get(narrowedId);
|
|
5577
|
-
}
|
|
5578
|
-
if (fields.filter((f) => !f.overflow).length !== shape.fields.length) {
|
|
5579
|
-
const provided = new Set(fields.filter((f) => !f.overflow).map((f) => f.name));
|
|
5580
|
-
for (const f of shape.fields) {
|
|
5581
|
-
if (provided.has(f.name))
|
|
5582
|
-
continue;
|
|
5583
|
-
// 'unknown' fields complete with the dyn undefined — the absent
|
|
5584
|
-
// property reads as undefined in Node, and a dyn slot holds
|
|
5585
|
-
// exactly that (the options-record call shape against
|
|
5586
|
-
// `{ plugins: unknown, ... }` — a JS caller the checker admits).
|
|
5587
|
-
const absent = lowerer.wrappedUndefined(f.type, loc) ?? (f.type.kind === "dyn" ? dynUndefinedExpr(loc) : null);
|
|
5588
|
-
if (!absent)
|
|
5589
|
-
throw shapeMismatch(expr); // only optional (undefined-armed) and 'unknown' fields may be omitted
|
|
5590
|
-
fields.push({ name: f.name, value: absent });
|
|
5591
|
-
}
|
|
5592
|
-
}
|
|
5593
|
-
return { kind: "recordLit", fields, type, loc };
|
|
5594
|
-
}
|
|
5595
|
-
/** Parses the conditional-spread idiom `...(c ? { k: v, ... } : {})`
|
|
5596
|
-
* (either orientation). Returns the condition, the non-empty arm's
|
|
5597
|
-
* properties, and which arm carries them; "unsupported" for conditional
|
|
5598
|
-
* sources OUTSIDE the idiom (both arms non-empty, computed/method
|
|
5599
|
-
* members); null when the spread source isn't a conditional at all.
|
|
5600
|
-
* Callers slice their own honest subset (the static record path takes
|
|
5601
|
-
* exactly one property; the island literal takes any number; spawn's
|
|
5602
|
-
* options walk takes a `detached` boolean literal). */
|
|
5603
|
-
export function conditionalSpreadOf(expr) {
|
|
5604
|
-
let e = expr;
|
|
5605
|
-
while (ts.isParenthesizedExpression(e))
|
|
5606
|
-
e = e.expression;
|
|
5607
|
-
if (!ts.isConditionalExpression(e))
|
|
5608
|
-
return null;
|
|
5609
|
-
const unwrap = (a) => {
|
|
5610
|
-
let x = a;
|
|
5611
|
-
while (ts.isParenthesizedExpression(x))
|
|
5612
|
-
x = x.expression;
|
|
5613
|
-
return x;
|
|
5614
|
-
};
|
|
5615
|
-
const whenTrue = unwrap(e.whenTrue);
|
|
5616
|
-
const whenFalse = unwrap(e.whenFalse);
|
|
5617
|
-
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
5618
|
-
return "unsupported";
|
|
5619
|
-
}
|
|
5620
|
-
const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0;
|
|
5621
|
-
const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0;
|
|
5622
|
-
if (!trueCarries && !falseCarries)
|
|
5623
|
-
return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar
|
|
5624
|
-
const carrier = trueCarries ? whenTrue : whenFalse;
|
|
5625
|
-
const props = [];
|
|
5626
|
-
for (const p of carrier.properties) {
|
|
5627
|
-
if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
|
5628
|
-
p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
|
|
5629
|
-
props.push(p);
|
|
5630
|
-
}
|
|
5631
|
-
else {
|
|
5632
|
-
return "unsupported";
|
|
5633
|
-
}
|
|
5634
|
-
}
|
|
5635
|
-
return { cond: e.condition, props, whenTrue: trueCarries };
|
|
5636
|
-
}
|
|
5637
|
-
/** `{ ...idx, ...idx2 }` typed a DECLARED shape (no index signature) — the
|
|
5638
|
-
* defaults-merge idiom over runtime-keyed sources (`{ ...fromEntries(a),
|
|
5639
|
-
* ...fromEntries(b) }` typed AppConfig). Lowers to ONE interned helper
|
|
5640
|
-
* call: sources evaluate once each as arguments (source order — computed
|
|
5641
|
-
* sources included, JS's evaluate-once), the result starts all-undefined
|
|
5642
|
-
* (every target field must be optional — the runtime keys decide
|
|
5643
|
-
* presence), and each source's keys apply in JS own-key order with a
|
|
5644
|
-
* per-key dispatch onto the declared fields: a matching key writes the
|
|
5645
|
-
* field — identity when the source's value slot IS the field type, a
|
|
5646
|
-
* validated extraction otherwise (a dyn slot dynChecks; a union slot
|
|
5647
|
-
* re-tags arm-by-arm, and a value outside the field's arms throws the
|
|
5648
|
-
* catchable TypeError — divergence 34's keyed-write stance, where Node's
|
|
5649
|
-
* untyped copy would store the lie) — and a key naming NO declared field
|
|
5650
|
-
* is DROPPED (the shape cannot represent it; Node keeps it invisibly —
|
|
5651
|
-
* divergence 68). Later contributors overwrite earlier ones
|
|
5652
|
-
* (last-write-wins). Sources must be PURE index-signature records. */
|
|
5653
|
-
function lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc) {
|
|
5654
|
-
const srcs = [];
|
|
5655
|
-
for (const prop of expr.properties) {
|
|
5656
|
-
const spread = prop; // caller-checked: all spreads
|
|
5657
|
-
const value = lowerer.lowerExpr(spread.expression);
|
|
5658
|
-
const srcShape = value.type.kind === "record" ? lowerer.shapes.get(value.type.shapeId) : undefined;
|
|
5659
|
-
if (value.type.kind !== "record" || !srcShape?.indexValue || srcShape.tuple || srcShape.fields.length > 0) {
|
|
5660
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(value.type)}' into '${lowerer.fmt(type)}' (only PURE index-signature records — Object.fromEntries results, Record<string, T> values — spread into a declared shape)`);
|
|
5661
|
-
}
|
|
5662
|
-
srcs.push({ value, shapeId: value.type.shapeId, iv: srcShape.indexValue });
|
|
5663
|
-
}
|
|
5664
|
-
// Every target field must be optional: absent keys leave the undefined
|
|
5665
|
-
// arm, exactly the unset-optional representation.
|
|
5666
|
-
for (const f of shape.fields) {
|
|
5667
|
-
if (f.type.kind !== "union" || lowerer.armTag(f.type.unionId, UNDEFINED_T) < 0) {
|
|
5668
|
-
lowerer.unsupported("SC1090", expr, `object spread of runtime-keyed sources onto the required field '${f.name}' (the keys decide presence at runtime — declare the field optional or spell it explicitly)`);
|
|
5669
|
-
}
|
|
5670
|
-
}
|
|
5671
|
-
// Per (source value slot → field) conversion, checked up front so the
|
|
5672
|
-
// fence fires at the literal, not inside the interned helper.
|
|
5673
|
-
const conv = (iv, f, v) => {
|
|
5674
|
-
if (typeEquals(iv, f.type))
|
|
5675
|
-
return { value: v };
|
|
5676
|
-
if (iv.kind === "dyn") {
|
|
5677
|
-
return { value: { kind: "dynCheck", value: v, type: f.type, loc } };
|
|
5678
|
-
}
|
|
5679
|
-
if (iv.kind === "union" && f.type.kind === "union") {
|
|
5680
|
-
const fUnion = f.type;
|
|
5681
|
-
const def = lowerer.unions.get(iv.unionId);
|
|
5682
|
-
const fDef = lowerer.unions.get(fUnion.unionId);
|
|
5683
|
-
if (def && fDef && def.arms.some((a) => lowerer.armTag(fUnion.unionId, a) >= 0)) {
|
|
5684
|
-
// Arm-by-arm validated re-tag, inline in the helper: matching
|
|
5685
|
-
// arms map by identity (unit arms re-wrap, value arms narrow and
|
|
5686
|
-
// wrap); anything else throws the catchable TypeError.
|
|
5687
|
-
return {
|
|
5688
|
-
stmts: (write) => {
|
|
5689
|
-
const chain = (i) => {
|
|
5690
|
-
if (i >= def.arms.length) {
|
|
5691
|
-
return [{
|
|
5692
|
-
kind: "throw",
|
|
5693
|
-
value: {
|
|
5694
|
-
kind: "libCall",
|
|
5695
|
-
fn: "error.new",
|
|
5696
|
-
args: [{ kind: "strLit", value: `expected ${lowerer.fmt(fUnion)} at $.${f.name}`, type: STRING, loc }],
|
|
5697
|
-
type: { kind: "object", className: "%TypeError" },
|
|
5698
|
-
loc,
|
|
5699
|
-
},
|
|
5700
|
-
loc,
|
|
5701
|
-
}];
|
|
5702
|
-
}
|
|
5703
|
-
const arm = def.arms[i];
|
|
5704
|
-
const toTag = lowerer.armTag(fUnion.unionId, arm);
|
|
5705
|
-
if (toTag < 0)
|
|
5706
|
-
return chain(i + 1);
|
|
5707
|
-
const extracted = isUnitType(arm)
|
|
5708
|
-
? { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }, type: fUnion, loc }
|
|
5709
|
-
: { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unionNarrow", unionId: iv.unionId, tag: i, value: v, type: arm, loc }, type: fUnion, loc };
|
|
5710
|
-
return [{
|
|
5711
|
-
kind: "if",
|
|
5712
|
-
cond: { kind: "unionIsTag", unionId: iv.unionId, tag: i, negated: false, value: v, type: BOOL, loc },
|
|
5713
|
-
then: [write(extracted)],
|
|
5714
|
-
else_: chain(i + 1),
|
|
5715
|
-
loc,
|
|
5716
|
-
}];
|
|
5717
|
-
};
|
|
5718
|
-
return chain(0);
|
|
5719
|
-
},
|
|
5720
|
-
};
|
|
5721
|
-
}
|
|
5722
|
-
}
|
|
5723
|
-
lowerer.unsupported("SC1090", expr, `object spread into '${lowerer.fmt(type)}' where the source's '${lowerer.fmt(iv)}' values cannot reach the '${lowerer.fmt(f.type)}' field '${f.name}' (the value slot must be the field type, 'unknown', or a union covering the field's arms)`);
|
|
5724
|
-
};
|
|
5725
|
-
const key = `declmerge:${type.shapeId}:${srcs.map((s) => s.shapeId).join(",")}`;
|
|
5726
|
-
let helper = lowerer.widthHelpers.get(key);
|
|
5727
|
-
if (!helper) {
|
|
5728
|
-
helper = `%rec.declmerge.${lowerer.widthHelpers.size}`;
|
|
5729
|
-
const outRef = varRef("out.0", type, loc);
|
|
5730
|
-
const ksT = arrayOf(STRING);
|
|
5731
|
-
const locals = [{ id: "out.0", name: "out", type, mutable: false }];
|
|
5732
|
-
const params = srcs.map((s, j) => {
|
|
5733
|
-
locals.push({ id: `s${j}.0`, name: `s${j}`, type: s.value.type, mutable: true });
|
|
5734
|
-
return { localId: `s${j}.0`, name: `s${j}`, type: s.value.type };
|
|
5735
|
-
});
|
|
5736
|
-
const body = [
|
|
5737
|
-
{
|
|
5738
|
-
kind: "varDecl",
|
|
5739
|
-
localId: "out.0",
|
|
5740
|
-
init: {
|
|
5741
|
-
kind: "recordLit",
|
|
5742
|
-
fields: shape.fields.map((f) => ({ name: f.name, value: lowerer.wrappedUndefined(f.type, loc) })),
|
|
5743
|
-
type,
|
|
5744
|
-
loc,
|
|
5745
|
-
},
|
|
5746
|
-
loc,
|
|
5747
|
-
},
|
|
5748
|
-
];
|
|
5749
|
-
srcs.forEach((s, j) => {
|
|
5750
|
-
const sRef = varRef(`s${j}.0`, s.value.type, loc);
|
|
5751
|
-
const kRef = varRef(`k${j}.0`, STRING, loc);
|
|
5752
|
-
const vRef = varRef(`v${j}.0`, s.iv, loc);
|
|
5753
|
-
locals.push({ id: `ks${j}.0`, name: `ks${j}`, type: ksT, mutable: false }, { id: `i${j}.0`, name: `i${j}`, type: F64, mutable: true }, { id: `k${j}.0`, name: `k${j}`, type: STRING, mutable: false }, { id: `v${j}.0`, name: `v${j}`, type: s.iv, mutable: false });
|
|
5754
|
-
// Per-key dispatch: if (k === "a") { write a } else if ... else drop.
|
|
5755
|
-
const dispatch = shape.fields.reduceRight((rest, f) => {
|
|
5756
|
-
const write = (value) => ({ kind: "recordSet", obj: outRef, shapeId: type.shapeId, field: f.name, value, loc });
|
|
5757
|
-
const c = conv(s.iv, f, vRef);
|
|
5758
|
-
const thenBody = "value" in c ? [write(c.value)] : c.stmts(write);
|
|
5759
|
-
return [{
|
|
5760
|
-
kind: "if",
|
|
5761
|
-
cond: { kind: "strEq", negated: false, left: kRef, right: { kind: "strLit", value: f.name, type: STRING, loc }, type: BOOL, loc },
|
|
5762
|
-
then: thenBody,
|
|
5763
|
-
else_: rest.length > 0 ? rest : null,
|
|
5764
|
-
loc,
|
|
5765
|
-
}];
|
|
5766
|
-
}, []);
|
|
5767
|
-
body.push({ kind: "varDecl", localId: `ks${j}.0`, init: { kind: "recordOvfKeys", obj: sRef, shapeId: s.shapeId, type: ksT, loc }, loc }, {
|
|
5768
|
-
kind: "for",
|
|
5769
|
-
init: { kind: "varDecl", localId: `i${j}.0`, init: numLit(0, loc), loc },
|
|
5770
|
-
cond: { kind: "bin", op: "<", left: varRef(`i${j}.0`, F64, loc), right: { kind: "arrIntrinsic", method: "length", receiver: varRef(`ks${j}.0`, ksT, loc), args: [], type: F64, loc }, type: BOOL, loc },
|
|
5771
|
-
update: { kind: "assign", localId: `i${j}.0`, value: { kind: "bin", op: "+", left: varRef(`i${j}.0`, F64, loc), right: numLit(1, loc), type: F64, loc }, loc },
|
|
5772
|
-
body: [
|
|
5773
|
-
{ kind: "varDecl", localId: `k${j}.0`, init: { kind: "arrayGet", arr: varRef(`ks${j}.0`, ksT, loc), index: varRef(`i${j}.0`, F64, loc), type: STRING, loc }, loc },
|
|
5774
|
-
{ kind: "varDecl", localId: `v${j}.0`, init: { kind: "recordKeyGet", obj: sRef, shapeId: s.shapeId, key: kRef, overflowOnly: true, type: s.iv, loc }, loc },
|
|
5775
|
-
...dispatch,
|
|
5776
|
-
],
|
|
5777
|
-
loc,
|
|
5778
|
-
});
|
|
5779
|
-
});
|
|
5780
|
-
body.push({ kind: "return", value: outRef, loc });
|
|
5781
|
-
lowerer.liftedFns.push({
|
|
5782
|
-
name: helper,
|
|
5783
|
-
params,
|
|
5784
|
-
returnType: type,
|
|
5785
|
-
locals,
|
|
5786
|
-
body,
|
|
5787
|
-
loc,
|
|
5788
|
-
});
|
|
5789
|
-
// Registered only after a fence-free build: a conv() fence mid-build
|
|
5790
|
-
// must not leave a phantom helper behind for the next literal.
|
|
5791
|
-
lowerer.widthHelpers.set(key, helper);
|
|
5792
|
-
}
|
|
5793
|
-
return { kind: "call", callee: helper, args: srcs.map((s) => s.value), type, loc };
|
|
5794
|
-
}
|
|
5795
|
-
/** The value of a shorthand property (`{ x }`): the binding `x` refers to.
|
|
5796
|
-
* The property name's own symbol is the PROPERTY, so resolution goes
|
|
5797
|
-
* through getShorthandAssignmentValueSymbol. */
|
|
5798
|
-
export function lowerShorthandValue(lowerer, prop) {
|
|
5799
|
-
// 7 types the shorthand's name as PropertyName; it is always an
|
|
5800
|
-
// Identifier (the grammar allows nothing else in shorthand position).
|
|
5801
|
-
const propName = prop.name;
|
|
5802
|
-
const loc = locOf(propName);
|
|
5803
|
-
const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
|
|
5804
|
-
if (symbol) {
|
|
5805
|
-
if (lowerer.ctx.selfSymbol === symbol) {
|
|
5806
|
-
return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
|
|
5807
|
-
}
|
|
5808
|
-
const local = lowerer.resolveKey(symbol, propName);
|
|
5809
|
-
if (local) {
|
|
5810
|
-
return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, propName);
|
|
5811
|
-
}
|
|
5812
|
-
const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
|
5813
|
-
lowerer.flushDeferred(resolved);
|
|
5814
|
-
const g = lowerer.globalsBySymbol.get(resolved);
|
|
5815
|
-
if (g) {
|
|
5816
|
-
return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName);
|
|
5817
|
-
}
|
|
5818
|
-
const sig = lowerer.fnSigsBySymbol.get(resolved);
|
|
5819
|
-
const decl = lowerer.checker.declarationsOf(resolved)[0];
|
|
5820
|
-
if (sig && decl && ts.isFunctionDeclaration(decl) &&
|
|
5821
|
-
(ts.isSourceFile(decl.parent) || lowerer.nsBlocks.get(decl.parent) === "flattened")) {
|
|
5822
|
-
lowerer.noteEdge(sig.name);
|
|
5823
|
-
const funcType = {
|
|
5824
|
-
kind: "func",
|
|
5825
|
-
params: sig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type),
|
|
5826
|
-
ret: sig.returnType,
|
|
5827
|
-
...(sig.params.some((p) => p.mode === "dynRest") ? { rest: true } : {}),
|
|
5828
|
-
};
|
|
5829
|
-
lowerer.requireExactArityValue(prop, propName, sig.params, funcType);
|
|
5830
|
-
return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
|
|
5831
|
-
}
|
|
5832
|
-
if (lowerer.genericFnsBySymbol.has(resolved)) {
|
|
5833
|
-
lowerer.unsupported("SC1090", prop, `generic functions as values (call '${propName.text}' directly)`);
|
|
5834
|
-
}
|
|
5835
|
-
}
|
|
5836
|
-
lowerer.rejectUnresolvedSymbol(symbol ? (symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol) : null, propName.text, prop, `the reference to '${propName.text}' (a binding form with no lowering)`);
|
|
5837
|
-
}
|
|
5838
|
-
/** Rejects any `this` inside an object-literal method body — including in
|
|
5839
|
-
* nested arrows, which inherit the method's `this` (nested function
|
|
5840
|
-
* expressions reset it, but their bare `this` is already a tsc error
|
|
5841
|
-
* under noImplicitThis, so over-rejecting them here changes nothing). */
|
|
5842
|
-
export function rejectThisInObjectMethod(lowerer, node) {
|
|
5843
|
-
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
|
5844
|
-
lowerer.unsupported("SC1090", node, "references to 'this' in object literal methods");
|
|
5845
|
-
}
|
|
5846
|
-
ts.forEachChild(node, (child) => lowerer.rejectThisInObjectMethod(child));
|
|
5847
|
-
}
|
|
5848
|
-
/** The accessor twin of rejectThisInObjectMethod: a get/set accessor body
|
|
5849
|
-
* referencing `this` (`get x() { return this._x }`). The accessor lowers
|
|
5850
|
-
* as a closure stored IN the record — passing the record as a receiver
|
|
5851
|
-
* would capture the value under construction (an RC cycle), and the
|
|
5852
|
-
* generic lexical-this walk would silently bind an ENCLOSING method's
|
|
5853
|
-
* `this`; the fence names the fix (capture a binding instead). */
|
|
5854
|
-
function rejectThisInObjectAccessor(lowerer, node) {
|
|
5855
|
-
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
|
5856
|
-
lowerer.unsupported("SC1090", node, "references to 'this' in object literal get/set accessors (the accessor lowers as a captured closure with no receiver — read a captured binding instead)");
|
|
5857
|
-
}
|
|
5858
|
-
ts.forEachChild(node, (child) => rejectThisInObjectAccessor(lowerer, child));
|
|
5859
|
-
}
|
|
5860
3788
|
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
|
|
5861
3789
|
* string-key element access (`a["length"]`) and string indexing (`s[0]`
|
|
5862
3790
|
* typechecks against the lib's index signature; use .charAt) stay out. */
|
|
@@ -5868,7 +3796,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5868
3796
|
}
|
|
5869
3797
|
// `a?.b[i]` — the tail of a chain whose token sits deeper: the whole
|
|
5870
3798
|
// tail short-circuits with the guard.
|
|
5871
|
-
if (!expr.questionDotToken &&
|
|
3799
|
+
if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
|
|
5872
3800
|
return lowerer.lowerOptionalChain(expr);
|
|
5873
3801
|
}
|
|
5874
3802
|
// Enum accesses in element clothing — `E["A"]` forward reads and
|
|
@@ -5879,6 +3807,9 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5879
3807
|
if (en)
|
|
5880
3808
|
return en;
|
|
5881
3809
|
}
|
|
3810
|
+
const cachedModule = lowerRequireCacheElement(lowerer, expr);
|
|
3811
|
+
if (cachedModule)
|
|
3812
|
+
return cachedModule;
|
|
5882
3813
|
// Native Web handles use the same supported surface for bracket and dot
|
|
5883
3814
|
// spellings. Fence a statically-known unsupported key before the generic
|
|
5884
3815
|
// checked-dynamic element-read path can turn it into a runtime missing
|
|
@@ -5975,7 +3906,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5975
3906
|
? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
|
|
5976
3907
|
: null;
|
|
5977
3908
|
if (key) {
|
|
5978
|
-
const opt =
|
|
3909
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
5979
3910
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc: locOf(expr) }, expr);
|
|
5980
3911
|
}
|
|
5981
3912
|
}
|
|
@@ -6089,7 +4020,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
6089
4020
|
? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
|
|
6090
4021
|
: null;
|
|
6091
4022
|
if (key) {
|
|
6092
|
-
const opt =
|
|
4023
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
6093
4024
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: obj, type: DYN, loc: locOf(expr) }, expr);
|
|
6094
4025
|
}
|
|
6095
4026
|
}
|
|
@@ -6111,6 +4042,17 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
6111
4042
|
const value = lowerer.lowerExpr(expr.expression);
|
|
6112
4043
|
if (value.type.kind === "union") {
|
|
6113
4044
|
const key = lowerer.lowerExpr(expr.argumentExpression);
|
|
4045
|
+
if (key.type.kind === "f64") {
|
|
4046
|
+
const def = lowerer.unions.get(value.type.unionId);
|
|
4047
|
+
const presentArms = def?.arms.filter((arm) => !isUnitType(arm)) ?? [];
|
|
4048
|
+
if (presentArms.length === 1 && presentArms[0]?.kind === "string") {
|
|
4049
|
+
const property = runtimeOptionalElementKey(expr.argumentExpression);
|
|
4050
|
+
const receiver = property === null ? null : lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, STRING, property);
|
|
4051
|
+
if (receiver !== null) {
|
|
4052
|
+
return { kind: "strIntrinsic", method: "charAt", receiver, args: [key], type: STRING, loc: locOf(expr) };
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
6114
4056
|
if (key.type.kind === "string") {
|
|
6115
4057
|
const lit = ts.isStringLiteral(expr.argumentExpression)
|
|
6116
4058
|
? expr.argumentExpression.text
|
|
@@ -6392,6 +4334,19 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
|
|
|
6392
4334
|
if (key.type.kind !== "string") {
|
|
6393
4335
|
lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
|
|
6394
4336
|
}
|
|
4337
|
+
if (shape.fields.length === 0 && shape.indexValue === undefined) {
|
|
4338
|
+
const result = dynUndefinedExpr(loc);
|
|
4339
|
+
return {
|
|
4340
|
+
kind: "seqExpr",
|
|
4341
|
+
stmts: [
|
|
4342
|
+
{ kind: "exprStmt", expr: obj, loc },
|
|
4343
|
+
{ kind: "exprStmt", expr: key, loc },
|
|
4344
|
+
],
|
|
4345
|
+
result,
|
|
4346
|
+
type: DYN,
|
|
4347
|
+
loc,
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
6395
4350
|
// The DECLARED result type of the access — the index signature's value
|
|
6396
4351
|
// type (armed with undefined under noUncheckedIndexedAccess), or the
|
|
6397
4352
|
// declared fields' one common type on signature-free shapes (tsc's
|
|
@@ -6483,6 +4438,9 @@ export function lowerAbsenceProbe(lowerer, node) {
|
|
|
6483
4438
|
}
|
|
6484
4439
|
if (!ts.isElementAccessExpression(expr))
|
|
6485
4440
|
return null;
|
|
4441
|
+
const cachedModule = lowerRequireCacheElement(lowerer, expr);
|
|
4442
|
+
if (cachedModule)
|
|
4443
|
+
return cachedModule;
|
|
6486
4444
|
const header = lowerer.lowerHttpHeadersElement(expr);
|
|
6487
4445
|
if (header)
|
|
6488
4446
|
return header;
|
|
@@ -6580,6 +4538,7 @@ function tupleLiteralIndex(node) {
|
|
|
6580
4538
|
* assignment, produce no value in our subset). */
|
|
6581
4539
|
export function lowerElementWrite(lowerer, expr) {
|
|
6582
4540
|
const target = expr.left;
|
|
4541
|
+
fenceNodeModuleMutation(lowerer, target, "assignment");
|
|
6583
4542
|
// `process.env[key] = v` — the computed twin of the dotted env write:
|
|
6584
4543
|
// setenv(3), string keys and string values only.
|
|
6585
4544
|
if (lowerer.isProcessEnv(target.expression) && !target.questionDotToken) {
|
|
@@ -6846,7 +4805,7 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6846
4805
|
// probes the receiver's own lowered world: a dyn value takes the same
|
|
6847
4806
|
// write, anything else falls through to the fences.
|
|
6848
4807
|
if (receiverIr?.kind === "dyn" || receiverIr === null) {
|
|
6849
|
-
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) :
|
|
4808
|
+
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : tryLowerExpression(lowerer, target.expression);
|
|
6850
4809
|
if (obj !== null && obj.type.kind === "dyn") {
|
|
6851
4810
|
const loc = locOf(expr);
|
|
6852
4811
|
const litKey = recordKeyLiteralText(target.argumentExpression);
|
|
@@ -7164,6 +5123,12 @@ export function lowerAsExpression(lowerer, expr) {
|
|
|
7164
5123
|
if (target.kind === "bytes" && target.elem === "u8") {
|
|
7165
5124
|
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
|
|
7166
5125
|
}
|
|
5126
|
+
// Class instances round-trip only through compiler-owned typed
|
|
5127
|
+
// references. Plain JSON objects fail the runtime class-brand check;
|
|
5128
|
+
// exact capsules unwrap the original object by identity.
|
|
5129
|
+
if (isDynTypedRefType(target)) {
|
|
5130
|
+
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
|
|
5131
|
+
}
|
|
7167
5132
|
// ADAPTABLE function targets (`u as (x: number) => number` — the
|
|
7168
5133
|
// checked-dynamic function boundary): kind check, then exact unwrap
|
|
7169
5134
|
// or the per-target adapter shim. Non-adaptable signatures keep the
|
|
@@ -7328,10 +5293,46 @@ function seqExprSafeStmt(s) {
|
|
|
7328
5293
|
return false;
|
|
7329
5294
|
}
|
|
7330
5295
|
}
|
|
5296
|
+
/** Run one supported `any` binary operation in the island. Checked-dynamic
|
|
5297
|
+
* operands marshal in only after lowerBinary's identity-preserving native
|
|
5298
|
+
* cases have had their chance (notably dyn strict equality). */
|
|
5299
|
+
function lowerAnyBinaryInIsland(lowerer, expr, left, right, loc) {
|
|
5300
|
+
const op = expr.operatorToken.kind;
|
|
5301
|
+
const JS_BIN = {
|
|
5302
|
+
[ts.SyntaxKind.PlusToken]: "add",
|
|
5303
|
+
[ts.SyntaxKind.MinusToken]: "sub",
|
|
5304
|
+
[ts.SyntaxKind.AsteriskToken]: "mul",
|
|
5305
|
+
[ts.SyntaxKind.SlashToken]: "div",
|
|
5306
|
+
[ts.SyntaxKind.PercentToken]: "mod",
|
|
5307
|
+
[ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
|
|
5308
|
+
[ts.SyntaxKind.LessThanToken]: "lt",
|
|
5309
|
+
[ts.SyntaxKind.LessThanEqualsToken]: "le",
|
|
5310
|
+
[ts.SyntaxKind.GreaterThanToken]: "gt",
|
|
5311
|
+
[ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
|
|
5312
|
+
[ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
|
|
5313
|
+
[ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
|
|
5314
|
+
};
|
|
5315
|
+
const jop = JS_BIN[op];
|
|
5316
|
+
if (jop === undefined) {
|
|
5317
|
+
lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
|
|
5318
|
+
}
|
|
5319
|
+
const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
|
|
5320
|
+
return {
|
|
5321
|
+
kind: "jsOp", op: jop,
|
|
5322
|
+
args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
|
|
5323
|
+
type, loc,
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
7331
5326
|
export function lowerBinary(lowerer, expr) {
|
|
7332
5327
|
const loc = locOf(expr);
|
|
7333
5328
|
const op = expr.operatorToken.kind;
|
|
5329
|
+
const cacheHas = lowerRequireCacheHas(lowerer, expr);
|
|
5330
|
+
if (cacheHas)
|
|
5331
|
+
return cacheHas;
|
|
7334
5332
|
if (op === ts.SyntaxKind.EqualsToken || (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment)) {
|
|
5333
|
+
if (ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) {
|
|
5334
|
+
fenceNodeModuleMutation(lowerer, expr.left, "assignment");
|
|
5335
|
+
}
|
|
7335
5336
|
// `x = e` in EXPRESSION position (`while ((idx = s.indexOf("\n")) !== -1)`,
|
|
7336
5337
|
// `f(x = v)`): evaluate e once, write the binding, yield the assigned
|
|
7337
5338
|
// value — JS evaluation order. Variable targets only (locals and module
|
|
@@ -7442,7 +5443,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7442
5443
|
// RHS value is the expression's value — its OWN type, so the
|
|
7443
5444
|
// consumer sees exactly what the checker typed.
|
|
7444
5445
|
if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) {
|
|
7445
|
-
const recv =
|
|
5446
|
+
const recv = tryLowerExpression(lowerer, expr.left.expression);
|
|
7446
5447
|
if (recv && recv.type.kind === "dyn") {
|
|
7447
5448
|
const recvTmp = lowerer.declareHiddenLocal("%setRecv", DYN);
|
|
7448
5449
|
const rhsVal = lowerer.lowerExpr(expr.right);
|
|
@@ -7567,8 +5568,74 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7567
5568
|
// Everything else (`u && flag`, whose value would need a wider
|
|
7568
5569
|
// re-tagged union) stays fenced — in CONDITION position those
|
|
7569
5570
|
// shapes lower through lowerCondition's bool descent instead.
|
|
7570
|
-
|
|
5571
|
+
let target = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
5572
|
+
// A broad-JSDoc npm-static body can leave the CHECKER result `any`
|
|
5573
|
+
// even after declaration-backed specialization has recovered both
|
|
5574
|
+
// operands (`helpOption && args.find(...)` in commander). When the
|
|
5575
|
+
// left consists only of always-truthy references and falsy units,
|
|
5576
|
+
// the static result is exactly the right operand plus those units.
|
|
5577
|
+
if (target === null &&
|
|
5578
|
+
lowerer.implicitParamTypes !== null &&
|
|
5579
|
+
npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
|
|
5580
|
+
(lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 &&
|
|
5581
|
+
left.type.kind === "union") {
|
|
5582
|
+
const leftArms = lowerer.unions.get(left.type.unionId)?.arms;
|
|
5583
|
+
const rightArms = right.type.kind === "union"
|
|
5584
|
+
? lowerer.unions.get(right.type.unionId)?.arms
|
|
5585
|
+
: [right.type];
|
|
5586
|
+
const rightIsStatic = rightArms?.every((arm) => arm.kind !== "dyn" && arm.kind !== "caught" && arm.kind !== "jsval" && arm.kind !== "void") ?? false;
|
|
5587
|
+
if (leftArms && rightArms && rightIsStatic &&
|
|
5588
|
+
leftArms.some(isUnitType) &&
|
|
5589
|
+
leftArms.every((arm) => isUnitType(arm) || REF_TRUTHY_KINDS.has(arm.kind))) {
|
|
5590
|
+
const byKey = new Map();
|
|
5591
|
+
for (const arm of [...rightArms, ...leftArms.filter(isUnitType)]) {
|
|
5592
|
+
byKey.set(typeKey(arm), arm);
|
|
5593
|
+
}
|
|
5594
|
+
const arms = [...byKey.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
|
5595
|
+
if (arms.length > 1) {
|
|
5596
|
+
target = { kind: "union", unionId: lowerer.unions.intern(arms) };
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
7571
5600
|
if (target?.kind === "union") {
|
|
5601
|
+
// `&&` whose left does NOT fit the result union: the checker
|
|
5602
|
+
// built that result by DROPPING left arms which are always
|
|
5603
|
+
// truthy (`Option | undefined && string | undefined` answers
|
|
5604
|
+
// `string | undefined`, not `Option | string | undefined`).
|
|
5605
|
+
// Evaluate the left once, test it in its own union, and retag it
|
|
5606
|
+
// only on the falsy path. Any stranded reference arms are
|
|
5607
|
+
// unreachable there because every JS object is truthy.
|
|
5608
|
+
if (op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && !typeEquals(left.type, target)) {
|
|
5609
|
+
const def = lowerer.unions.get(left.type.unionId);
|
|
5610
|
+
const trappable = new Set();
|
|
5611
|
+
def?.arms.forEach((arm, tag) => {
|
|
5612
|
+
if (lowerer.armTag(target.unionId, arm) < 0 && REF_TRUTHY_KINDS.has(arm.kind)) {
|
|
5613
|
+
trappable.add(tag);
|
|
5614
|
+
}
|
|
5615
|
+
});
|
|
5616
|
+
const retag = trappable.size === 0
|
|
5617
|
+
? null
|
|
5618
|
+
: lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc, trappable);
|
|
5619
|
+
if (retag) {
|
|
5620
|
+
lowerer.requireTruthyUnion(left.type.unionId, expr);
|
|
5621
|
+
const stmts = [];
|
|
5622
|
+
let stable = left;
|
|
5623
|
+
if (!isSafeToRepeat(left)) {
|
|
5624
|
+
const local = lowerer.declareHiddenLocal("%and", left.type);
|
|
5625
|
+
stmts.push({ kind: "varDecl", localId: local.id, init: left, loc });
|
|
5626
|
+
stable = varRef(local.id, left.type, loc);
|
|
5627
|
+
}
|
|
5628
|
+
const result = {
|
|
5629
|
+
kind: "ternary",
|
|
5630
|
+
cond: lowerer.ensureBool(stable, expr.left),
|
|
5631
|
+
then: lowerer.coerceInto(expr.right, right, target),
|
|
5632
|
+
else_: { kind: "call", callee: retag, args: [stable], type: target, loc },
|
|
5633
|
+
type: target,
|
|
5634
|
+
loc,
|
|
5635
|
+
};
|
|
5636
|
+
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: target, loc };
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
7572
5639
|
// `||` whose left does NOT fit the result union: the checker built
|
|
7573
5640
|
// that result by DROPPING the left's falsy arms (`process.env.X ||
|
|
7574
5641
|
// null` is `string | null`, `|| 3000` is `string | number` — the
|
|
@@ -7576,9 +5643,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7576
5643
|
// the shared shape below does, retags an arm the test is about to
|
|
7577
5644
|
// rule out and throws where Node yields the default. Single-eval
|
|
7578
5645
|
// instead: test the left in its OWN union and retag only on the
|
|
7579
|
-
// truthy side, where the dropped arms are unreachable.
|
|
7580
|
-
// the eager shape — there the falsy left IS the result, so the
|
|
7581
|
-
// checker keeps those arms in the target and they must coerce.
|
|
5646
|
+
// truthy side, where the dropped arms are unreachable.
|
|
7582
5647
|
if (op === ts.SyntaxKind.BarBarToken && left.type.kind === "union" && !typeEquals(left.type, target)) {
|
|
7583
5648
|
const retag = lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc);
|
|
7584
5649
|
if (retag) {
|
|
@@ -7791,6 +5856,14 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7791
5856
|
// retry lifts the site. Genuine unknown keeps the SC1100 story.
|
|
7792
5857
|
if ((left.type.kind === "dyn" && lowerer.anyOrigin(expr.left)) ||
|
|
7793
5858
|
(right.type.kind === "dyn" && lowerer.anyOrigin(expr.right))) {
|
|
5859
|
+
// `JSON.parse` and other checked-dynamic producers deliberately
|
|
5860
|
+
// remain dyn when assigned to an `any` local: eagerly converting
|
|
5861
|
+
// the binding would deep-copy its data and sever aliases. At an
|
|
5862
|
+
// operator the dynamic build can cross only the operands and let
|
|
5863
|
+
// the engine apply JS's exact coercion semantics. Static builds
|
|
5864
|
+
// retain the SC2011 promise that --dynamic lifts this site.
|
|
5865
|
+
if (lowerer.dynamic)
|
|
5866
|
+
return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
|
|
7794
5867
|
lowerer.anyOpFence(`the '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' operator`, expr);
|
|
7795
5868
|
}
|
|
7796
5869
|
// tsc allows ===/!== on unknown (arithmetic/comparisons it rejects
|
|
@@ -7802,30 +5875,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7802
5875
|
// computes. Comparisons come back as static bools; arithmetic stays
|
|
7803
5876
|
// an island value ('1 as any + "x"' is a string over there).
|
|
7804
5877
|
if (left.type.kind === "jsval" || right.type.kind === "jsval") {
|
|
7805
|
-
|
|
7806
|
-
[ts.SyntaxKind.PlusToken]: "add",
|
|
7807
|
-
[ts.SyntaxKind.MinusToken]: "sub",
|
|
7808
|
-
[ts.SyntaxKind.AsteriskToken]: "mul",
|
|
7809
|
-
[ts.SyntaxKind.SlashToken]: "div",
|
|
7810
|
-
[ts.SyntaxKind.PercentToken]: "mod",
|
|
7811
|
-
[ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
|
|
7812
|
-
[ts.SyntaxKind.LessThanToken]: "lt",
|
|
7813
|
-
[ts.SyntaxKind.LessThanEqualsToken]: "le",
|
|
7814
|
-
[ts.SyntaxKind.GreaterThanToken]: "gt",
|
|
7815
|
-
[ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
|
|
7816
|
-
[ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
|
|
7817
|
-
[ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
|
|
7818
|
-
};
|
|
7819
|
-
const jop = JS_BIN[op];
|
|
7820
|
-
if (jop === undefined) {
|
|
7821
|
-
lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
|
|
7822
|
-
}
|
|
7823
|
-
const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
|
|
7824
|
-
return {
|
|
7825
|
-
kind: "jsOp", op: jop,
|
|
7826
|
-
args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
|
|
7827
|
-
type, loc,
|
|
7828
|
-
};
|
|
5878
|
+
return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
|
|
7829
5879
|
}
|
|
7830
5880
|
// An unchecked array read keeps its undefined arm in the IR even when
|
|
7831
5881
|
// the checker typed the binding as `string`. An optional string is not
|
|
@@ -7860,7 +5910,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7860
5910
|
// with observable getters/calls.
|
|
7861
5911
|
const prefix = [];
|
|
7862
5912
|
const stable = (operand) => {
|
|
7863
|
-
if (
|
|
5913
|
+
if (isSafeToRepeat(operand) || isSafeToDiscard(operand))
|
|
7864
5914
|
return operand;
|
|
7865
5915
|
const tmp = lowerer.declareHiddenLocal("%plus", operand.type);
|
|
7866
5916
|
prefix.push({ kind: "varDecl", localId: tmp.id, init: operand, loc });
|
|
@@ -8518,7 +6568,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
|
|
|
8518
6568
|
continue;
|
|
8519
6569
|
const tags = answers.flatMap((s, i) => (s === b.text ? [i] : []));
|
|
8520
6570
|
if (tags.length === 0 || tags.length === def.arms.length) {
|
|
8521
|
-
if (!
|
|
6571
|
+
if (!isSafeToRepeat(value)) {
|
|
8522
6572
|
lowerer.unsupported("SC1090", expr, "statically-decided 'typeof' tests over effectful operands (bind the value to a const first)");
|
|
8523
6573
|
}
|
|
8524
6574
|
return { kind: "boolLit", value: (tags.length !== 0) !== negated, type: BOOL, loc };
|
|
@@ -8528,7 +6578,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
|
|
|
8528
6578
|
});
|
|
8529
6579
|
if (tags.length === 1)
|
|
8530
6580
|
return isTag(tags[0]);
|
|
8531
|
-
if (!
|
|
6581
|
+
if (!isSafeToRepeat(value)) {
|
|
8532
6582
|
lowerer.unsupported("SC1090", expr, "'typeof' tests matching several union arms over operands that aren't plain reads (bind the value to a const first)");
|
|
8533
6583
|
}
|
|
8534
6584
|
// Tag-in-set as a short-circuit chain (De Morgan for `!==`).
|
|
@@ -8583,7 +6633,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
|
|
|
8583
6633
|
const recv = unparen(pn.expression);
|
|
8584
6634
|
if (ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) {
|
|
8585
6635
|
const inner = unparen(recv.expression);
|
|
8586
|
-
const obj =
|
|
6636
|
+
const obj = tryLowerExpression(lowerer, inner);
|
|
8587
6637
|
if (obj?.type.kind === "dyn") {
|
|
8588
6638
|
keyed = {
|
|
8589
6639
|
kind: "dynKeyGet",
|
|
@@ -8606,7 +6656,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
|
|
|
8606
6656
|
// "string") ... }`, the validateConfig idiom). Probe the lowering
|
|
8607
6657
|
// and claim exactly the dyn results; anything else keeps its
|
|
8608
6658
|
// sibling lowerings and fences untouched.
|
|
8609
|
-
const probed =
|
|
6659
|
+
const probed = tryLowerExpression(lowerer, a.expression);
|
|
8610
6660
|
if (probed?.type.kind !== "dyn")
|
|
8611
6661
|
continue;
|
|
8612
6662
|
value = probed;
|
|
@@ -8770,33 +6820,6 @@ function lowerCjsExportTableThisMember(lowerer, expr) {
|
|
|
8770
6820
|
return null;
|
|
8771
6821
|
return cjsAccessorCall(lowerer, sibling, locOf(expr));
|
|
8772
6822
|
}
|
|
8773
|
-
/** Lowers an expression, converting a fence (PoisonError) into a null
|
|
8774
|
-
* DECLINE — the probing dispatch for shapes where only the lowered
|
|
8775
|
-
* value's type can say whether a lowering applies. unsupported() pushes
|
|
8776
|
-
* its diagnostic BEFORE throwing, so the probe runs behind a diagnostic
|
|
8777
|
-
* sink: a declined probe's diagnostics vanish (the sibling lowerings
|
|
8778
|
-
* own the site), a successful probe's flush through afterwards. */
|
|
8779
|
-
export function probeLower(lowerer, node) {
|
|
8780
|
-
const saved = lowerer.diagSink;
|
|
8781
|
-
const captured = [];
|
|
8782
|
-
lowerer.diagSink = captured;
|
|
8783
|
-
let result;
|
|
8784
|
-
try {
|
|
8785
|
-
result = lowerer.lowerExpr(node);
|
|
8786
|
-
}
|
|
8787
|
-
catch (e) {
|
|
8788
|
-
if (e instanceof PoisonError) {
|
|
8789
|
-
lowerer.diagSink = saved;
|
|
8790
|
-
return null;
|
|
8791
|
-
}
|
|
8792
|
-
lowerer.diagSink = saved;
|
|
8793
|
-
throw e;
|
|
8794
|
-
}
|
|
8795
|
-
lowerer.diagSink = saved;
|
|
8796
|
-
for (const d of captured)
|
|
8797
|
-
lowerer.pushDiag(d);
|
|
8798
|
-
return result;
|
|
8799
|
-
}
|
|
8800
6823
|
/** A read of a catch binding — the caught analog of maybeNarrow. Where
|
|
8801
6824
|
* tsc's control-flow narrowing has proven a supported test (`instanceof`
|
|
8802
6825
|
* over a hierarchy class, `typeof` over a primitive), the read bridges
|
|
@@ -9276,7 +7299,7 @@ function lowerInExpression(lowerer, expr, loc) {
|
|
|
9276
7299
|
// stringified like every property key (o[k] is o[String(k)] in JS;
|
|
9277
7300
|
// `in` shares the coercion).
|
|
9278
7301
|
{
|
|
9279
|
-
const probed =
|
|
7302
|
+
const probed = tryLowerExpression(lowerer, expr.right);
|
|
9280
7303
|
if (probed?.type.kind === "dyn") {
|
|
9281
7304
|
let k = lowerer.lowerExpr(expr.left); // JS order: the key evaluates first
|
|
9282
7305
|
if (k.type.kind === "f64" || k.type.kind === "string" || k.type.kind === "dyn") {
|
|
@@ -10106,6 +8129,34 @@ export function lowerUnionProperty(lowerer, expr) {
|
|
|
10106
8129
|
if (!def)
|
|
10107
8130
|
throw new InternalCompilerError(`lowerer bug: unknown union ${value.type.unionId}`);
|
|
10108
8131
|
const field = expr.name.text;
|
|
8132
|
+
if (field === "length") {
|
|
8133
|
+
const presentArms = def.arms.filter((arm) => !isUnitType(arm));
|
|
8134
|
+
const present = presentArms.length === 1 ? presentArms[0] : null;
|
|
8135
|
+
if (present?.kind === "string" || present?.kind === "array") {
|
|
8136
|
+
const receiver = lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, present, field);
|
|
8137
|
+
if (receiver !== null) {
|
|
8138
|
+
return present.kind === "string"
|
|
8139
|
+
? { kind: "strIntrinsic", method: "length", receiver, args: [], type: F64, loc: locOf(expr) }
|
|
8140
|
+
: { kind: "arrIntrinsic", method: "length", receiver, args: [], type: F64, loc: locOf(expr) };
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
const nonUnitArms = def.arms.filter((arm) => !isUnitType(arm));
|
|
8145
|
+
if (nonUnitArms.length === 1 && nonUnitArms[0]?.kind === "object") {
|
|
8146
|
+
const objectType = nonUnitArms[0];
|
|
8147
|
+
const fieldType = lowerer.classes.get(objectType.className)?.fields.get(field);
|
|
8148
|
+
if (fieldType !== undefined) {
|
|
8149
|
+
let receiver = lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, objectType, field);
|
|
8150
|
+
if (receiver === null) {
|
|
8151
|
+
const helper = lowerer.narrowedArmHelper(value.type.unionId, objectType, locOf(expr.expression));
|
|
8152
|
+
if (helper !== null)
|
|
8153
|
+
receiver = { kind: "call", callee: helper, args: [value], type: objectType, loc: locOf(expr.expression) };
|
|
8154
|
+
}
|
|
8155
|
+
if (receiver !== null) {
|
|
8156
|
+
return { kind: "fieldGet", obj: receiver, className: objectType.className, field, type: fieldType, loc: locOf(expr) };
|
|
8157
|
+
}
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
10109
8160
|
let common = null;
|
|
10110
8161
|
for (const arm of def.arms) {
|
|
10111
8162
|
let ft;
|
|
@@ -10770,7 +8821,7 @@ export function lowerFieldCompound(lowerer, access, op, rhsNode, loc) {
|
|
|
10770
8821
|
// receiver is an identifier (checked above), so evaluating it for
|
|
10771
8822
|
// read and write matches JS's once-evaluation observably.
|
|
10772
8823
|
if (ts.isPropertyAccessExpression(access)) {
|
|
10773
|
-
const probed =
|
|
8824
|
+
const probed = tryLowerExpression(lowerer, access.expression);
|
|
10774
8825
|
if (probed?.type.kind === "dyn") {
|
|
10775
8826
|
const key = { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access.name) };
|
|
10776
8827
|
const read = { kind: "dynKeyGet", key, value: probed, type: DYN, loc };
|