@scriptc/compiler 0.0.36 → 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 +52 -22
- package/ambient/scriptc-overrides.d.ts +0 -7
- package/ambient/scriptc.d.ts +3 -2
- 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 +289 -55
- 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 +3 -0
- 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 +76 -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 +36 -12
- package/dist/backend/c/walkers.js.map +1 -1
- package/dist/backend/external-c.d.ts +33 -0
- package/dist/backend/external-c.js +51 -0
- package/dist/backend/external-c.js.map +1 -0
- package/dist/backend/link-plan.d.ts +37 -0
- package/dist/backend/link-plan.js +26 -0
- package/dist/backend/link-plan.js.map +1 -0
- package/dist/backend/linker.d.ts +22 -0
- package/dist/backend/linker.js +236 -0
- package/dist/backend/linker.js.map +1 -0
- package/dist/backend/llvm/blocks.js.map +1 -1
- package/dist/backend/llvm/dyn.js +63 -3
- package/dist/backend/llvm/dyn.js.map +1 -1
- package/dist/backend/llvm/emitter.js +132 -26
- 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 +68 -54
- package/dist/backend/llvm/expr-containers.js.map +1 -1
- package/dist/backend/llvm/expr-context.d.ts +2 -1
- package/dist/backend/llvm/expr-control.js.map +1 -1
- package/dist/backend/llvm/expr-dispatch.js +2 -0
- 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.d.ts +1 -1
- package/dist/backend/llvm/expr-primitives.js +20 -0
- 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 +245 -10
- 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 +31 -4
- package/dist/backend/llvm/walkers.js.map +1 -1
- package/dist/backend/native-codegen.d.ts +8 -1
- package/dist/backend/native-codegen.js +39 -25
- package/dist/backend/native-codegen.js.map +1 -1
- package/dist/backend/native-codegen.test.js +44 -4
- package/dist/backend/native-codegen.test.js.map +1 -1
- package/dist/backend/native-link-info.d.ts +1 -1
- package/dist/backend/native-link-info.js +42 -18
- package/dist/backend/native-link-info.js.map +1 -1
- package/dist/backend/native-toolchain.js.map +1 -1
- package/dist/backend/native-toolchain.test.js +5 -1
- package/dist/backend/native-toolchain.test.js.map +1 -1
- package/dist/backend/runtime-pack-targets.test.d.ts +1 -0
- package/dist/backend/runtime-pack-targets.test.js +54 -0
- package/dist/backend/runtime-pack-targets.test.js.map +1 -0
- package/dist/backend/runtime-pack.d.ts +5 -30
- package/dist/backend/runtime-pack.js +46 -141
- package/dist/backend/runtime-pack.js.map +1 -1
- package/dist/backend/runtime-pack.test.js +49 -14
- package/dist/backend/runtime-pack.test.js.map +1 -1
- package/dist/backend/targets.d.ts +63 -17
- package/dist/backend/targets.js +251 -35
- package/dist/backend/targets.js.map +1 -1
- package/dist/backend/targets.test.js +27 -5
- package/dist/backend/targets.test.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 +22 -1
- 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 +12 -0
- package/dist/frontend/lowering/array-values.js +76 -0
- package/dist/frontend/lowering/array-values.js.map +1 -0
- 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-array-sort.d.ts +22 -0
- package/dist/frontend/lowering/lower-array-sort.js +619 -0
- package/dist/frontend/lowering/lower-array-sort.js.map +1 -0
- 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 +379 -48
- package/dist/frontend/lowering/lower-builtins.js.map +1 -1
- package/dist/frontend/lowering/lower-calls.d.ts +16 -8
- package/dist/frontend/lowering/lower-calls.js +667 -91
- 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 +779 -2038
- 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 +4 -78
- package/dist/frontend/lowering/lower-exprs.js +1128 -2409
- 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 +50 -36
- package/dist/frontend/lowering/lower-inspect.js.map +1 -1
- package/dist/frontend/lowering/lower-island.d.ts +11 -4
- package/dist/frontend/lowering/lower-island.js +47 -10
- 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 +11 -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 +811 -314
- 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 +109 -34
- package/dist/frontend/lowering/lowerer.js +1225 -62
- 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/lowering/surfaces.d.ts +10 -2
- package/dist/frontend/lowering/surfaces.js +15 -3
- package/dist/frontend/lowering/surfaces.js.map +1 -1
- 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.d.ts +3 -3
- package/dist/index.js +50 -28
- package/dist/index.js.map +1 -1
- package/dist/ir/analysis.js.map +1 -1
- package/dist/ir/ir.d.ts +125 -41
- package/dist/ir/ir.js +82 -31
- package/dist/ir/ir.js.map +1 -1
- package/dist/ir/serialize.d.ts +1 -1
- package/dist/ir/serialize.js +1 -1
- package/dist/ir/validate.js +127 -95
- 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/fence-eval.js +3 -1
- package/dist/library/fence-eval.js.map +1 -1
- package/dist/library/int-infer.js +12 -1
- package/dist/library/int-infer.js.map +1 -1
- package/dist/library/int-infer.test.js +4 -4
- package/dist/library/int-infer.test.js.map +1 -1
- package/dist/library/library-cache.test.js +5 -5
- 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.d.ts +9 -1
- package/dist/startup-cache.js +16 -1
- package/dist/startup-cache.js.map +1 -1
- package/dist/wasi-paths.js.map +1 -1
- package/package.json +18 -5
- package/surface-manifest.json +64 -8
|
@@ -7,13 +7,15 @@ import { InternalCompilerError } from "../../errors.js";
|
|
|
7
7
|
* (FieldTarget). */
|
|
8
8
|
import * as ts from "../ts7/adapter.js";
|
|
9
9
|
import { dirname, posix } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
10
11
|
import { wasiGuestPath } from "../../wasi-paths.js";
|
|
11
|
-
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";
|
|
12
13
|
import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
|
|
13
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";
|
|
14
|
-
import { blockedBindingUseDiag,
|
|
15
|
+
import { blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
|
|
15
16
|
import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
|
|
16
|
-
import {
|
|
17
|
+
import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
|
|
18
|
+
import { arrayValueStore } from "./array-values.js";
|
|
17
19
|
import { npmStaticPackageOfPath } from "../npm-static.js";
|
|
18
20
|
import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
|
|
19
21
|
import { fenceEnumObjectValue, lowerEnumAccess } from "./lower-enums.js";
|
|
@@ -27,6 +29,11 @@ import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTy
|
|
|
27
29
|
import { lowerYield } from "./lower-generators.js";
|
|
28
30
|
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
|
|
29
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";
|
|
30
37
|
/** A template piece's RAW text (String.raw's contract: escapes stay
|
|
31
38
|
* characters). 7's client AST ships no rawText at runtime (the typing
|
|
32
39
|
* declares it; the serialized node data omits it), so the raw span comes
|
|
@@ -403,7 +410,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
403
410
|
lowerer.unsupported("SC1090", prop, "a conditional spread mixed with plain spreads in an 'any'-typed object literal");
|
|
404
411
|
}
|
|
405
412
|
const cond = lowerer.lowerCondition(cs.cond);
|
|
406
|
-
if (!
|
|
413
|
+
if (!isSafeToMoveConditionEarlier(cond)) {
|
|
407
414
|
lowerer.unsupported("SC1090", prop, "conditional spreads with effectful conditions in an 'any'-typed object literal (bind the condition to a const first)");
|
|
408
415
|
}
|
|
409
416
|
spread = { cond, whenTrue: cs.whenTrue };
|
|
@@ -531,6 +538,14 @@ function lowerExprInner(lowerer, expr) {
|
|
|
531
538
|
return { kind: "jsOp", op: "arrLit", args, type: JSVAL, loc };
|
|
532
539
|
}
|
|
533
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
|
+
}
|
|
534
549
|
// `typeof queueMicrotask` / `typeof DOMException` on a STDLIB global
|
|
535
550
|
// whose declared type is callable or constructable: folds to
|
|
536
551
|
// "function" BEFORE the operand lowers — the identity-token story
|
|
@@ -576,7 +591,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
576
591
|
undefinedT: "undefined", nullT: "object",
|
|
577
592
|
};
|
|
578
593
|
const folded = FOLD[operand.type.kind];
|
|
579
|
-
if (folded &&
|
|
594
|
+
if (folded && isSafeToDiscard(operand)) {
|
|
580
595
|
return { kind: "strLit", value: folded, type: STRING, loc };
|
|
581
596
|
}
|
|
582
597
|
// A union operand: every arm's typeof answer is static, so the value
|
|
@@ -585,7 +600,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
585
600
|
// tests, so only side-effect-free reads compose — and when every arm
|
|
586
601
|
// agrees the whole expression folds to that one string (dropping only
|
|
587
602
|
// the pure read, the trust-the-checker bet lowerUnitComparison makes).
|
|
588
|
-
if (operand.type.kind === "union" &&
|
|
603
|
+
if (operand.type.kind === "union" && isSafeToRepeat(operand)) {
|
|
589
604
|
const def = lowerer.unions.get(operand.type.unionId);
|
|
590
605
|
const answers = def?.arms.map(typeofAnswer);
|
|
591
606
|
if (def && answers && answers.every((s) => s !== null)) {
|
|
@@ -612,6 +627,9 @@ function lowerExprInner(lowerer, expr) {
|
|
|
612
627
|
lowerer.unsupported("SC1090", expr, "typeof expressions on statically-typed values");
|
|
613
628
|
}
|
|
614
629
|
if (ts.isIdentifier(expr)) {
|
|
630
|
+
const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
|
|
631
|
+
if (moduleValue)
|
|
632
|
+
return moduleValue;
|
|
615
633
|
if (lowerer.isSelfReference(expr)) {
|
|
616
634
|
return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
|
|
617
635
|
}
|
|
@@ -640,9 +658,13 @@ function lowerExprInner(lowerer, expr) {
|
|
|
640
658
|
const symbol = lowerer.resolveValueSymbol(expr);
|
|
641
659
|
const runtimeOptionalRoot = lowerer.runtimeOptionalRootOf(local);
|
|
642
660
|
const active = lowerer.runtimeOptionalLocals.has(runtimeOptionalRoot);
|
|
661
|
+
const arithmetic = lowerer.runtimeOptionalArithmeticLocals.has(runtimeOptionalRoot);
|
|
643
662
|
const captured = !!symbol &&
|
|
644
663
|
lowerer.runtimeOptionalStorageLocals.has(runtimeOptionalRoot) &&
|
|
645
|
-
lowerer.ctx.captureBySymbol.get(symbol) === local;
|
|
664
|
+
(lowerer.ctx.captureBySymbol.get(symbol) === local || runtimeOptionalRoot !== local);
|
|
665
|
+
if (arithmetic && !runtimeOptionalUseOf(expr)) {
|
|
666
|
+
return { kind: "varRef", localId: local.id, type: local.type, loc };
|
|
667
|
+
}
|
|
646
668
|
if (active || captured) {
|
|
647
669
|
const use = runtimeOptionalUseOf(expr);
|
|
648
670
|
if (use?.complex) {
|
|
@@ -665,8 +687,20 @@ function lowerExprInner(lowerer, expr) {
|
|
|
665
687
|
}
|
|
666
688
|
return runtimeOptionalReceiverRead(lowerer, expr, local, null, loc);
|
|
667
689
|
}
|
|
668
|
-
if (
|
|
669
|
-
|
|
690
|
+
if (runtimeOptionalObjectWalkerArg(lowerer, expr)) {
|
|
691
|
+
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
692
|
+
if (local.type.kind === "union" && narrowed) {
|
|
693
|
+
const helper = narrowed.kind === "union"
|
|
694
|
+
? lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc)
|
|
695
|
+
: !isUnitType(narrowed)
|
|
696
|
+
? lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc)
|
|
697
|
+
: null;
|
|
698
|
+
if (helper) {
|
|
699
|
+
return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return { kind: "varRef", localId: local.id, type: local.type, loc };
|
|
670
704
|
}
|
|
671
705
|
return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, expr);
|
|
672
706
|
}
|
|
@@ -686,7 +720,53 @@ function lowerExprInner(lowerer, expr) {
|
|
|
686
720
|
lowerer.pushDiag(blockedBindingUseDiag(expr.text, loc));
|
|
687
721
|
throw new PoisonError();
|
|
688
722
|
}
|
|
689
|
-
|
|
723
|
+
const ref = { kind: "varRef", localId: g.id, type: g.type, loc };
|
|
724
|
+
if (lowerer.isRuntimeOptionalArithmeticGlobal(g) && !runtimeOptionalUseOf(expr)) {
|
|
725
|
+
return ref;
|
|
726
|
+
}
|
|
727
|
+
const storageHasUndefined = g.type.kind === "union" &&
|
|
728
|
+
lowerer.armTag(g.type.unionId, UNDEFINED_T) >= 0;
|
|
729
|
+
if (lowerer.isRuntimeOptionalGlobal(g) || storageHasUndefined) {
|
|
730
|
+
// A checker-narrowed global receiver (`if (tags !== undefined)
|
|
731
|
+
// tags[tags.length] = ...`, or `a ? Object.keys(a) : ...`) is
|
|
732
|
+
// still stored as the runtime optional union. Extract the proven
|
|
733
|
+
// value arm before routing the member/element operation so dynamic
|
|
734
|
+
// keys and builtin record walkers do not see a union receiver.
|
|
735
|
+
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
736
|
+
const arithmeticUnion = g.type.kind === "union" &&
|
|
737
|
+
(lowerer.unions.get(g.type.unionId)?.arms.length === 2) &&
|
|
738
|
+
lowerer.unions.get(g.type.unionId).arms.some((a) => a.kind === "f64") &&
|
|
739
|
+
lowerer.unions.get(g.type.unionId).arms.some((a) => a.kind === "string");
|
|
740
|
+
if (!arithmeticUnion && g.type.kind === "union" && narrowed && narrowed.kind !== "union" &&
|
|
741
|
+
!isUnitType(narrowed) &&
|
|
742
|
+
narrowed.kind !== "f64" && narrowed.kind !== "string" && narrowed.kind !== "bool" &&
|
|
743
|
+
lowerer.armTag(g.type.unionId, narrowed) >= 0) {
|
|
744
|
+
const helper = lowerer.narrowedArmHelper(g.type.unionId, narrowed, loc);
|
|
745
|
+
if (helper)
|
|
746
|
+
return { kind: "call", callee: helper, args: [ref], type: narrowed, loc };
|
|
747
|
+
}
|
|
748
|
+
const use = runtimeOptionalUseOf(expr);
|
|
749
|
+
// The enclosing logical/conditional expression consumes the union
|
|
750
|
+
// before any member operation runs (`(value ?? fallback).length`).
|
|
751
|
+
// Keep the tagged value intact and let that lazy expression decide
|
|
752
|
+
// its branch; extracting here would evaluate or throw too early.
|
|
753
|
+
if (use?.complex)
|
|
754
|
+
return ref;
|
|
755
|
+
if (use?.optional)
|
|
756
|
+
return ref;
|
|
757
|
+
if (use?.kind === "property")
|
|
758
|
+
return runtimeOptionalReceiverRead(lowerer, expr, g, use.access.name.text, loc);
|
|
759
|
+
if (use?.kind === "element") {
|
|
760
|
+
const key = runtimeOptionalElementKey(use.access.argumentExpression);
|
|
761
|
+
if (key === null)
|
|
762
|
+
lowerer.unsupported("SC1090", use.access, "a computed read from a runtime-optional global (use a literal key)");
|
|
763
|
+
return runtimeOptionalReceiverRead(lowerer, expr, g, key, loc);
|
|
764
|
+
}
|
|
765
|
+
if (use?.kind === "call")
|
|
766
|
+
return runtimeOptionalReceiverRead(lowerer, expr, g, null, loc);
|
|
767
|
+
return ref;
|
|
768
|
+
}
|
|
769
|
+
return lowerer.maybeNarrow(ref, expr);
|
|
690
770
|
}
|
|
691
771
|
// `undefined` — tsc's intrinsic global (a local shadowing the name
|
|
692
772
|
// resolved above): a unit literal, exactly like the `null` keyword.
|
|
@@ -709,9 +789,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
709
789
|
// directory at `/tmp`. Bake the matching GUEST spelling so resource
|
|
710
790
|
// lookups relative to these CommonJS globals stay inside the exposed
|
|
711
791
|
// capability instead of naming an unreachable host-absolute path.
|
|
712
|
-
const fileName = lowerer
|
|
713
|
-
? wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")
|
|
714
|
-
: sf.fileName;
|
|
792
|
+
const fileName = moduleFileName(lowerer, sf);
|
|
715
793
|
const value = expr.text === "__dirname"
|
|
716
794
|
? (lowerer.targetPlatform === "wasi" ? posix.dirname(fileName) : dirname(fileName))
|
|
717
795
|
: fileName;
|
|
@@ -1214,8 +1292,8 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1214
1292
|
// The tail of an optional chain whose `?.` sits deeper
|
|
1215
1293
|
// (`x?.trim().toLowerCase()`): the whole tail short-circuits with
|
|
1216
1294
|
// the guard, so it lowers as one chain (dyn/island receivers keep
|
|
1217
|
-
// their own undefined-propagating reads — see
|
|
1218
|
-
if (
|
|
1295
|
+
// their own undefined-propagating reads — see isOptionalChainTail).
|
|
1296
|
+
if (isOptionalChainTail(lowerer, expr))
|
|
1219
1297
|
return lowerer.lowerOptionalChain(expr);
|
|
1220
1298
|
return lowerer.lowerCall(expr);
|
|
1221
1299
|
}
|
|
@@ -1243,6 +1321,15 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1243
1321
|
if (en)
|
|
1244
1322
|
return en;
|
|
1245
1323
|
}
|
|
1324
|
+
// `import.meta.<field>` — the file-backed ESM metadata that survives
|
|
1325
|
+
// compilation as a constant. Only direct, identifier-named reads are
|
|
1326
|
+
// claimed here; the bare meta object, computed access, writes, and
|
|
1327
|
+
// unsupported members continue through the generic module-loader fence
|
|
1328
|
+
// below. The source file is the module identity, so imported modules
|
|
1329
|
+
// receive their own filename/dirname and only the entry receives main.
|
|
1330
|
+
const importMeta = importMetaProperty(lowerer, expr);
|
|
1331
|
+
if (importMeta)
|
|
1332
|
+
return importMeta;
|
|
1246
1333
|
// `require.main.filename` / `require.main?.filename` — CommonJS
|
|
1247
1334
|
// entry-module identity: in a compiled binary require.main IS the
|
|
1248
1335
|
// entry module (never undefined in a CJS graph, so the chain's guard
|
|
@@ -1254,6 +1341,17 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1254
1341
|
if (isRequireMainFilename(lowerer, expr)) {
|
|
1255
1342
|
return { kind: "strLit", value: lowerer.entry.fileName, type: STRING, loc };
|
|
1256
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
|
+
}
|
|
1257
1355
|
// Optional chaining `a?.b`: the guard lowers here (a tag test around
|
|
1258
1356
|
// the plain property lowering below); the handled marker keeps this
|
|
1259
1357
|
// re-entrant dispatch from looping.
|
|
@@ -1262,7 +1360,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1262
1360
|
}
|
|
1263
1361
|
// `a?.b.c` — the tail of a chain whose token sits deeper: the whole
|
|
1264
1362
|
// tail short-circuits with the guard.
|
|
1265
|
-
if (!expr.questionDotToken &&
|
|
1363
|
+
if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
|
|
1266
1364
|
return lowerer.lowerOptionalChain(expr);
|
|
1267
1365
|
}
|
|
1268
1366
|
// Island receiver: o.x is an engine property read (getProp may throw
|
|
@@ -1536,8 +1634,14 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1536
1634
|
lowerer.lowerFieldRead(expr);
|
|
1537
1635
|
// A union-typed field read narrows like an identifier when the
|
|
1538
1636
|
// checker has narrowed this use to one arm.
|
|
1539
|
-
if (handled)
|
|
1637
|
+
if (handled) {
|
|
1638
|
+
if (handled.type.kind === "union" &&
|
|
1639
|
+
lowerer.armTag(handled.type.unionId, UNDEFINED_T) >= 0 &&
|
|
1640
|
+
handled.kind === "recordGet" &&
|
|
1641
|
+
lowerer.isRuntimeOptionalField(handled.shapeId, handled.field))
|
|
1642
|
+
return handled;
|
|
1540
1643
|
return lowerer.maybeNarrow(handled, expr);
|
|
1644
|
+
}
|
|
1541
1645
|
// `globalThis.<name>` that no lowering above claimed
|
|
1542
1646
|
// (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage
|
|
1543
1647
|
// — the harness's capability-conditional knownGlobals adds): the
|
|
@@ -1585,7 +1689,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1585
1689
|
const recv = lowerer.lowerExpr(expr.expression);
|
|
1586
1690
|
if (recv.type.kind === "dyn") {
|
|
1587
1691
|
const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
|
|
1588
|
-
const opt =
|
|
1692
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
1589
1693
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc }, expr);
|
|
1590
1694
|
}
|
|
1591
1695
|
}
|
|
@@ -1599,7 +1703,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1599
1703
|
// `f.call` as a value). JS files only; TS keeps the fence.
|
|
1600
1704
|
if (!["apply", "bind", "call", "toString", "constructor", "prototype", "caller", "arguments"].includes(expr.name.text) &&
|
|
1601
1705
|
isJsSourceFile(expr.getSourceFile())) {
|
|
1602
|
-
const probed =
|
|
1706
|
+
const probed = tryLowerExpression(lowerer, expr.expression);
|
|
1603
1707
|
if (probed?.type.kind === "func" &&
|
|
1604
1708
|
canBoxFuncIntoDyn(probed.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
|
|
1605
1709
|
const fnName = jsFuncNameOf(expr.expression);
|
|
@@ -1664,7 +1768,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1664
1768
|
type: STRING,
|
|
1665
1769
|
loc: locOf(expr.name),
|
|
1666
1770
|
};
|
|
1667
|
-
const opt =
|
|
1771
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
1668
1772
|
return lowerer.maybeNarrow({
|
|
1669
1773
|
kind: "dynKeyGet",
|
|
1670
1774
|
key,
|
|
@@ -1724,7 +1828,7 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1724
1828
|
// validated dynCheck as usual.
|
|
1725
1829
|
if (recvLowered.type.kind === "dyn") {
|
|
1726
1830
|
const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
|
|
1727
|
-
const opt =
|
|
1831
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
1728
1832
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recvLowered, type: DYN, loc }, expr);
|
|
1729
1833
|
}
|
|
1730
1834
|
// The lowered receiver is a RECORD the checker spelled wider —
|
|
@@ -1778,6 +1882,43 @@ function lowerExprInner(lowerer, expr) {
|
|
|
1778
1882
|
lowerer.unsupported(entry.code, expr, entry.feature);
|
|
1779
1883
|
lowerer.unsupported("SC1090", expr, `syntax '${ts.SyntaxKind[expr.kind]}'`);
|
|
1780
1884
|
}
|
|
1885
|
+
function moduleFileName(lowerer, sf) {
|
|
1886
|
+
return lowerer.targetPlatform === "wasi"
|
|
1887
|
+
? wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")
|
|
1888
|
+
: sf.fileName;
|
|
1889
|
+
}
|
|
1890
|
+
function importMetaProperty(lowerer, expr) {
|
|
1891
|
+
if (expr.questionDotToken !== undefined || !ts.isMetaProperty(expr.expression))
|
|
1892
|
+
return null;
|
|
1893
|
+
const meta = expr.expression;
|
|
1894
|
+
if (meta.keywordToken !== ts.SyntaxKind.ImportKeyword || meta.name.text !== "meta")
|
|
1895
|
+
return null;
|
|
1896
|
+
const sf = expr.getSourceFile();
|
|
1897
|
+
const fileName = moduleFileName(lowerer, sf);
|
|
1898
|
+
switch (expr.name.text) {
|
|
1899
|
+
case "url":
|
|
1900
|
+
return {
|
|
1901
|
+
kind: "strLit",
|
|
1902
|
+
value: pathToFileURL(fileName, { windows: lowerer.targetPlatform === "win32" }).href,
|
|
1903
|
+
type: STRING,
|
|
1904
|
+
loc: locOf(expr),
|
|
1905
|
+
};
|
|
1906
|
+
case "filename":
|
|
1907
|
+
return { kind: "strLit", value: fileName, type: STRING, loc: locOf(expr) };
|
|
1908
|
+
case "dirname":
|
|
1909
|
+
return {
|
|
1910
|
+
kind: "strLit",
|
|
1911
|
+
value: lowerer.targetPlatform === "wasi" ? posix.dirname(fileName) : dirname(fileName),
|
|
1912
|
+
type: STRING,
|
|
1913
|
+
loc: locOf(expr),
|
|
1914
|
+
};
|
|
1915
|
+
case "main":
|
|
1916
|
+
return { kind: "boolLit", value: sf === lowerer.entry, type: BOOL, loc: locOf(expr) };
|
|
1917
|
+
default:
|
|
1918
|
+
lowerer.unsupported("SC1090", expr, `'import.meta.${expr.name.text}' (only url, filename, dirname, and main are supported in static ESM)`);
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1781
1922
|
/** `c ? a : b` — see the inline comments. `expected` plays the contextual
|
|
1782
1923
|
* array type's role when the caller knows the slot's array type and tsc's
|
|
1783
1924
|
* API doesn't surface it (a ternary as a SPREAD source — lowerArrayLiteral
|
|
@@ -2125,19 +2266,65 @@ export function maybeNarrow(lowerer, expr, node) {
|
|
|
2125
2266
|
* even though an OOB-safe assignment may have restored undefined. Check
|
|
2126
2267
|
* that hidden arm and throw the member-read TypeError Node would produce. */
|
|
2127
2268
|
function runtimeOptionalReceiverRead(lowerer, expr, local, property, loc) {
|
|
2128
|
-
if (local.type.kind !== "union")
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2269
|
+
if (local.type.kind !== "union") {
|
|
2270
|
+
lowerer.runtimeOptionalLocals.delete(local);
|
|
2271
|
+
lowerer.runtimeOptionalStorageLocals.delete(local);
|
|
2272
|
+
return varRef(local.id, local.type, loc);
|
|
2273
|
+
}
|
|
2274
|
+
// A non-null/type assertion around the receiver carries the useful
|
|
2275
|
+
// checker narrowing on the wrapper rather than on the identifier token
|
|
2276
|
+
// itself (`gg!.day`). Follow only erased wrappers; the member/call node
|
|
2277
|
+
// still receives the checked extraction built below.
|
|
2278
|
+
let narrowedNode = expr;
|
|
2279
|
+
while ((ts.isParenthesizedExpression(narrowedNode.parent) ||
|
|
2280
|
+
ts.isAssertionExpression(narrowedNode.parent) ||
|
|
2281
|
+
ts.isSatisfiesExpression(narrowedNode.parent) ||
|
|
2282
|
+
ts.isNonNullExpression(narrowedNode.parent)) &&
|
|
2283
|
+
narrowedNode.parent.expression === narrowedNode) {
|
|
2284
|
+
narrowedNode = narrowedNode.parent;
|
|
2285
|
+
}
|
|
2286
|
+
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(narrowedNode));
|
|
2287
|
+
if (!narrowed || isUnitType(narrowed)) {
|
|
2132
2288
|
lowerer.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed value type is not representable");
|
|
2133
2289
|
}
|
|
2290
|
+
if (narrowed.kind === "union") {
|
|
2291
|
+
// The checker may keep the receiver at the storage union unchanged
|
|
2292
|
+
// (`string | undefined` on a JS `.match()` call). There is no
|
|
2293
|
+
// sub-union to re-tag: preserve the value for the downstream
|
|
2294
|
+
// nullable-receiver lowering, which performs the member-specific
|
|
2295
|
+
// checked extraction. Refusing here deferred the whole statement to
|
|
2296
|
+
// a runtime fence before that lowering could run.
|
|
2297
|
+
if (typeEquals(local.type, narrowed)) {
|
|
2298
|
+
return varRef(local.id, local.type, loc);
|
|
2299
|
+
}
|
|
2300
|
+
// A captured array element can itself be a union of records (for
|
|
2301
|
+
// example Circle | Square). The optional storage union has one extra
|
|
2302
|
+
// undefined arm; re-tag it to the checker-narrowed value union with a
|
|
2303
|
+
// checked trap for that arm, then let union property lowering inspect
|
|
2304
|
+
// the surviving record arms.
|
|
2305
|
+
const helper = lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc);
|
|
2306
|
+
if (!helper) {
|
|
2307
|
+
lowerer.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed union is not representable");
|
|
2308
|
+
}
|
|
2309
|
+
return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
|
|
2310
|
+
}
|
|
2134
2311
|
const valueTag = lowerer.armTag(local.type.unionId, narrowed);
|
|
2135
2312
|
const undefTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
|
|
2136
2313
|
if (valueTag < 0 || undefTag < 0)
|
|
2137
2314
|
throw new InternalCompilerError("runtime-optional local is missing its value or undefined arm");
|
|
2138
2315
|
const def = lowerer.unions.get(local.type.unionId);
|
|
2139
2316
|
if (!def || def.arms.length !== 2) {
|
|
2140
|
-
|
|
2317
|
+
// The checker may narrow a runtime-optional capture to one arm of a
|
|
2318
|
+
// value union (`Circle | Square | undefined` -> `Circle`). The
|
|
2319
|
+
// two-arm fast path below cannot extract that arm safely: a later
|
|
2320
|
+
// array mutation can leave any of the stored tags in the slot. Use
|
|
2321
|
+
// the checked helper so every other arm, including undefined, remains
|
|
2322
|
+
// a catchable TypeError rather than an unchecked payload read.
|
|
2323
|
+
const helper = lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc);
|
|
2324
|
+
if (!helper) {
|
|
2325
|
+
lowerer.unsupported("SC1090", expr, "a direct read from a runtime-optional capture with multiple value arms");
|
|
2326
|
+
}
|
|
2327
|
+
return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
|
|
2141
2328
|
}
|
|
2142
2329
|
const message = property === null
|
|
2143
2330
|
? `${expr.text} is not a function`
|
|
@@ -2209,6 +2396,25 @@ function runtimeOptionalUseOf(expr) {
|
|
|
2209
2396
|
}
|
|
2210
2397
|
return null;
|
|
2211
2398
|
}
|
|
2399
|
+
/** Object.keys/Object.entries require an object receiver, but their first
|
|
2400
|
+
* argument is not syntactically a member receiver. Recognize that one
|
|
2401
|
+
* standard-library position so a runtime-optional loop element can be
|
|
2402
|
+
* checked against the checker-narrowed record arm before builtin lowering. */
|
|
2403
|
+
function runtimeOptionalObjectWalkerArg(lowerer, expr) {
|
|
2404
|
+
let arg = expr;
|
|
2405
|
+
while ((ts.isParenthesizedExpression(arg.parent) ||
|
|
2406
|
+
ts.isAssertionExpression(arg.parent) ||
|
|
2407
|
+
ts.isSatisfiesExpression(arg.parent) ||
|
|
2408
|
+
ts.isNonNullExpression(arg.parent)) &&
|
|
2409
|
+
arg.parent.expression === arg) {
|
|
2410
|
+
arg = arg.parent;
|
|
2411
|
+
}
|
|
2412
|
+
const call = arg.parent;
|
|
2413
|
+
if (!ts.isCallExpression(call) || call.arguments[0] !== arg || !ts.isPropertyAccessExpression(call.expression))
|
|
2414
|
+
return false;
|
|
2415
|
+
const access = call.expression;
|
|
2416
|
+
return (access.name.text === "keys" || access.name.text === "entries") && lowerer.isStdlibMember(access);
|
|
2417
|
+
}
|
|
2212
2418
|
function runtimeOptionalElementKey(expr) {
|
|
2213
2419
|
if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr))
|
|
2214
2420
|
return expr.text;
|
|
@@ -2255,7 +2461,7 @@ export function lowerUnitComparison(lowerer, left, right, negated, loc) {
|
|
|
2255
2461
|
// callback's effects are observable) rides one throwaway tag
|
|
2256
2462
|
// test; droppable reads fold to the bare literal.
|
|
2257
2463
|
const answer = { kind: "boolLit", value: negated, type: BOOL, loc };
|
|
2258
|
-
if (
|
|
2464
|
+
if (isSafeToDiscard(other))
|
|
2259
2465
|
return answer;
|
|
2260
2466
|
const evalOnce = {
|
|
2261
2467
|
kind: "unionIsTag", unionId: other.type.unionId, tag: 0, negated: false, value: other, type: BOOL, loc,
|
|
@@ -2363,7 +2569,7 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
|
|
|
2363
2569
|
// backend emits it per test — restricted to re-emittable pure reads;
|
|
2364
2570
|
// anything effectful gets its own actionable fence (the generic
|
|
2365
2571
|
// SC1040 hint would wrongly suggest `== null` as the fix).
|
|
2366
|
-
if (!
|
|
2572
|
+
if (!isSafeToRepeat(other)) {
|
|
2367
2573
|
lowerer.unsupported("SC1090", expr, `'== null' / '!= null' on a '${lowerer.fmt(ut)}' value that isn't a plain read ` +
|
|
2368
2574
|
`(both unit arms need the operand twice — bind it to a const first)`);
|
|
2369
2575
|
}
|
|
@@ -2393,88 +2599,6 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
|
|
|
2393
2599
|
// `== null` is statically false, `!= null` statically true.
|
|
2394
2600
|
return { kind: "boolLit", value: negated, type: BOOL, loc };
|
|
2395
2601
|
}
|
|
2396
|
-
/** Safe to EMIT twice: plain reads with no side effects — local/global
|
|
2397
|
-
* reads and record/class field reads over such (accessor properties
|
|
2398
|
-
* lower to getter CALLS, so they never appear as field reads). Used by
|
|
2399
|
-
* the two-unit-arm `== null` composition, the union `typeof` forms, and
|
|
2400
|
-
* the union-switch desugar, whose operands ride several tests. */
|
|
2401
|
-
/** A static expression whose evaluation is unobservable — safe to DROP
|
|
2402
|
-
* (typeof's constant fold: JS evaluates the operand, but when nothing in
|
|
2403
|
-
* it can throw, call, write, or allocate observably, folding it away
|
|
2404
|
-
* changes nothing). Strictly wider than pureReemittable: numeric
|
|
2405
|
-
* arithmetic (f64 ops never throw — division by zero is Infinity),
|
|
2406
|
-
* string concatenation, logical/ternary composition, and unit/literal
|
|
2407
|
-
* leaves compose; anything with a call, an island op, an index read (can
|
|
2408
|
-
* trap), or an unknown kind answers false. */
|
|
2409
|
-
export function droppableStatic(e) {
|
|
2410
|
-
switch (e.kind) {
|
|
2411
|
-
case "numLit":
|
|
2412
|
-
case "strLit":
|
|
2413
|
-
case "boolLit":
|
|
2414
|
-
case "unitLit":
|
|
2415
|
-
return true;
|
|
2416
|
-
case "bin":
|
|
2417
|
-
return droppableStatic(e.left) && droppableStatic(e.right);
|
|
2418
|
-
case "unary":
|
|
2419
|
-
return droppableStatic(e.operand);
|
|
2420
|
-
case "logical":
|
|
2421
|
-
return droppableStatic(e.left) && droppableStatic(e.right);
|
|
2422
|
-
case "ternary":
|
|
2423
|
-
return droppableStatic(e.cond) && droppableStatic(e.then) && droppableStatic(e.else_);
|
|
2424
|
-
case "strConcat":
|
|
2425
|
-
return droppableStatic(e.left) && droppableStatic(e.right);
|
|
2426
|
-
case "toBool":
|
|
2427
|
-
return droppableStatic(e.operand);
|
|
2428
|
-
case "unionIsTag":
|
|
2429
|
-
return droppableStatic(e.value);
|
|
2430
|
-
case "unionWrap":
|
|
2431
|
-
return droppableStatic(e.value);
|
|
2432
|
-
// Fresh allocations are unobservable too — only their PIECES can
|
|
2433
|
-
// carry effects (a spread re-reads its source: pure; element and
|
|
2434
|
-
// field initializers recurse).
|
|
2435
|
-
case "arrayLit":
|
|
2436
|
-
return e.elems.every(droppableStatic);
|
|
2437
|
-
case "recordLit":
|
|
2438
|
-
return e.fields.every((f) => droppableStatic(f.value));
|
|
2439
|
-
case "recordClone":
|
|
2440
|
-
return droppableStatic(e.source) && e.overrides.every((f) => droppableStatic(f.value));
|
|
2441
|
-
case "closure":
|
|
2442
|
-
return true;
|
|
2443
|
-
default:
|
|
2444
|
-
return pureReemittable(e);
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
export function pureReemittable(e) {
|
|
2448
|
-
if (e.kind === "varRef")
|
|
2449
|
-
return true;
|
|
2450
|
-
if (e.kind === "recordGet" || e.kind === "fieldGet")
|
|
2451
|
-
return pureReemittable(e.obj);
|
|
2452
|
-
// The trust-the-checker narrowing bridges over a pure read: extraction
|
|
2453
|
-
// and downcast are reads too (the +1 on ref payloads is RC bookkeeping,
|
|
2454
|
-
// not an observable effect — each emission owns its own copy).
|
|
2455
|
-
if (e.kind === "unionNarrow" || e.kind === "downcast")
|
|
2456
|
-
return pureReemittable(e.value);
|
|
2457
|
-
return false;
|
|
2458
|
-
}
|
|
2459
|
-
/** A side-effect-free CONDITION: the ToBoolean/test wrappers lowerCondition
|
|
2460
|
-
* builds over pure reads (evaluating one early or twice changes nothing
|
|
2461
|
-
* observable). Used by the island-literal conditional spread, whose
|
|
2462
|
-
* desugar hoists the condition ahead of the properties. */
|
|
2463
|
-
function pureCondExpr(e) {
|
|
2464
|
-
if (e.kind === "boolLit")
|
|
2465
|
-
return true;
|
|
2466
|
-
if (e.kind === "toBool")
|
|
2467
|
-
return pureReemittable(e.operand);
|
|
2468
|
-
if (e.kind === "unionIsTag" || e.kind === "dynTest")
|
|
2469
|
-
return pureReemittable(e.value);
|
|
2470
|
-
if (e.kind === "unary" && e.op === "!")
|
|
2471
|
-
return pureCondExpr(e.operand);
|
|
2472
|
-
if (e.kind === "logical")
|
|
2473
|
-
return pureCondExpr(e.left) && pureCondExpr(e.right);
|
|
2474
|
-
if (e.kind === "jsOp" && (e.op === "truthy" || e.op === "not"))
|
|
2475
|
-
return e.args.every(pureReemittable);
|
|
2476
|
-
return pureReemittable(e);
|
|
2477
|
-
}
|
|
2478
2602
|
/** `a ?? b` — JS-exact nullish coalescing: ONLY null/undefined take the
|
|
2479
2603
|
* default (0, "", and false do not), and the right side evaluates lazily.
|
|
2480
2604
|
* On a unit-armed union left this is the `nullish` node (a runtime tag
|
|
@@ -2523,6 +2647,42 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
|
|
|
2523
2647
|
const right = lowerer.lowerExprExpecting(expr.right, type);
|
|
2524
2648
|
return { kind: "nullish", left, right, type, loc };
|
|
2525
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
|
+
}
|
|
2526
2686
|
// The RETAGGED shape — the default changes the result union (`s ??
|
|
2527
2687
|
// null` over `string | undefined` answers `null | string`): every
|
|
2528
2688
|
// non-unit arm of the left has a home in the result, so an interned
|
|
@@ -2533,9 +2693,72 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
|
|
|
2533
2693
|
if (type.kind === "union") {
|
|
2534
2694
|
const leftT = left.type;
|
|
2535
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
|
+
}
|
|
2536
2759
|
if (armPairs.every((p) => p.src >= 0 && p.dst >= 0)) {
|
|
2537
2760
|
const right = lowerer.lowerExprExpecting(expr.right, type);
|
|
2538
|
-
if (
|
|
2761
|
+
if (isSafeToDiscard(right)) {
|
|
2539
2762
|
const helper = nullishRetagHelper(lowerer, leftT, type, loc);
|
|
2540
2763
|
return { kind: "call", callee: helper, args: [left, right], type, loc };
|
|
2541
2764
|
}
|
|
@@ -2602,383 +2825,6 @@ function nullishRetagHelper(lowerer, leftT, resT, loc) {
|
|
|
2602
2825
|
});
|
|
2603
2826
|
return name;
|
|
2604
2827
|
}
|
|
2605
|
-
/** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
|
|
2606
|
-
* the member access) and `f?.()` (the token on the call). The receiver
|
|
2607
|
-
* lowers once; when it is a unit-armed union with ONE non-unit arm, the
|
|
2608
|
-
* member/call lowers exactly as its non-optional spelling would — the
|
|
2609
|
-
* receiver node reads back as the chain's bound narrowed value
|
|
2610
|
-
* (chainRecv) and types as its non-nullish type — and the whole thing
|
|
2611
|
-
* becomes an optChain node: tag test, undefined on the unit path (JS:
|
|
2612
|
-
* null receivers still yield undefined), the body lazily otherwise,
|
|
2613
|
-
* argument side effects included. A receiver the checker types
|
|
2614
|
-
* never-nullish makes `?.` behave exactly like `.` — the guard folds
|
|
2615
|
-
* away and the plain lowering is the value (trust-the-checker, like
|
|
2616
|
-
* ??'s fold). Multi-step TAILS (`a?.b.c`, `x?.trim().toLowerCase()`)
|
|
2617
|
-
* short-circuit whole: the guarded member step is the chain's dot and
|
|
2618
|
-
* every later step lowers inside the guard, checker-narrowed non-nullish
|
|
2619
|
-
* (see chainTailDot); sub-union receivers are fenced with rewrite
|
|
2620
|
-
* hints. */
|
|
2621
|
-
/** The unhandled `?.`-carrying MEMBER step in this node's receiver
|
|
2622
|
-
* spine, when the node is the TAIL of an optional chain whose token sits
|
|
2623
|
-
* deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x
|
|
2624
|
-
* is nullish, so the whole tail must lower inside it. Walks
|
|
2625
|
-
* property/element accesses and call steps only (parens and `!` break
|
|
2626
|
-
* the chain per the grammar: `(a?.b).c` throws on undefined in JS);
|
|
2627
|
-
* the node's OWN token — and a call's immediate callee token — are the
|
|
2628
|
-
* plain entries' cases, not tails, and handled markers make the chain
|
|
2629
|
-
* lowering's re-dispatch walk past its own step. `f?.()` steps stay out
|
|
2630
|
-
* (a call carrying the token has no member step to re-enter — the
|
|
2631
|
-
* split-it fence keeps them). */
|
|
2632
|
-
function chainTailDot(lowerer, expr) {
|
|
2633
|
-
let cur = expr;
|
|
2634
|
-
for (;;) {
|
|
2635
|
-
// An active chain's bound receiver read is a LEAF — its own token
|
|
2636
|
-
// was consumed by the chain that bound it.
|
|
2637
|
-
if (lowerer.chainRecvByNode.has(cur))
|
|
2638
|
-
return null;
|
|
2639
|
-
if (ts.isCallExpression(cur)) {
|
|
2640
|
-
// `f?.()` steps route through their own entry (or are being
|
|
2641
|
-
// handled); a deeper token under one is that chain's business.
|
|
2642
|
-
if (cur.questionDotToken)
|
|
2643
|
-
return null;
|
|
2644
|
-
cur = cur.expression;
|
|
2645
|
-
continue;
|
|
2646
|
-
}
|
|
2647
|
-
if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
|
|
2648
|
-
if (cur.questionDotToken) {
|
|
2649
|
-
// The node's own token and a call's immediate callee token are
|
|
2650
|
-
// the plain entries' cases; a handled one is mid-re-dispatch
|
|
2651
|
-
// (its receiver read, further down, ends the walk). The walk
|
|
2652
|
-
// NEVER continues past an unhandled deeper token: that guard is
|
|
2653
|
-
// where this tail's chain enters — anything below it belongs to
|
|
2654
|
-
// the receiver's own (nested) chain.
|
|
2655
|
-
if (cur === expr || lowerer.chainHandled.has(cur)) {
|
|
2656
|
-
cur = cur.expression;
|
|
2657
|
-
continue;
|
|
2658
|
-
}
|
|
2659
|
-
if (ts.isCallExpression(expr) && expr.expression === cur)
|
|
2660
|
-
return null;
|
|
2661
|
-
return cur;
|
|
2662
|
-
}
|
|
2663
|
-
cur = cur.expression;
|
|
2664
|
-
continue;
|
|
2665
|
-
}
|
|
2666
|
-
return null;
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
/** True when `expr` is `require.main.filename` (either dot optional) on
|
|
2670
|
-
* the AMBIENT CommonJS require — the entry-module identity read. The
|
|
2671
|
-
* value is the ENTRY file's path, a compile-time constant like
|
|
2672
|
-
* __filename; ESM files stay out (Node never defines require there —
|
|
2673
|
-
* the ambient symbol would not resolve anyway). Shared by the property
|
|
2674
|
-
* lowering (the fold) and lowerStringMethodCall's receiver gate (the
|
|
2675
|
-
* checker types the chain `string | undefined`, but the folded receiver
|
|
2676
|
-
* IS a string). */
|
|
2677
|
-
export function isRequireMainFilename(lowerer, expr) {
|
|
2678
|
-
if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "filename")
|
|
2679
|
-
return false;
|
|
2680
|
-
const main = expr.expression;
|
|
2681
|
-
if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main")
|
|
2682
|
-
return false;
|
|
2683
|
-
if (!lowerer.isStdlibGlobal(main.expression, "require"))
|
|
2684
|
-
return false;
|
|
2685
|
-
return !isNodeEsmFile(expr.getSourceFile());
|
|
2686
|
-
}
|
|
2687
|
-
/** True when `expr` is the tail of an optional chain the STATIC chain
|
|
2688
|
-
* machinery should short-circuit whole: an unhandled deeper `?.` whose
|
|
2689
|
-
* guarded receiver lowers as a unit-armed union. Dyn ('unknown') and
|
|
2690
|
-
* island ('any') receivers answer their tails through their own
|
|
2691
|
-
* undefined-propagating reads and stay out; never-nullish receivers fold
|
|
2692
|
-
* at the token's own entry. */
|
|
2693
|
-
function chainTailClaimed(lowerer, expr) {
|
|
2694
|
-
const tail = chainTailDot(lowerer, expr);
|
|
2695
|
-
if (!tail)
|
|
2696
|
-
return false;
|
|
2697
|
-
const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
|
|
2698
|
-
if (recvT?.kind !== "union")
|
|
2699
|
-
return false;
|
|
2700
|
-
const def = lowerer.unions.get(recvT.unionId);
|
|
2701
|
-
return !!def && def.arms.some(isUnitType);
|
|
2702
|
-
}
|
|
2703
|
-
/** True when an EARLIER step of this access chain carries `?.` — JS
|
|
2704
|
-
* short-circuits the whole tail (`a?.b.c` reads nothing when a is
|
|
2705
|
-
* nullish), so a dyn tail read must answer undefined instead of
|
|
2706
|
-
* throwing. Walks the receiver spine only; the argument of the access
|
|
2707
|
-
* itself is not part of the guard. */
|
|
2708
|
-
function chainGuardedByQuestionDot(expr) {
|
|
2709
|
-
let cur = expr;
|
|
2710
|
-
for (;;) {
|
|
2711
|
-
if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) {
|
|
2712
|
-
cur = cur.expression;
|
|
2713
|
-
continue;
|
|
2714
|
-
}
|
|
2715
|
-
if (ts.isPropertyAccessExpression(cur) ||
|
|
2716
|
-
ts.isElementAccessExpression(cur) ||
|
|
2717
|
-
ts.isCallExpression(cur)) {
|
|
2718
|
-
if (cur.questionDotToken)
|
|
2719
|
-
return true;
|
|
2720
|
-
cur = cur.expression;
|
|
2721
|
-
continue;
|
|
2722
|
-
}
|
|
2723
|
-
return false;
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
export function lowerOptionalChain(lowerer, expr) {
|
|
2727
|
-
const loc = locOf(expr);
|
|
2728
|
-
// The node CARRYING the ?. token and the receiver expression it guards.
|
|
2729
|
-
let dotNode;
|
|
2730
|
-
let recvNode;
|
|
2731
|
-
if (ts.isCallExpression(expr) && expr.questionDotToken) {
|
|
2732
|
-
dotNode = expr; // f?.()
|
|
2733
|
-
recvNode = expr.expression;
|
|
2734
|
-
}
|
|
2735
|
-
else if (ts.isCallExpression(expr) &&
|
|
2736
|
-
ts.isPropertyAccessExpression(expr.expression) &&
|
|
2737
|
-
expr.expression.questionDotToken) {
|
|
2738
|
-
dotNode = expr.expression; // a?.m()
|
|
2739
|
-
recvNode = expr.expression.expression;
|
|
2740
|
-
}
|
|
2741
|
-
else if ((ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) &&
|
|
2742
|
-
expr.questionDotToken) {
|
|
2743
|
-
dotNode = expr; // a?.b / a?.[i]
|
|
2744
|
-
recvNode = expr.expression;
|
|
2745
|
-
}
|
|
2746
|
-
else {
|
|
2747
|
-
// TAIL entry: the token sits deeper in the receiver spine and JS
|
|
2748
|
-
// short-circuits the WHOLE tail with it (`x?.trim().toLowerCase()`
|
|
2749
|
-
// reads nothing when x is nullish). The guarded member step becomes
|
|
2750
|
-
// the chain's dot; every step above it lowers inside the guard, its
|
|
2751
|
-
// checker type narrowed non-nullish below (the tag test proved it).
|
|
2752
|
-
const tail = chainTailDot(lowerer, expr);
|
|
2753
|
-
if (!tail) {
|
|
2754
|
-
// A `f?.()` step under a member tail, or no token at all (a
|
|
2755
|
-
// dispatch bug): short-circuiting those is not modeled.
|
|
2756
|
-
lowerer.unsupported("SC1090", expr, "multi-step optional chains (split them: const v = a?.b; then use v?.c)");
|
|
2757
|
-
}
|
|
2758
|
-
dotNode = tail;
|
|
2759
|
-
recvNode = tail.expression;
|
|
2760
|
-
}
|
|
2761
|
-
const receiver = lowerer.lowerExpr(recvNode);
|
|
2762
|
-
if (receiver.type.kind === "dyn") {
|
|
2763
|
-
// `pkg?.name` / `pkg?.scripts?.[k]` on a JSON.parse result: dyn
|
|
2764
|
-
// represents undefined directly, so the chain step IS the keyed
|
|
2765
|
-
// read with the optional (unit-answers-undefined) policy — no
|
|
2766
|
-
// optChain wrapper needed; nested steps compose the same way.
|
|
2767
|
-
if (dotNode === expr && ts.isPropertyAccessExpression(expr)) {
|
|
2768
|
-
const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
|
|
2769
|
-
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
|
|
2770
|
-
}
|
|
2771
|
-
if (dotNode === expr && ts.isElementAccessExpression(expr)) {
|
|
2772
|
-
const key = lowerer.lowerExpr(expr.argumentExpression);
|
|
2773
|
-
if (key.type.kind === "string") {
|
|
2774
|
-
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
|
|
2775
|
-
}
|
|
2776
|
-
// NUMBER-typed indices (`entries?.[0]`): the property key is
|
|
2777
|
-
// ToString(i), exactly JS — the canonical number text answers
|
|
2778
|
-
// array indices in the dyn helper, anything else (fractions,
|
|
2779
|
-
// negatives, NaN) reads as an absent key.
|
|
2780
|
-
if (key.type.kind === "f64") {
|
|
2781
|
-
const skey = { kind: "toString", operand: key, type: STRING, loc: key.loc };
|
|
2782
|
-
return lowerer.maybeNarrow({ kind: "dynKeyGet", key: skey, optional: true, value: receiver, type: DYN, loc }, expr);
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
// The METHOD-call step (`rawName?.match(re)` on a dyn value): a
|
|
2786
|
-
// nullish receiver short-circuits to the undefined dyn singleton
|
|
2787
|
-
// (dyn represents undefined directly); anything else runs the
|
|
2788
|
-
// VALIDATED dynamic dispatch — the same one the truthy-guarded
|
|
2789
|
-
// spelling (`v ? v.match(...) : null`) compiles to, Node-shaped
|
|
2790
|
-
// TypeError on a kind mismatch included. The dispatch's static
|
|
2791
|
-
// result converts back into the checked-dynamic tree (the chain's checker type is
|
|
2792
|
-
// the error-any world's `any`, so dyn IS its representation).
|
|
2793
|
-
if (ts.isCallExpression(expr) &&
|
|
2794
|
-
ts.isPropertyAccessExpression(dotNode) &&
|
|
2795
|
-
dotNode === expr.expression) {
|
|
2796
|
-
const m = dotNode.name.text;
|
|
2797
|
-
const id = `chain.${lowerer.chainCounter++}`;
|
|
2798
|
-
const recvRef = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) };
|
|
2799
|
-
lowerer.chainRecvByNode.set(recvNode, recvRef);
|
|
2800
|
-
lowerer.chainHandled.add(dotNode);
|
|
2801
|
-
let body;
|
|
2802
|
-
try {
|
|
2803
|
-
body = lowerer.lowerExpr(expr);
|
|
2804
|
-
}
|
|
2805
|
-
finally {
|
|
2806
|
-
lowerer.chainRecvByNode.delete(recvNode);
|
|
2807
|
-
lowerer.chainHandled.delete(dotNode);
|
|
2808
|
-
}
|
|
2809
|
-
if (body.type.kind !== "dyn") {
|
|
2810
|
-
if (body.type.kind !== "void" && !lowerer.dynConvertible(body.type)) {
|
|
2811
|
-
lowerer.unsupported("SC1100", expr, `optional METHOD calls on 'unknown' values where the result ('${lowerer.fmt(body.type)}' ` +
|
|
2812
|
-
`from '.${m}(...)') has no dynamic representation`);
|
|
2813
|
-
}
|
|
2814
|
-
if (body.type.kind === "void") {
|
|
2815
|
-
return { kind: "optChain", id, receiver, body, type: VOID, loc };
|
|
2816
|
-
}
|
|
2817
|
-
body = { kind: "dynFrom", value: body, type: DYN, loc };
|
|
2818
|
-
}
|
|
2819
|
-
return { kind: "optChain", id, receiver, body, type: DYN, loc };
|
|
2820
|
-
}
|
|
2821
|
-
lowerer.unsupported("SC1100", expr, "optional chaining on 'unknown' values");
|
|
2822
|
-
}
|
|
2823
|
-
// An 'any' (island-handle) receiver: the nullish test asks the ENGINE
|
|
2824
|
-
// value at runtime — null/undefined short-circuit to the engine's
|
|
2825
|
-
// undefined (JS: null receivers yield undefined too), anything else
|
|
2826
|
-
// proceeds as the plain island operation with the receiver evaluated
|
|
2827
|
-
// once (argument side effects stay lazy, like every optChain). The
|
|
2828
|
-
// result is an island value again ('any' in, 'any' out).
|
|
2829
|
-
if (receiver.type.kind === "jsval") {
|
|
2830
|
-
const id = `chain.${lowerer.chainCounter++}`;
|
|
2831
|
-
const recvRef = { kind: "chainRecv", id, type: JSVAL, loc: locOf(recvNode) };
|
|
2832
|
-
if (dotNode === expr && ts.isCallExpression(expr)) {
|
|
2833
|
-
// `a.b?.(...)` — a MEMBER callee: JS calls it with `this` bound to
|
|
2834
|
-
// `a` and short-circuits a nullish member — exactly the engine's
|
|
2835
|
-
// own `o.name?.()` (optCallMethod: the RECEIVER is `a`, evaluated
|
|
2836
|
-
// once through the chain; a nullish `a` itself short-circuits
|
|
2837
|
-
// earlier like any optional chain). Computed members keep a fence.
|
|
2838
|
-
if (ts.isPropertyAccessExpression(recvNode) || ts.isElementAccessExpression(recvNode)) {
|
|
2839
|
-
if (ts.isPropertyAccessExpression(recvNode) &&
|
|
2840
|
-
!recvNode.questionDotToken) {
|
|
2841
|
-
const obj = lowerer.lowerExpr(recvNode.expression);
|
|
2842
|
-
if (obj.type.kind !== "jsval")
|
|
2843
|
-
lowerer.badType(recvNode.expression, lowerer.typeOf(recvNode.expression));
|
|
2844
|
-
const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
|
|
2845
|
-
return { kind: "jsOp", op: "optCallMethod", name: recvNode.name.text, args: [obj, ...args], type: JSVAL, loc };
|
|
2846
|
-
}
|
|
2847
|
-
lowerer.unsupported("SC1090", expr, "optional calls of computed 'any' member values (a[k]?.() — bind the member to a const first)");
|
|
2848
|
-
}
|
|
2849
|
-
const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
|
|
2850
|
-
const body = { kind: "jsOp", op: "callFn", args: [recvRef, ...args], type: JSVAL, loc };
|
|
2851
|
-
return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
|
|
2852
|
-
}
|
|
2853
|
-
// Member forms (`x?.y`, `x?.y(...)`, `x?.[i]`): re-dispatch the plain
|
|
2854
|
-
// island lowering with the receiver node bound to the chain.
|
|
2855
|
-
lowerer.chainRecvByNode.set(recvNode, recvRef);
|
|
2856
|
-
lowerer.chainHandled.add(dotNode);
|
|
2857
|
-
let body;
|
|
2858
|
-
try {
|
|
2859
|
-
body = lowerer.lowerExpr(expr);
|
|
2860
|
-
}
|
|
2861
|
-
finally {
|
|
2862
|
-
lowerer.chainRecvByNode.delete(recvNode);
|
|
2863
|
-
lowerer.chainHandled.delete(dotNode);
|
|
2864
|
-
}
|
|
2865
|
-
if (body.type.kind !== "jsval")
|
|
2866
|
-
lowerer.badType(expr, lowerer.typeOf(expr));
|
|
2867
|
-
return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
|
|
2868
|
-
}
|
|
2869
|
-
const def = receiver.type.kind === "union" ? lowerer.unions.get(receiver.type.unionId) : undefined;
|
|
2870
|
-
if (!def || !def.arms.some(isUnitType)) {
|
|
2871
|
-
// Never nullish: `?.` IS `.` — re-dispatch the plain lowering. The
|
|
2872
|
-
// receiver subtree above is discarded (lowering is pure IR
|
|
2873
|
-
// construction); the fresh dispatch lowers it again in place. The
|
|
2874
|
-
// checker may still SPELL the receiver nullable while the lowering
|
|
2875
|
-
// answers the plain value (`process.getuid?.()` is the POSIX
|
|
2876
|
-
// number), so narrow the node too — downstream receiver-typed
|
|
2877
|
-
// dispatch must agree with the lowered value, not the spelling.
|
|
2878
|
-
lowerer.chainHandled.add(dotNode);
|
|
2879
|
-
const hadNarrow = lowerer.chainNarrowedType.has(recvNode);
|
|
2880
|
-
if (!hadNarrow) {
|
|
2881
|
-
lowerer.chainNarrowedType.set(recvNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode)));
|
|
2882
|
-
}
|
|
2883
|
-
try {
|
|
2884
|
-
return lowerer.lowerExpr(expr);
|
|
2885
|
-
}
|
|
2886
|
-
finally {
|
|
2887
|
-
lowerer.chainHandled.delete(dotNode);
|
|
2888
|
-
if (!hadNarrow)
|
|
2889
|
-
lowerer.chainNarrowedType.delete(recvNode);
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
const rest = def.arms.filter((a) => !isUnitType(a));
|
|
2893
|
-
if (rest.length !== 1) {
|
|
2894
|
-
lowerer.unsupported("SC1090", expr, `'?.' on '${lowerer.fmt(receiver.type)}' (the guarded receiver is a sub-union; check a discriminant field first)`);
|
|
2895
|
-
}
|
|
2896
|
-
const narrowed = rest[0];
|
|
2897
|
-
const id = `chain.${lowerer.chainCounter++}`;
|
|
2898
|
-
const recvRef = { kind: "chainRecv", id, type: narrowed, loc: locOf(recvNode) };
|
|
2899
|
-
// `f?.()`: the callee IS the guarded value — build the indirect call
|
|
2900
|
-
// directly (no member dispatch exists to re-enter).
|
|
2901
|
-
if (dotNode === expr && ts.isCallExpression(expr)) {
|
|
2902
|
-
if (narrowed.kind !== "func")
|
|
2903
|
-
lowerer.badType(recvNode, lowerer.typeOf(recvNode));
|
|
2904
|
-
const params = narrowed.params;
|
|
2905
|
-
const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
|
|
2906
|
-
const body = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
|
|
2907
|
-
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
|
|
2908
|
-
}
|
|
2909
|
-
// Member forms: re-dispatch the normal lowering with the receiver node
|
|
2910
|
-
// bound to the chain (reads as chainRecv, types as non-nullish).
|
|
2911
|
-
const narrowedTs = lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode));
|
|
2912
|
-
lowerer.chainRecvByNode.set(recvNode, recvRef);
|
|
2913
|
-
lowerer.chainNarrowedType.set(recvNode, narrowedTs);
|
|
2914
|
-
lowerer.chainHandled.add(dotNode);
|
|
2915
|
-
// A TAIL entry's intermediate steps (`x?.trim()` inside
|
|
2916
|
-
// `x?.trim().toLowerCase()`) checker-type with the chain's `|
|
|
2917
|
-
// undefined` even though inside the guard they are proven non-nullish
|
|
2918
|
-
// — narrow each so the downstream method/member dispatch rides the
|
|
2919
|
-
// real receiver kind. Only nodes this chain registers are cleaned up.
|
|
2920
|
-
const tailSteps = [];
|
|
2921
|
-
if (dotNode !== expr && !(ts.isCallExpression(expr) && expr.expression === dotNode)) {
|
|
2922
|
-
for (let cur = expr; cur !== dotNode;) {
|
|
2923
|
-
const next = ts.isCallExpression(cur)
|
|
2924
|
-
? cur.expression
|
|
2925
|
-
: cur.expression;
|
|
2926
|
-
if (next === dotNode)
|
|
2927
|
-
break;
|
|
2928
|
-
if (!lowerer.chainNarrowedType.has(next)) {
|
|
2929
|
-
lowerer.chainNarrowedType.set(next, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(next)));
|
|
2930
|
-
tailSteps.push(next);
|
|
2931
|
-
}
|
|
2932
|
-
cur = next;
|
|
2933
|
-
}
|
|
2934
|
-
if (!lowerer.chainNarrowedType.has(dotNode)) {
|
|
2935
|
-
lowerer.chainNarrowedType.set(dotNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(dotNode)));
|
|
2936
|
-
tailSteps.push(dotNode);
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
let body;
|
|
2940
|
-
try {
|
|
2941
|
-
body = lowerer.lowerExpr(expr);
|
|
2942
|
-
}
|
|
2943
|
-
finally {
|
|
2944
|
-
lowerer.chainRecvByNode.delete(recvNode);
|
|
2945
|
-
lowerer.chainNarrowedType.delete(recvNode);
|
|
2946
|
-
lowerer.chainHandled.delete(dotNode);
|
|
2947
|
-
for (const n of tailSteps)
|
|
2948
|
-
lowerer.chainNarrowedType.delete(n);
|
|
2949
|
-
}
|
|
2950
|
-
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
|
|
2951
|
-
}
|
|
2952
|
-
/** The optChain node around a lowered chain body: void bodies keep the
|
|
2953
|
-
* statement form (`cb?.();` — the checker's `void | undefined` result IS
|
|
2954
|
-
* void here); value bodies wrap into the checker's undefined-armed
|
|
2955
|
-
* result union. */
|
|
2956
|
-
export function finishOptionalChain(lowerer, expr, id, receiver, body, loc) {
|
|
2957
|
-
if (body.type.kind === "void") {
|
|
2958
|
-
return { kind: "optChain", id, receiver, body, type: VOID, loc };
|
|
2959
|
-
}
|
|
2960
|
-
// A dyn body (`pricing?.[key]` — an unknown-valued index-signature
|
|
2961
|
-
// read) stays dyn: `unknown | undefined` IS unknown, and dyn represents
|
|
2962
|
-
// undefined directly (the unit path yields the undefined dyn value).
|
|
2963
|
-
if (body.type.kind === "dyn") {
|
|
2964
|
-
return { kind: "optChain", id, receiver, body, type: DYN, loc };
|
|
2965
|
-
}
|
|
2966
|
-
let type = lowerer.irTypeOf(expr);
|
|
2967
|
-
const hiddenOptionalResult = (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) &&
|
|
2968
|
-
receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 &&
|
|
2969
|
-
body.type.kind !== "generator" && body.type.kind !== "jsval"
|
|
2970
|
-
? lowerer.withUndefinedArmOf(body.type)
|
|
2971
|
-
: null;
|
|
2972
|
-
if (hiddenOptionalResult)
|
|
2973
|
-
type = hiddenOptionalResult;
|
|
2974
|
-
if (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) {
|
|
2975
|
-
lowerer.unsupported("SC1090", expr, `'?.' where the result '${lowerer.fmt(type)}' has no undefined arm (narrow the receiver with 'if (x !== undefined)' instead)`);
|
|
2976
|
-
}
|
|
2977
|
-
const wrapped = lowerer.coerceToExpected(body, type);
|
|
2978
|
-
if (!typeEquals(wrapped.type, type))
|
|
2979
|
-
lowerer.badType(expr, lowerer.typeOf(expr));
|
|
2980
|
-
return { kind: "optChain", id, receiver, body: wrapped, type, loc };
|
|
2981
|
-
}
|
|
2982
2828
|
/** A CONDITION-position expression: the result is consumed as a bool
|
|
2983
2829
|
* only, so `&&`/`||` descend recursively over ToBoolean'd operands —
|
|
2984
2830
|
* JS-exact (`ToBoolean(a && b)` ≡ `ToBoolean(a) && ToBoolean(b)`), still
|
|
@@ -2988,6 +2834,11 @@ export function lowerCondition(lowerer, expr) {
|
|
|
2988
2834
|
let e = expr;
|
|
2989
2835
|
while (ts.isParenthesizedExpression(e))
|
|
2990
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
|
+
}
|
|
2991
2842
|
if (ts.isBinaryExpression(e)) {
|
|
2992
2843
|
const op = e.operatorToken.kind;
|
|
2993
2844
|
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
|
|
@@ -3021,6 +2872,47 @@ export function lowerCondition(lowerer, expr) {
|
|
|
3021
2872
|
}
|
|
3022
2873
|
return lowerer.ensureBool(lowerAbsenceProbe(lowerer, expr) ?? lowerer.lowerExpr(expr), expr);
|
|
3023
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
|
+
}
|
|
3024
2916
|
function runtimeOptionalLocalOf(lowerer, node) {
|
|
3025
2917
|
let expr = node;
|
|
3026
2918
|
while (ts.isParenthesizedExpression(expr))
|
|
@@ -3153,6 +3045,10 @@ export function lowerIntrinsicProperty(lowerer, expr) {
|
|
|
3153
3045
|
if (kind === "child" ? !isChildSurfaceMember(lowerer, expr) : !tupleLengthOnArray && !lowerer.isStdlibMember(expr))
|
|
3154
3046
|
return null;
|
|
3155
3047
|
const name = expr.name.text;
|
|
3048
|
+
const strictReceiver = (expected) => {
|
|
3049
|
+
const value = lowerer.lowerExpr(expr.expression);
|
|
3050
|
+
return lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, expected, name) ?? value;
|
|
3051
|
+
};
|
|
3156
3052
|
if (kind === "child") {
|
|
3157
3053
|
const loc = locOf(expr);
|
|
3158
3054
|
// The lifecycle reads, Node's exact shapes (SEMANTICS.md pins the
|
|
@@ -3315,7 +3211,7 @@ export function lowerIntrinsicProperty(lowerer, expr) {
|
|
|
3315
3211
|
}
|
|
3316
3212
|
if (kind === "regex") {
|
|
3317
3213
|
if (name === "source" || name === "flags") {
|
|
3318
|
-
const receiver =
|
|
3214
|
+
const receiver = strictReceiver({ kind: "regex" });
|
|
3319
3215
|
return { kind: "regexIntrinsic", method: name, receiver, args: [], type: STRING, loc: locOf(expr) };
|
|
3320
3216
|
}
|
|
3321
3217
|
if (name === "test") {
|
|
@@ -3334,7 +3230,19 @@ export function lowerIntrinsicProperty(lowerer, expr) {
|
|
|
3334
3230
|
if (kind === "bytes") {
|
|
3335
3231
|
const loc = locOf(expr);
|
|
3336
3232
|
if (name === "length" || name === "byteLength") {
|
|
3337
|
-
const
|
|
3233
|
+
const expected = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
|
3234
|
+
let receiver = expected?.kind === "bytes"
|
|
3235
|
+
? strictReceiver(expected)
|
|
3236
|
+
: lowerer.lowerExpr(expr.expression);
|
|
3237
|
+
if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
|
|
3238
|
+
const present = lowerer.stripUndefinedArm(receiver.type);
|
|
3239
|
+
const helper = present.kind === "bytes"
|
|
3240
|
+
? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(expr.expression))
|
|
3241
|
+
: null;
|
|
3242
|
+
receiver = helper
|
|
3243
|
+
? { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(expr.expression) }
|
|
3244
|
+
: lowerer.maybeNarrow(receiver, expr.expression);
|
|
3245
|
+
}
|
|
3338
3246
|
// An island handle behind a typed-array .d.ts surface: the engine
|
|
3339
3247
|
// property read, exiting at the declared number type (the array
|
|
3340
3248
|
// .length rule). Chain-handled reads stay jsval.
|
|
@@ -3387,7 +3295,21 @@ export function lowerIntrinsicProperty(lowerer, expr) {
|
|
|
3387
3295
|
return null;
|
|
3388
3296
|
}
|
|
3389
3297
|
if (name === "length") {
|
|
3390
|
-
const
|
|
3298
|
+
const expected = kind === "string"
|
|
3299
|
+
? STRING
|
|
3300
|
+
: lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
|
3301
|
+
let receiver = expected && (expected.kind === "string" || expected.kind === "array")
|
|
3302
|
+
? strictReceiver(expected)
|
|
3303
|
+
: lowerer.lowerExpr(expr.expression);
|
|
3304
|
+
if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 && kind === "array") {
|
|
3305
|
+
const present = lowerer.stripUndefinedArm(receiver.type);
|
|
3306
|
+
const helper = present.kind === "array"
|
|
3307
|
+
? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(expr.expression))
|
|
3308
|
+
: null;
|
|
3309
|
+
receiver = helper
|
|
3310
|
+
? { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(expr.expression) }
|
|
3311
|
+
: lowerer.maybeNarrow(receiver, expr.expression);
|
|
3312
|
+
}
|
|
3391
3313
|
// An island handle behind an array-typed .d.ts surface
|
|
3392
3314
|
// (`parts().length` on a declared `string[]` return — arrays never
|
|
3393
3315
|
// exit eagerly, so the value stays jsval): an engine property read,
|
|
@@ -3516,6 +3438,27 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
3516
3438
|
}
|
|
3517
3439
|
}
|
|
3518
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
|
+
}
|
|
3519
3462
|
// An EMPTY literal under a CONST ASSERTION whose type queries panicked
|
|
3520
3463
|
// (tsgo's readonly-[] TupleType conversion — the facade answers `any`,
|
|
3521
3464
|
// which maps to nothing statically and to an island value under
|
|
@@ -3598,7 +3541,24 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
3598
3541
|
lowerer.badType(expr, tsType);
|
|
3599
3542
|
}
|
|
3600
3543
|
const type = mapped;
|
|
3544
|
+
// Keep the array payload type fixed. Optional reads are stored through
|
|
3545
|
+
// arrayValueStore, which records UNDEFINED in the state byte while the
|
|
3546
|
+
// payload remains number/string/etc.; widening the array element here
|
|
3547
|
+
// would force boxed union storage and change aliases.
|
|
3601
3548
|
const spreads = [];
|
|
3549
|
+
const holes = new Set();
|
|
3550
|
+
const runtimeOptionalElements = new Set();
|
|
3551
|
+
let needsStateBuild = false;
|
|
3552
|
+
for (let i = 0; i < expr.elements.length; i++) {
|
|
3553
|
+
const candidate = expr.elements[i];
|
|
3554
|
+
if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate))
|
|
3555
|
+
continue;
|
|
3556
|
+
const probe = tryLowerExpression(lowerer, candidate);
|
|
3557
|
+
if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) {
|
|
3558
|
+
runtimeOptionalElements.add(i);
|
|
3559
|
+
needsStateBuild = true;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3602
3562
|
const elems = expr.elements.map((el, i) => {
|
|
3603
3563
|
if (ts.isSpreadElement(el)) {
|
|
3604
3564
|
// `[...xs, b]`: xs must be an array of the literal's own element
|
|
@@ -3664,1807 +3624,166 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
|
|
|
3664
3624
|
// (string | symbol)[] literal — per-element wrap/width copy):
|
|
3665
3625
|
// the interned width helper reshapes before the spread copies.
|
|
3666
3626
|
if (src.type.kind === "array" && !typeEquals(src.type, type)) {
|
|
3667
|
-
const w = lowerer.widthCoerce(src, type);
|
|
3668
|
-
if (w)
|
|
3669
|
-
src = w;
|
|
3670
|
-
}
|
|
3671
|
-
if (!typeEquals(src.type, type)) {
|
|
3672
|
-
lowerer.unsupported("SC1090", el, `spreading '${lowerer.fmt(src.type)}' into a '${lowerer.fmt(type)}' literal (only a same-element-type array spreads)`);
|
|
3673
|
-
}
|
|
3674
|
-
spreads.push(i);
|
|
3675
|
-
return src;
|
|
3676
|
-
}
|
|
3677
|
-
// A HOLE (`[,]` — an elision): the slot materializes the undefined
|
|
3678
|
-
// arm — reads, length, and JSON answer exactly Node (JSON.stringify
|
|
3679
|
-
// prints null for holes AND for undefined elements). Iteration
|
|
3680
|
-
// methods do not skip the position the way JS skips holes — the
|
|
3681
|
-
// documented sparse-literal divergence.
|
|
3682
|
-
if (ts.isOmittedExpression(el)) {
|
|
3683
|
-
const hole = type.elem.kind === "union" ? lowerer.wrappedUndefined(type.elem, locOf(el)) : null;
|
|
3684
|
-
if (hole)
|
|
3685
|
-
return hole;
|
|
3686
|
-
lowerer.unsupported("SC1090", el, "holes in array literals of this element type");
|
|
3687
|
-
}
|
|
3688
|
-
// An ARRAY-LITERAL element under a UNION element slot whose own type
|
|
3689
|
-
// has no lift into the slot builds against the union's single array
|
|
3690
|
-
// arm instead (`['pwd', []]` as (string | string[])[] — tsc pushes no
|
|
3691
|
-
// contextual type through the unannotated chain, so the empty nested
|
|
3692
|
-
// literal types never[], whose f64-element representation re-tags
|
|
3693
|
-
// into nothing). One array arm is unambiguous — the union-slot rule's
|
|
3694
|
-
// stance; several keep the fence.
|
|
3695
|
-
if (type.elem.kind === "union" || type.elem.kind === "array") {
|
|
3696
|
-
let x = el;
|
|
3697
|
-
while (ts.isParenthesizedExpression(x))
|
|
3698
|
-
x = x.expression;
|
|
3699
|
-
if (ts.isArrayLiteralExpression(x)) {
|
|
3700
|
-
const ownT = lowerer.mapTypeOf(lowerer.checker.getContextualType(x) ?? lowerer.typeOf(x));
|
|
3701
|
-
if (ownT === null || lowerer.widthLiftPlan(ownT, type.elem) === null) {
|
|
3702
|
-
// An ARRAY-typed slot takes the literal directly; a union slot
|
|
3703
|
-
// routes through its single array arm and wraps.
|
|
3704
|
-
if (type.elem.kind === "array") {
|
|
3705
|
-
return lowerer.lowerArrayLiteral(x, type.elem);
|
|
3706
|
-
}
|
|
3707
|
-
const def = lowerer.unions.get(type.elem.unionId);
|
|
3708
|
-
const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
|
|
3709
|
-
if (arrayArms.length === 1) {
|
|
3710
|
-
const built = lowerer.lowerArrayLiteral(x, arrayArms[0]);
|
|
3711
|
-
return lowerer.coerceInto(el, built, type.elem);
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
}
|
|
3716
|
-
// Elements flow into the element slot like an assignment: union
|
|
3717
|
-
// elements wrap plain arm values (`[1, "a"]` as (number | string)[]),
|
|
3718
|
-
// holes reject inside lowerExpr.
|
|
3719
|
-
const lowered = lowerer.lowerExprExpecting(el, type.elem);
|
|
3720
|
-
if (!typeEquals(lowered.type, type.elem))
|
|
3721
|
-
lowerer.badType(el, lowerer.typeOf(el));
|
|
3722
|
-
return lowered;
|
|
3723
|
-
});
|
|
3724
|
-
return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
|
|
3725
|
-
}
|
|
3726
|
-
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
|
|
3727
|
-
* contextual type when tsc has one (annotated declarations, arguments,
|
|
3728
|
-
* nested literals) and from the literal's own type otherwise — both intern
|
|
3729
|
-
* to the same shapeId unless width subtyping is in play (an `as` cast can
|
|
3730
|
-
* smuggle a wider/narrower literal past tsc's freshness check), which the
|
|
3731
|
-
* exact-shape checks below reject with SC2002. Fields lower IN SOURCE
|
|
3732
|
-
* ORDER: JS evaluates property values in source order. */
|
|
3733
|
-
/** The dropped-field names of the PromiseSettledResult honest subset
|
|
3734
|
-
* (SEMANTICS.md 46): when the literal's target type is (a union
|
|
3735
|
-
* containing) the lib's PromiseFulfilledResult / PromiseRejectedResult,
|
|
3736
|
-
* the record mapping kept only `status` — `value` and `reason` evaluate
|
|
3737
|
-
* for effect and are not stored. Null for every other target. */
|
|
3738
|
-
function settledDropNames(lowerer, tsType) {
|
|
3739
|
-
let names = null;
|
|
3740
|
-
const parts = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
|
|
3741
|
-
for (const part of parts) {
|
|
3742
|
-
const sym = part.getSymbol();
|
|
3743
|
-
if (!sym ||
|
|
3744
|
-
!lowerer.checker.declarationsOf(sym).some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))) {
|
|
3745
|
-
continue;
|
|
3746
|
-
}
|
|
3747
|
-
if (sym.name === "PromiseFulfilledResult")
|
|
3748
|
-
(names ??= new Set()).add("value");
|
|
3749
|
-
if (sym.name === "PromiseRejectedResult")
|
|
3750
|
-
(names ??= new Set()).add("reason");
|
|
3751
|
-
}
|
|
3752
|
-
return names;
|
|
3753
|
-
}
|
|
3754
|
-
/** `{ [KEY]: v }` where KEY is a compile-time-known string or number:
|
|
3755
|
-
* the key folds into an ordinary (spelled) property name — the record
|
|
3756
|
-
* shape is exactly what tsc computed for the literal (the late-bound
|
|
3757
|
-
* name IS the checker's literal type), so the fold is just spelling.
|
|
3758
|
-
* PURE key forms only, so skipping the key's evaluation is exact (JS
|
|
3759
|
-
* evaluates the key before the value; effectful expressions keep the
|
|
3760
|
-
* fence): any side-effect-free expression — identifiers, property
|
|
3761
|
-
* chains with no accessor on them (enum members, `other.name` reads),
|
|
3762
|
-
* literals, parenthesized/as-wrapped forms, operators over those —
|
|
3763
|
-
* whose checker type is ONE string or number literal (or a union whose
|
|
3764
|
-
* arms all spell the same name — `E1.x || E2.x` where both are 0).
|
|
3765
|
-
* Number keys take JS's canonical spelling (`{ [E.member]: v }` stores
|
|
3766
|
-
* "0" — ToPropertyKey), exactly the name tsc late-bound. Templates
|
|
3767
|
-
* fold structurally span by span as well (a template of literals whose
|
|
3768
|
-
* checker type widened still spells one string). Symbol-typed and
|
|
3769
|
-
* runtime-valued keys stay out. */
|
|
3770
|
-
function literalComputedKey(lowerer, name) {
|
|
3771
|
-
return foldedStringKeyOf(lowerer, name.expression);
|
|
3772
|
-
}
|
|
3773
|
-
export function foldedStringKeyOf(lowerer, expr) {
|
|
3774
|
-
if (ts.isParenthesizedExpression(expr))
|
|
3775
|
-
return foldedStringKeyOf(lowerer, expr.expression);
|
|
3776
|
-
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
|
|
3777
|
-
return expr.text;
|
|
3778
|
-
if (ts.isTemplateExpression(expr)) {
|
|
3779
|
-
// Structural fold first — exact even where the checker widened the
|
|
3780
|
-
// template's own type; the general type-directed fold below is the
|
|
3781
|
-
// fallback (a template the checker DID type as one literal).
|
|
3782
|
-
let out = expr.head.text;
|
|
3783
|
-
let folded = true;
|
|
3784
|
-
for (const span of expr.templateSpans) {
|
|
3785
|
-
const part = foldedStringKeyOf(lowerer, span.expression);
|
|
3786
|
-
if (part === null) {
|
|
3787
|
-
folded = false;
|
|
3788
|
-
break;
|
|
3789
|
-
}
|
|
3790
|
-
out += part + span.literal.text;
|
|
3791
|
-
}
|
|
3792
|
-
if (folded)
|
|
3793
|
-
return out;
|
|
3794
|
-
}
|
|
3795
|
-
// The general fold: a PURE expression whose checker type spells one
|
|
3796
|
-
// property name. An as-cast folds by its OWN checker type — that is
|
|
3797
|
-
// the name tsc late-bound the property under, so shape and storage
|
|
3798
|
-
// stay one name (the standard trust-the-checker bet); a cast that
|
|
3799
|
-
// WIDENS to string un-late-binds the property, its type is no literal,
|
|
3800
|
-
// and the fence stays.
|
|
3801
|
-
if (!pureKeyExpr(lowerer, expr))
|
|
3802
|
-
return null;
|
|
3803
|
-
return literalKeySpellingOf(lowerer.typeOf(expr));
|
|
3804
|
-
}
|
|
3805
|
-
/** The property-name spelling of a single-literal checker type: string
|
|
3806
|
-
* literals directly, number literals in JS's canonical ToString spelling
|
|
3807
|
-
* (enum members included — their literal flags carry the value), and
|
|
3808
|
-
* unions whose arms all spell the SAME name (`E1.x || E2.x` — two enum
|
|
3809
|
-
* literal types, one value). Null for everything else. */
|
|
3810
|
-
function literalKeySpellingOf(t) {
|
|
3811
|
-
if (t.isStringLiteralType())
|
|
3812
|
-
return t.value;
|
|
3813
|
-
if (t.isNumberLiteralType())
|
|
3814
|
-
return String(t.value);
|
|
3815
|
-
if (t.isUnionType()) {
|
|
3816
|
-
let out = null;
|
|
3817
|
-
for (const arm of ts.constituentTypes(t)) {
|
|
3818
|
-
const s = arm.isStringLiteralType() ? arm.value : arm.isNumberLiteralType() ? String(arm.value) : null;
|
|
3819
|
-
if (s === null || (out !== null && s !== out))
|
|
3820
|
-
return null;
|
|
3821
|
-
out = s;
|
|
3822
|
-
}
|
|
3823
|
-
return out;
|
|
3824
|
-
}
|
|
3825
|
-
return null;
|
|
3826
|
-
}
|
|
3827
|
-
/** True iff evaluating `expr` can have no observable effect, so a fold
|
|
3828
|
-
* may skip it: literals, identifier reads, property chains whose every
|
|
3829
|
-
* member is a plain field/enum member (an accessor anywhere on the chain
|
|
3830
|
-
* is a call), operators and casts over those. Calls, `new`, assignments,
|
|
3831
|
-
* and element accesses stay impure. */
|
|
3832
|
-
function pureKeyExpr(lowerer, expr) {
|
|
3833
|
-
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr))
|
|
3834
|
-
return pureKeyExpr(lowerer, expr.expression);
|
|
3835
|
-
if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr))
|
|
3836
|
-
return pureKeyExpr(lowerer, expr.expression);
|
|
3837
|
-
if (ts.isIdentifier(expr))
|
|
3838
|
-
return true;
|
|
3839
|
-
if (ts.isStringLiteralLike(expr) || ts.isNumericLiteral(expr))
|
|
3840
|
-
return true;
|
|
3841
|
-
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword ||
|
|
3842
|
-
expr.kind === ts.SyntaxKind.NullKeyword)
|
|
3843
|
-
return true;
|
|
3844
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
3845
|
-
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
|
|
3846
|
-
if (sym && sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))
|
|
3847
|
-
return false;
|
|
3848
|
-
return pureKeyExpr(lowerer, expr.expression);
|
|
3849
|
-
}
|
|
3850
|
-
if (ts.isPrefixUnaryExpression(expr) &&
|
|
3851
|
-
(expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken ||
|
|
3852
|
-
expr.operator === ts.SyntaxKind.ExclamationToken || expr.operator === ts.SyntaxKind.TildeToken)) {
|
|
3853
|
-
return pureKeyExpr(lowerer, expr.operand);
|
|
3854
|
-
}
|
|
3855
|
-
if (ts.isBinaryExpression(expr)) {
|
|
3856
|
-
const op = expr.operatorToken.kind;
|
|
3857
|
-
const pureOp = op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken ||
|
|
3858
|
-
op === ts.SyntaxKind.QuestionQuestionToken ||
|
|
3859
|
-
(op >= ts.SyntaxKind.LessThanToken && op <= ts.SyntaxKind.CaretToken &&
|
|
3860
|
-
op !== ts.SyntaxKind.InstanceOfKeyword && op !== ts.SyntaxKind.InKeyword);
|
|
3861
|
-
return pureOp && pureKeyExpr(lowerer, expr.left) && pureKeyExpr(lowerer, expr.right);
|
|
3862
|
-
}
|
|
3863
|
-
if (ts.isConditionalExpression(expr)) {
|
|
3864
|
-
return pureKeyExpr(lowerer, expr.condition) && pureKeyExpr(lowerer, expr.whenTrue) && pureKeyExpr(lowerer, expr.whenFalse);
|
|
3865
|
-
}
|
|
3866
|
-
if (ts.isTemplateExpression(expr))
|
|
3867
|
-
return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression));
|
|
3868
|
-
return false;
|
|
3869
|
-
}
|
|
3870
|
-
/** A property's field name: identifier/string-literal keys directly,
|
|
3871
|
-
* foldable computed keys through literalComputedKey (the syntax pass
|
|
3872
|
-
* fenced everything else). */
|
|
3873
|
-
function propNameText(lowerer, name) {
|
|
3874
|
-
if (ts.isComputedPropertyName(name)) {
|
|
3875
|
-
const k = literalComputedKey(lowerer, name);
|
|
3876
|
-
if (k === null)
|
|
3877
|
-
throw new InternalCompilerError("lowerer bug: unfoldable computed key past the fence");
|
|
3878
|
-
return k;
|
|
3879
|
-
}
|
|
3880
|
-
// Numeric literals name their canonical string key (JS's ToPropertyKey
|
|
3881
|
-
// — the same name the checker gives the property symbol).
|
|
3882
|
-
if (ts.isNumericLiteral(name))
|
|
3883
|
-
return String(Number(name.text));
|
|
3884
|
-
return name.text;
|
|
3885
|
-
}
|
|
3886
|
-
/** The runtime-keyed JS object literal (a computed key that doesn't fold):
|
|
3887
|
-
* builds a dyn object member-by-member. Keys evaluate before their values,
|
|
3888
|
-
* properties in source order — JS's object-literal evaluation exactly.
|
|
3889
|
-
* Identifier/string keys are compile-time strings; numeric keys take
|
|
3890
|
-
* their canonical number string (`{ 0x10: v }` stores "16" — JS's
|
|
3891
|
-
* ToPropertyKey); computed keys evaluate and pass through ToString (the
|
|
3892
|
-
* dyn's String() for dyn operands — `{ [field]: v }` where field is a
|
|
3893
|
-
* checked-dynamic param). Values convert through the usual dyn boundary
|
|
3894
|
-
* (dynFrom's domain, functions box); a value with no dyn representation
|
|
3895
|
-
* fences per property. Spreads copy through dyn.assign in source order;
|
|
3896
|
-
* accessors stay fenced. */
|
|
3897
|
-
export function lowerDynObjectLiteral(lowerer, expr, boxValue) {
|
|
3898
|
-
const loc = locOf(expr);
|
|
3899
|
-
let fields = [];
|
|
3900
|
-
let acc = null;
|
|
3901
|
-
const flushFields = () => {
|
|
3902
|
-
if (fields.length === 0)
|
|
3903
|
-
return;
|
|
3904
|
-
const chunk = { kind: "dynObjLit", fields, type: DYN, loc };
|
|
3905
|
-
acc = acc === null
|
|
3906
|
-
? chunk
|
|
3907
|
-
: { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc };
|
|
3908
|
-
fields = [];
|
|
3909
|
-
};
|
|
3910
|
-
for (const prop of expr.properties) {
|
|
3911
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
3912
|
-
flushFields();
|
|
3913
|
-
const raw = lowerer.lowerExpr(prop.expression);
|
|
3914
|
-
const source = boxValue
|
|
3915
|
-
? boxValue(prop.expression, raw)
|
|
3916
|
-
: lowerer.coerceToExpected(raw, DYN);
|
|
3917
|
-
if (source.type.kind !== "dyn") {
|
|
3918
|
-
lowerer.unsupported("SC1101", prop.expression, `spreading '${lowerer.fmt(source.type)}' into a checked-dynamic object literal`);
|
|
3919
|
-
}
|
|
3920
|
-
acc ??= { kind: "dynObjLit", fields: [], type: DYN, loc };
|
|
3921
|
-
acc = { kind: "libCall", fn: "dyn.assign", args: [acc, source], type: DYN, loc: locOf(prop) };
|
|
3922
|
-
continue;
|
|
3923
|
-
}
|
|
3924
|
-
if (!ts.isPropertyAssignment(prop) &&
|
|
3925
|
-
!ts.isShorthandPropertyAssignment(prop) &&
|
|
3926
|
-
!ts.isMethodDeclaration(prop)) {
|
|
3927
|
-
lowerer.unsupported("SC1090", prop, "accessors in a runtime-keyed (computed-key) object literal");
|
|
3928
|
-
}
|
|
3929
|
-
const name = prop.name;
|
|
3930
|
-
let key;
|
|
3931
|
-
if (ts.isComputedPropertyName(name)) {
|
|
3932
|
-
const folded = literalComputedKey(lowerer, name);
|
|
3933
|
-
if (folded !== null) {
|
|
3934
|
-
key = { kind: "strLit", value: folded, type: STRING, loc: locOf(name) };
|
|
3935
|
-
}
|
|
3936
|
-
else {
|
|
3937
|
-
let k = lowerer.lowerExpr(name.expression);
|
|
3938
|
-
if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
|
|
3939
|
-
k = { kind: "toString", operand: k, type: STRING, loc: locOf(name) };
|
|
3940
|
-
}
|
|
3941
|
-
if (k.type.kind !== "string") {
|
|
3942
|
-
lowerer.unsupported("SC1090", name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
|
|
3943
|
-
}
|
|
3944
|
-
key = k;
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
else if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
3948
|
-
key = { kind: "strLit", value: name.text, type: STRING, loc: locOf(name) };
|
|
3949
|
-
}
|
|
3950
|
-
else if (ts.isNumericLiteral(name)) {
|
|
3951
|
-
key = { kind: "strLit", value: String(Number(name.text)), type: STRING, loc: locOf(name) };
|
|
3952
|
-
}
|
|
3953
|
-
else {
|
|
3954
|
-
lowerer.unsupported("SC1090", prop, "non-identifier property names");
|
|
3955
|
-
}
|
|
3956
|
-
const valueExpr = ts.isMethodDeclaration(prop)
|
|
3957
|
-
? prop
|
|
3958
|
-
: ts.isShorthandPropertyAssignment(prop)
|
|
3959
|
-
? prop.name
|
|
3960
|
-
: prop.initializer;
|
|
3961
|
-
// Lambda values that fail to lower become trap closures (the
|
|
3962
|
-
// per-call fence granularity — fenceClosureProbe) so the object
|
|
3963
|
-
// still builds; everything else keeps the per-property fence below.
|
|
3964
|
-
let raw;
|
|
3965
|
-
const propDiagsBefore = lowerer.diags.length;
|
|
3966
|
-
try {
|
|
3967
|
-
const lowerValue = () => ts.isMethodDeclaration(prop)
|
|
3968
|
-
? (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop))
|
|
3969
|
-
: lowerer.lowerExpr(valueExpr);
|
|
3970
|
-
raw =
|
|
3971
|
-
fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
|
|
3972
|
-
lowerValue();
|
|
3973
|
-
}
|
|
3974
|
-
catch (err) {
|
|
3975
|
-
// A PURE member read a JS file cannot lower (a namespace object
|
|
3976
|
-
// in an export aggregate): the slot takes a boxed fence closure —
|
|
3977
|
-
// the diagnostics defer to the runtime-fence ledger, the object
|
|
3978
|
-
// builds, and only USING the member stops the run.
|
|
3979
|
-
const pureMember = ts.isIdentifier(valueExpr);
|
|
3980
|
-
if (!(err instanceof PoisonError) || !pureMember)
|
|
3981
|
-
throw err;
|
|
3982
|
-
const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, {
|
|
3983
|
-
kind: "closure",
|
|
3984
|
-
name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
|
|
3985
|
-
returnType: VOID,
|
|
3986
|
-
type: { kind: "func", params: [], ret: VOID },
|
|
3987
|
-
});
|
|
3988
|
-
if (!fence)
|
|
3989
|
-
throw err;
|
|
3990
|
-
raw = fence;
|
|
3991
|
-
}
|
|
3992
|
-
let v = boxValue
|
|
3993
|
-
? boxValue(valueExpr, raw)
|
|
3994
|
-
: lowerer.coerceToExpected(raw, DYN);
|
|
3995
|
-
if (v.type.kind !== "dyn") {
|
|
3996
|
-
const convDiagsBefore = lowerer.diags.length;
|
|
3997
|
-
try {
|
|
3998
|
-
lowerer.unsupported("SC1101", valueExpr, `holding '${lowerer.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
|
|
3999
|
-
}
|
|
4000
|
-
catch (err) {
|
|
4001
|
-
// A member value the checked-dynamic tree cannot hold — a func whose signature
|
|
4002
|
-
// cannot box, an island ('any') handle, a record carrying func
|
|
4003
|
-
// fields (the export aggregate's typed utilities and npm-handle
|
|
4004
|
-
// members): the same call-time fence deferral as an unlowerable
|
|
4005
|
-
// member — the slot takes a boxed fence closure; only USING it
|
|
4006
|
-
// stops the run. Probe mode and ICEs keep the poison.
|
|
4007
|
-
if (!(err instanceof PoisonError))
|
|
4008
|
-
throw err;
|
|
4009
|
-
const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, {
|
|
4010
|
-
kind: "closure",
|
|
4011
|
-
name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
|
|
4012
|
-
returnType: VOID,
|
|
4013
|
-
type: { kind: "func", params: [], ret: VOID },
|
|
4014
|
-
});
|
|
4015
|
-
if (!fence)
|
|
4016
|
-
throw err;
|
|
4017
|
-
v = lowerer.coerceToExpected(fence, DYN);
|
|
4018
|
-
}
|
|
4019
|
-
}
|
|
4020
|
-
fields.push({ key, value: v });
|
|
4021
|
-
}
|
|
4022
|
-
flushFields();
|
|
4023
|
-
return acc ?? { kind: "dynObjLit", fields: [], type: DYN, loc };
|
|
4024
|
-
}
|
|
4025
|
-
/** Spreading a source whose shape carries accessor slots: Node's copy
|
|
4026
|
-
* invokes each getter exactly once in insertion order — even for keys a
|
|
4027
|
-
* later contributor overrides — while the field-copy desugar assumes pure
|
|
4028
|
-
* reads it may drop or reorder. Getter-call counts would silently diverge,
|
|
4029
|
-
* so accessor-carrying sources fence by name. */
|
|
4030
|
-
function fenceAccessorSpreadSource(lowerer, prop, srcShape) {
|
|
4031
|
-
if (srcShape && shapeHasAccessorSlots(srcShape)) {
|
|
4032
|
-
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)");
|
|
4033
|
-
}
|
|
4034
|
-
}
|
|
4035
|
-
/** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
|
|
4036
|
-
* lambda whose body fails to lower inside a JS object literal becomes a
|
|
4037
|
-
* closure of the field's exact func type whose body is the runtime fence —
|
|
4038
|
-
* the object still BUILDS (commander's `_outputConfiguration` table: the
|
|
4039
|
-
* driven writeOut/writeErr entries work; an unloweraable color probe traps
|
|
4040
|
-
* only if something CALLS it). The failed lambda's diagnostics move to the
|
|
4041
|
-
* runtime-fence inventory, exactly like a fenced JS statement. Null when
|
|
4042
|
-
* the fallback does not apply (not a JS file, not a lambda, no func slot)
|
|
4043
|
-
* — the caller lowers normally. ICEs (SC9001) never convert. */
|
|
4044
|
-
function fenceClosureProbe(lowerer, node, slotType, attempt) {
|
|
4045
|
-
if (!isJsSourceFile(node.getSourceFile()))
|
|
4046
|
-
return null;
|
|
4047
|
-
let inner = node;
|
|
4048
|
-
while (ts.isParenthesizedExpression(inner))
|
|
4049
|
-
inner = inner.expression;
|
|
4050
|
-
if (!ts.isArrowFunction(inner) && !ts.isFunctionExpression(inner) && !ts.isMethodDeclaration(inner)) {
|
|
4051
|
-
return null;
|
|
4052
|
-
}
|
|
4053
|
-
// The trap's ABI: the field's func slot when one exists; otherwise (the
|
|
4054
|
-
// dyn-object literal path — the value boxes into dyn) the all-dyn
|
|
4055
|
-
// signature of the lambda's own arity, which canBoxFuncIntoDyn always
|
|
4056
|
-
// admits.
|
|
4057
|
-
const fieldType = slotType !== undefined && slotType.kind === "func"
|
|
4058
|
-
? slotType
|
|
4059
|
-
: { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN };
|
|
4060
|
-
if (slotType !== undefined && slotType.kind !== "func")
|
|
4061
|
-
return null;
|
|
4062
|
-
const diagsBefore = lowerer.diags.length;
|
|
4063
|
-
try {
|
|
4064
|
-
return attempt();
|
|
4065
|
-
}
|
|
4066
|
-
catch (e) {
|
|
4067
|
-
if (!(e instanceof PoisonError))
|
|
4068
|
-
throw e;
|
|
4069
|
-
const params = fieldType.params.map((t, i) => ({ localId: `p.${i}`, name: `p${i}`, type: t }));
|
|
4070
|
-
const fence = lowerer.deferToRuntimeFence(diagsBefore, node, {
|
|
4071
|
-
kind: "closure",
|
|
4072
|
-
name: () => `%fence.fn.${lowerer.liftedFns.length}`,
|
|
4073
|
-
params,
|
|
4074
|
-
returnType: fieldType.ret,
|
|
4075
|
-
paramsMutable: true,
|
|
4076
|
-
type: fieldType,
|
|
4077
|
-
fallback: { code: "SC1090", message: "this function's body has no static lowering" },
|
|
4078
|
-
bareMessage: true,
|
|
4079
|
-
allowDiagSink: true,
|
|
4080
|
-
});
|
|
4081
|
-
if (!fence)
|
|
4082
|
-
throw e;
|
|
4083
|
-
return fence;
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
/** The union arm an object LITERAL inhabits when its fields must widen
|
|
4087
|
-
* PER FIELD into exactly one record arm — the reducer-action pattern
|
|
4088
|
-
* (`{ kind: "a", parsed: 1 }` into `{ kind: "a"; parsed: number | null }
|
|
4089
|
-
* | { kind: "b" }`). The IR shapes erased the literal types tsc
|
|
4090
|
-
* discriminated on, so the probe runs against the contextual union's
|
|
4091
|
-
* CHECKER members: every literal field must exist on the member
|
|
4092
|
-
* (excess-property freshness — tsc already enforced it), every arm field
|
|
4093
|
-
* missing from the literal must be optional-flavored (an undefined-armed
|
|
4094
|
-
* union, or a dyn slot — the absent-completion rule), and each present
|
|
4095
|
-
* field's LITERAL type must fit the member's field type — literal against
|
|
4096
|
-
* literal decides by VALUE (the discriminant), everything else by the
|
|
4097
|
-
* widened IR pair under the width-lift relation. Exactly ONE fitting arm
|
|
4098
|
-
* answers it; zero or several answer null and the caller keeps its
|
|
4099
|
-
* fences. Plain property-assignment/shorthand literals only — spreads,
|
|
4100
|
-
* accessors, methods, and unfoldable computed keys keep their own paths. */
|
|
4101
|
-
function literalUnionArmOf(lowerer, expr, tsType, recordArms) {
|
|
4102
|
-
if (!tsType.isUnionType())
|
|
4103
|
-
return null;
|
|
4104
|
-
const props = [];
|
|
4105
|
-
for (const p of expr.properties) {
|
|
4106
|
-
if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
|
|
4107
|
-
props.push({ name: propNameText(lowerer, p.name), node: p.initializer });
|
|
4108
|
-
}
|
|
4109
|
-
else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) {
|
|
4110
|
-
props.push({ name: p.name.text, node: p.name });
|
|
4111
|
-
}
|
|
4112
|
-
else {
|
|
4113
|
-
return null;
|
|
4114
|
-
}
|
|
4115
|
-
}
|
|
4116
|
-
/** litT fits ftT: unions per arm; literal-vs-literal by value; unit
|
|
4117
|
-
* types only into their own unit; otherwise the widened IR pair must be
|
|
4118
|
-
* equal or width-liftable. */
|
|
4119
|
-
const fits = (litT, ftT) => {
|
|
4120
|
-
if (ftT.isUnionType())
|
|
4121
|
-
return ts.constituentTypes(ftT).some((a) => fits(litT, a));
|
|
4122
|
-
if (ftT.isStringLiteralType())
|
|
4123
|
-
return litT.isStringLiteralType() && litT.value === ftT.value;
|
|
4124
|
-
if (ftT.isNumberLiteralType())
|
|
4125
|
-
return litT.isNumberLiteralType() && litT.value === ftT.value;
|
|
4126
|
-
if (ftT.flags & ts.TypeFlags.BooleanLiteral) {
|
|
4127
|
-
return (litT.flags & ts.TypeFlags.BooleanLiteral) !== 0 && lowerer.checker.typeToString(litT) === lowerer.checker.typeToString(ftT);
|
|
4128
|
-
}
|
|
4129
|
-
if (ftT.flags & ts.TypeFlags.Null)
|
|
4130
|
-
return (litT.flags & ts.TypeFlags.Null) !== 0;
|
|
4131
|
-
if (ftT.flags & ts.TypeFlags.Undefined)
|
|
4132
|
-
return (litT.flags & ts.TypeFlags.Undefined) !== 0;
|
|
4133
|
-
if (litT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined))
|
|
4134
|
-
return false;
|
|
4135
|
-
const li = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(litT));
|
|
4136
|
-
const fi = lowerer.mapTypeOf(ftT);
|
|
4137
|
-
if (!li || !fi)
|
|
4138
|
-
return false;
|
|
4139
|
-
return typeEquals(li, fi) || lowerer.widthLiftPlan(li, fi) !== null;
|
|
4140
|
-
};
|
|
4141
|
-
const armShapeIds = new Set(recordArms.map((a) => a.shapeId));
|
|
4142
|
-
const candidates = new Set();
|
|
4143
|
-
for (const member of ts.constituentTypes(tsType)) {
|
|
4144
|
-
const mMapped = lowerer.mapTypeOf(member);
|
|
4145
|
-
if (mMapped?.kind !== "record" || !armShapeIds.has(mMapped.shapeId) || candidates.has(mMapped.shapeId))
|
|
4146
|
-
continue;
|
|
4147
|
-
const shape = lowerer.shapes.get(mMapped.shapeId);
|
|
4148
|
-
if (!shape || shape.tuple || shape.indexValue)
|
|
4149
|
-
continue;
|
|
4150
|
-
// Excess-property freshness: every literal field must exist on the
|
|
4151
|
-
// member (tsc rejected the others for a fresh literal).
|
|
4152
|
-
if (!props.every((p) => lowerer.checker.getPropertyOfType(member, p.name) !== undefined))
|
|
4153
|
-
continue;
|
|
4154
|
-
// Arm fields the literal leaves unset must be optional-flavored (the
|
|
4155
|
-
// absent-completion rule: an undefined-armed union or a dyn slot).
|
|
4156
|
-
const names = new Set(props.map((p) => p.name));
|
|
4157
|
-
const absentOk = shape.fields.every((f) => {
|
|
4158
|
-
if (names.has(f.name))
|
|
4159
|
-
return true;
|
|
4160
|
-
if (f.type.kind === "dyn")
|
|
4161
|
-
return true;
|
|
4162
|
-
if (f.type.kind !== "union")
|
|
4163
|
-
return false;
|
|
4164
|
-
return lowerer.unions.get(f.type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false;
|
|
4165
|
-
});
|
|
4166
|
-
if (!absentOk)
|
|
4167
|
-
continue;
|
|
4168
|
-
const fieldsFit = props.every((p) => {
|
|
4169
|
-
const sym = lowerer.checker.getPropertyOfType(member, p.name);
|
|
4170
|
-
if (!sym)
|
|
4171
|
-
return false;
|
|
4172
|
-
return fits(lowerer.typeOf(p.node), lowerer.checker.getTypeOfSymbol(sym));
|
|
4173
|
-
});
|
|
4174
|
-
if (fieldsFit)
|
|
4175
|
-
candidates.add(mMapped.shapeId);
|
|
4176
|
-
}
|
|
4177
|
-
if (candidates.size !== 1)
|
|
4178
|
-
return null;
|
|
4179
|
-
const [only] = candidates;
|
|
4180
|
-
return recordArms.find((a) => a.shapeId === only) ?? null;
|
|
4181
|
-
}
|
|
4182
|
-
export function lowerObjectLiteral(lowerer, expr) {
|
|
4183
|
-
const loc = locOf(expr);
|
|
4184
|
-
// The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
|
|
4185
|
-
// compile-time string means the literal's shape is not a compile-time
|
|
4186
|
-
// fact — no record shape can hold it. The whole literal builds as a dyn
|
|
4187
|
-
// object instead (`{ [field]: criteria, actual: 0, ... }` —
|
|
4188
|
-
// test/common's _mustCallInner context), where keys are runtime string
|
|
4189
|
-
// values. TypeScript keeps the record world and its fence: this shape
|
|
4190
|
-
// only arises in checked-dynamic JS.
|
|
4191
|
-
if (isJsSourceFile(expr.getSourceFile()) &&
|
|
4192
|
-
expr.properties.some((p) => {
|
|
4193
|
-
const n = ts.isSpreadAssignment(p) ? undefined : p.name;
|
|
4194
|
-
return n !== undefined && ts.isComputedPropertyName(n) && literalComputedKey(lowerer, n) === null;
|
|
4195
|
-
})) {
|
|
4196
|
-
return lowerDynObjectLiteral(lowerer, expr);
|
|
4197
|
-
}
|
|
4198
|
-
// Syntax fence FIRST: unsupported member forms get their specific
|
|
4199
|
-
// message even when they also make the literal's type unmappable
|
|
4200
|
-
// (a getter or a `this`-returning method would otherwise surface as an
|
|
4201
|
-
// opaque type fence on the whole literal).
|
|
4202
|
-
for (const prop of expr.properties) {
|
|
4203
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
4204
|
-
// Supported shapes: FULL spreads of known record shapes read as
|
|
4205
|
-
// identifiers or other side-effect-free reads, all BEFORE any
|
|
4206
|
-
// explicit property (the field-copy desugar reads spread fields
|
|
4207
|
-
// first, exactly JS's eager copy order); and CONDITIONAL spreads
|
|
4208
|
-
// `...(c ? {k: v} : {})` — the optional-field idiom tsc types as
|
|
4209
|
-
// `k?: ...` — which desugar to one conditional field at the
|
|
4210
|
-
// spread's own position (any position: they introduce one fresh
|
|
4211
|
-
// name, collision-fenced below). Everything else stays fenced.
|
|
4212
|
-
const cs = conditionalSpreadOf(prop.expression);
|
|
4213
|
-
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
|
4214
|
-
lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)");
|
|
4215
|
-
}
|
|
4216
|
-
// Spread ORDER is fenced in the field-by-field desugar below, not
|
|
4217
|
-
// here: the index-signature merge path supports any order (keyed
|
|
4218
|
-
// last-write-wins writes), so `{ K: v, ...extra }` into a pure
|
|
4219
|
-
// Record shape compiles — the buildServiceEnv pattern.
|
|
4220
|
-
continue;
|
|
4221
|
-
}
|
|
4222
|
-
if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
|
|
4223
|
-
// JavaScript CJS-export tables reach reads through the lifted-
|
|
4224
|
-
// accessor path (cjsExportAccessorRead) — the literal VALUE
|
|
4225
|
-
// narrows to its plain fields below (accessor names are not
|
|
4226
|
-
// record storage; SEMANTICS.md documents the enumeration
|
|
4227
|
-
// divergence). TypeScript accessors lower into the shape's
|
|
4228
|
-
// reserved closure slots (%get:/%set: — see accessorSlotProp);
|
|
4229
|
-
// `this` in the body is fenced up front: the closure slot has no
|
|
4230
|
-
// receiver (capturing the record under construction would be an
|
|
4231
|
-
// RC cycle), and the generic lexical-this walk would silently
|
|
4232
|
-
// capture an ENCLOSING method's `this` — the object-method rule.
|
|
4233
|
-
if (!isJsSourceFile(expr.getSourceFile())) {
|
|
4234
|
-
rejectThisInObjectAccessor(lowerer, prop.body ?? prop);
|
|
4235
|
-
}
|
|
4236
|
-
}
|
|
4237
|
-
// Unfoldable computed keys are NOT fenced here: an index-signature
|
|
4238
|
-
// target lowers them as runtime keyed writes (the merge path below);
|
|
4239
|
-
// every other target re-fences them after the merge path declined
|
|
4240
|
-
// (fenceUnfoldableComputedKeys).
|
|
4241
|
-
// Identifier keys, STRING-LITERAL keys (`"content-type": v` —
|
|
4242
|
-
// record field names are data, never C identifiers; the mangler
|
|
4243
|
-
// encodes what C can't spell), NUMERIC-LITERAL keys in their
|
|
4244
|
-
// canonical string spelling (`{ 0: v }` stores "0", `{ 0x10: v }`
|
|
4245
|
-
// stores "16" — JS's ToPropertyKey; the checker names the property
|
|
4246
|
-
// symbol the same way), and FOLDABLE computed keys (above).
|
|
4247
|
-
if (!prop.name ||
|
|
4248
|
-
!(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ||
|
|
4249
|
-
ts.isNumericLiteral(prop.name) || ts.isComputedPropertyName(prop.name))) {
|
|
4250
|
-
lowerer.unsupported("SC1090", prop, "non-identifier property names");
|
|
4251
|
-
}
|
|
4252
|
-
// Shorthand methods are just closure-valued fields — but their `this`
|
|
4253
|
-
// is dynamically bound (typed as the literal by tsc, so noImplicitThis
|
|
4254
|
-
// lets it through) and records don't model it; the generic
|
|
4255
|
-
// lexical-this walk would silently capture an ENCLOSING method's
|
|
4256
|
-
// `this`, so any `this` in the body is rejected up front.
|
|
4257
|
-
if (ts.isMethodDeclaration(prop))
|
|
4258
|
-
lowerer.rejectThisInObjectMethod(prop.body ?? prop);
|
|
4259
|
-
}
|
|
4260
|
-
let tsType = lowerer.checker.getContextualType(expr) ?? lowerer.typeOf(expr);
|
|
4261
|
-
// `lit satisfies T` is TYPE-LEVEL only: the expression's checker type —
|
|
4262
|
-
// and therefore the shape every downstream consumer sees — is the
|
|
4263
|
-
// literal's OWN type (T still contextually types members, so inferred
|
|
4264
|
-
// parameter types flow). Building at T would reshape the value tsc
|
|
4265
|
-
// says has the literal's type: `{...} satisfies Movable &
|
|
4266
|
-
// Record<string, unknown>` must NOT become an index-signature record.
|
|
4267
|
-
// Own type wins whenever it maps to a record; an unmappable own type
|
|
4268
|
-
// keeps the contextual fallback (a bare-null field whose satisfies
|
|
4269
|
-
// target names the wider slot type).
|
|
4270
|
-
{
|
|
4271
|
-
let p = expr.parent;
|
|
4272
|
-
while (ts.isParenthesizedExpression(p))
|
|
4273
|
-
p = p.parent;
|
|
4274
|
-
if (ts.isSatisfiesExpression(p)) {
|
|
4275
|
-
const own = lowerer.typeOf(expr);
|
|
4276
|
-
if (lowerer.mapTypeOf(own)?.kind === "record")
|
|
4277
|
-
tsType = own;
|
|
4278
|
-
}
|
|
4279
|
-
}
|
|
4280
|
-
// An async function's return position types the literal
|
|
4281
|
-
// `T | PromiseLike<T>` (the lib's await-unwrapping contract). The
|
|
4282
|
-
// PromiseLike arm never maps, and the record the return slot actually
|
|
4283
|
-
// holds is exactly the checker's awaited type — strip to it BEFORE the
|
|
4284
|
-
// own-type fallback below: the awaited contextual type carries the
|
|
4285
|
-
// slot's field types (`lanIp: string | null`), which the literal's own
|
|
4286
|
-
// type narrows away (a field written as `lanIp: null` types as bare
|
|
4287
|
-
// `null`, which maps to nothing on its own).
|
|
4288
|
-
if (tsType.isUnionType() && ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike")) {
|
|
4289
|
-
tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
|
|
4290
|
-
}
|
|
4291
|
-
let mapped = lowerer.mapTypeOf(tsType);
|
|
4292
|
-
// An EMPTY-record context under a NON-empty literal (`Object.keys({
|
|
4293
|
-
// ...process.env })` — the lib's `{}`-typed parameters admit every
|
|
4294
|
-
// object): `{}` carries no shape information, so the literal builds as
|
|
4295
|
-
// its OWN type, exactly the unmappable-context fallback below. An
|
|
4296
|
-
// empty LITERAL keeps the context (the shapes agree).
|
|
4297
|
-
if (mapped?.kind === "record" && expr.properties.length > 0) {
|
|
4298
|
-
const ctxShape = lowerer.shapes.get(mapped.shapeId);
|
|
4299
|
-
if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) {
|
|
4300
|
-
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
|
|
4301
|
-
}
|
|
4302
|
-
}
|
|
4303
|
-
// A literal with a property the contextual TYPE ITSELF lacks — only
|
|
4304
|
-
// reachable through type assertions and satisfies (fresh-literal
|
|
4305
|
-
// excess-property checks reject the direct spelling): the context
|
|
4306
|
-
// cannot hold the value, so the literal builds at its OWN type and the
|
|
4307
|
-
// slot's width coercion narrows it (divergence 36's copy stance — the
|
|
4308
|
-
// extra fields drop in the copy). The probe is against the CHECKER's
|
|
4309
|
-
// contextual type, not the mapped shape: a property the contextual
|
|
4310
|
-
// type carries but the shape dropped (settled value/reason,
|
|
4311
|
-
// generic-callable members) belongs to the drop paths below, and
|
|
4312
|
-
// index-signature contexts keep every key (overflow capture).
|
|
4313
|
-
if (mapped?.kind === "record" && expr.properties.length > 0) {
|
|
4314
|
-
const ctxShape = lowerer.shapes.get(mapped.shapeId);
|
|
4315
|
-
if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) {
|
|
4316
|
-
const names = new Set(ctxShape.fields.map((f) => f.name));
|
|
4317
|
-
const ctxHasProp = (name) => {
|
|
4318
|
-
const members = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
|
|
4319
|
-
return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined);
|
|
4320
|
-
};
|
|
4321
|
-
const extraOf = (text) => !names.has(text) && !ctxHasProp(text);
|
|
4322
|
-
const extra = expr.properties.some((p) => {
|
|
4323
|
-
if (ts.isSpreadAssignment(p) || !p.name)
|
|
4324
|
-
return false;
|
|
4325
|
-
if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
|
|
4326
|
-
return extraOf(p.name.text);
|
|
4327
|
-
if (ts.isNumericLiteral(p.name))
|
|
4328
|
-
return extraOf(String(Number(p.name.text)));
|
|
4329
|
-
return false; // computed keys keep their existing paths
|
|
4330
|
-
});
|
|
4331
|
-
if (extra)
|
|
4332
|
-
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
// A union-typed slot (`const r: Res = { kind: "ok", ... }`) contextually
|
|
4336
|
-
// types the literal as the WHOLE union; build the literal as its own
|
|
4337
|
-
// (arm) shape and let the slot's coercion wrap it into the union. An
|
|
4338
|
-
// unknown-typed slot (`JSON.stringify({ a: 1 })`) likewise. An
|
|
4339
|
-
// UNMAPPABLE context falls back the same way — the literal's own type
|
|
4340
|
-
// is the plain record the slot coerces. A CLASS-INSTANCE context
|
|
4341
|
-
// (`const p: Point = { x, y }` — tsc's structural view of data
|
|
4342
|
-
// classes) falls back too: the literal builds as its own record and
|
|
4343
|
-
// the slot's width coercion constructs through the trivial
|
|
4344
|
-
// parameter-property constructor (recordToClassPlan), or fences with
|
|
4345
|
-
// the record-shape story.
|
|
4346
|
-
// A jsval-mapped context reaches here only when lowerExpr's island
|
|
4347
|
-
// gate DECLINED it (a project-declared typedef that absorbed to the
|
|
4348
|
-
// island through checker-`any` field residue): the literal builds at
|
|
4349
|
-
// its own type like every unmappable context.
|
|
4350
|
-
if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object" || mapped.kind === "jsval") {
|
|
4351
|
-
const ctxUnion = mapped?.kind === "union" ? mapped : null;
|
|
4352
|
-
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
|
|
4353
|
-
// A literal whose own shape re-tags into NO arm of the contextual
|
|
4354
|
-
// union, where the union has exactly ONE record arm: build AS that
|
|
4355
|
-
// arm — there is no ambiguity (tsc already checked the literal
|
|
4356
|
-
// against the union, and the record arm is the only shape it can
|
|
4357
|
-
// inhabit), and the arm's field types drive every property's
|
|
4358
|
-
// coercion (`{ env: {...spread...}, onCleanup }` against an
|
|
4359
|
-
// optional options param — the env value builds by ITS contextual
|
|
4360
|
-
// index-signature type and wraps into the arm's `| undefined`
|
|
4361
|
-
// field, where the literal's own inferred width would mismatch).
|
|
4362
|
-
// Empty literals (`_env = {}` — the fieldless own shape) and
|
|
4363
|
-
// literals against PURE index-signature arms (`{ OPENSSL_CONF:
|
|
4364
|
-
// candidate }` — keys become overflow entries) are the same rule's
|
|
4365
|
-
// simplest cases. The empty-array-in-union rule, record form.
|
|
4366
|
-
if (ctxUnion) {
|
|
4367
|
-
const def = lowerer.unions.get(ctxUnion.unionId);
|
|
4368
|
-
const recordArms = def?.arms.filter((a) => a.kind === "record") ?? [];
|
|
4369
|
-
if (recordArms.length === 1) {
|
|
4370
|
-
const armShape = lowerer.shapes.get(recordArms[0].shapeId);
|
|
4371
|
-
if ((mapped?.kind !== "record" || mapped.shapeId !== recordArms[0].shapeId) &&
|
|
4372
|
-
!armShape?.tuple) {
|
|
4373
|
-
mapped = recordArms[0];
|
|
4374
|
-
}
|
|
4375
|
-
}
|
|
4376
|
-
else if (recordArms.length > 1) {
|
|
4377
|
-
const ownShapeId = mapped?.kind === "record" ? mapped.shapeId : null;
|
|
4378
|
-
// SEVERAL record arms (the reducer-action / discriminated-message
|
|
4379
|
-
// pattern): the literal's own inferred shape re-tags into no arm
|
|
4380
|
-
// — its field types widened per field (`parsed: 1` against
|
|
4381
|
-
// `parsed: number | null`), so the IR-level candidate probe is
|
|
4382
|
-
// ambiguous (a narrower arm also admits the literal by dropping
|
|
4383
|
-
// fields). The LITERAL types tsc checked carry the discriminant
|
|
4384
|
-
// the shapes erased: match the literal's fields against each
|
|
4385
|
-
// union member's CHECKER types (literal-vs-literal field pairs
|
|
4386
|
-
// decide by value — the `kind: "a"` discriminant), and when
|
|
4387
|
-
// exactly ONE member fits, build AS that arm — its field types
|
|
4388
|
-
// drive every property's coercion, exactly the single-record-arm
|
|
4389
|
-
// rule above. Ambiguous literals keep the SC2003 fence.
|
|
4390
|
-
if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId)) {
|
|
4391
|
-
const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms);
|
|
4392
|
-
if (arm)
|
|
4393
|
-
mapped = arm;
|
|
4394
|
-
}
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
// The CJS EXPORT-TABLE literal in VALUE position (JS): importers reach
|
|
4399
|
-
// every member through alias plumbing and accessor lifts — the record
|
|
4400
|
-
// VALUE exists for the module's own reads (Object.keys, internal
|
|
4401
|
-
// member reads), so it keeps exactly the plain fields whose values
|
|
4402
|
-
// lower; accessor entries and members with no value representation
|
|
4403
|
-
// (rest-param function types) narrow away. SEMANTICS.md documents the
|
|
4404
|
-
// enumeration divergence.
|
|
4405
|
-
if (isJsSourceFile(expr.getSourceFile()) &&
|
|
4406
|
-
(!mapped || expr.properties.some((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))) &&
|
|
4407
|
-
isCjsExportTableLiteral(expr)) {
|
|
4408
|
-
const fields = [];
|
|
4409
|
-
for (const prop of expr.properties) {
|
|
4410
|
-
if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop))
|
|
4411
|
-
continue;
|
|
4412
|
-
const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name;
|
|
4413
|
-
if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName)))
|
|
4414
|
-
continue;
|
|
4415
|
-
const name = propName.text;
|
|
4416
|
-
let v = null;
|
|
4417
|
-
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
4418
|
-
v = probeLower(lowerer, propName);
|
|
4419
|
-
}
|
|
4420
|
-
else if (ts.isPropertyAssignment(prop)) {
|
|
4421
|
-
v = probeLower(lowerer, prop.initializer);
|
|
4422
|
-
}
|
|
4423
|
-
if (!v || v.type.kind === "void" || v.type.kind === "caught" || v.type.kind === "jsval")
|
|
4424
|
-
continue;
|
|
4425
|
-
if (isUnitType(v.type))
|
|
4426
|
-
continue;
|
|
4427
|
-
fields.push({ name, value: v });
|
|
4428
|
-
}
|
|
4429
|
-
// Canonical (sorted) field order — the shape registry's invariant;
|
|
4430
|
-
// the dropped reads are all pure, so reordering loses nothing.
|
|
4431
|
-
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
4432
|
-
const shapeId = lowerer.shapes.intern(fields.map((f) => ({ name: f.name, type: f.value.type })));
|
|
4433
|
-
return { kind: "recordLit", fields, type: { kind: "record", shapeId }, loc };
|
|
4434
|
-
}
|
|
4435
|
-
// The JS declaration fallback, literal-side (the checked-dynamic tree-array rule's
|
|
4436
|
-
// object form): a literal whose shape has no static home — an
|
|
4437
|
-
// unmappable contextual type over unmappable own fields (the
|
|
4438
|
-
// PropertyDescriptorMap argument of Object.defineProperties, nested
|
|
4439
|
-
// descriptor records with `any` values) — builds as a dyn OBJECT:
|
|
4440
|
-
// each field converts through the usual dyn boundary, and dynamic
|
|
4441
|
-
// consumers ride the keyed-dyn paths. TypeScript keeps the fence.
|
|
4442
|
-
if ((!mapped || mapped.kind === "dyn") && isJsSourceFile(expr.getSourceFile())) {
|
|
4443
|
-
return lowerDynObjectLiteral(lowerer, expr);
|
|
4444
|
-
}
|
|
4445
|
-
if (!mapped || mapped.kind !== "record")
|
|
4446
|
-
lowerer.badType(expr, tsType);
|
|
4447
|
-
let type = mapped;
|
|
4448
|
-
let shape = lowerer.shapes.get(type.shapeId);
|
|
4449
|
-
// ACCESSOR properties, JS literals only (TS accessors fill the shape's
|
|
4450
|
-
// %get:/%set: closure slots below): no record storage exists for them,
|
|
4451
|
-
// so the literal's shape NARROWS to its plain fields (reads resolve
|
|
4452
|
-
// through the lifted accessors; Object.keys over the value omits
|
|
4453
|
-
// accessor names — the documented divergence).
|
|
4454
|
-
if (isJsSourceFile(expr.getSourceFile())) {
|
|
4455
|
-
const accessorNames = new Set(expr.properties
|
|
4456
|
-
.filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
|
|
4457
|
-
.map((p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : ""))
|
|
4458
|
-
.filter((n) => n !== ""));
|
|
4459
|
-
if (accessorNames.size > 0) {
|
|
4460
|
-
const narrowed = shape.fields.filter((f) => !accessorNames.has(f.name));
|
|
4461
|
-
const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
|
|
4462
|
-
type = { kind: "record", shapeId: narrowedId };
|
|
4463
|
-
shape = lowerer.shapes.get(narrowedId);
|
|
4464
|
-
}
|
|
4465
|
-
}
|
|
4466
|
-
const fieldTypes = new Map(shape.fields.map((f) => [f.name, f.type]));
|
|
4467
|
-
// The clone probe may lower the leading spread before declining (a
|
|
4468
|
-
// static `as` cast can make the checker-visible shape match while its
|
|
4469
|
-
// erased IR value keeps a wider shape). Reuse that exact value in the
|
|
4470
|
-
// ordinary spread path so lowering still happens once.
|
|
4471
|
-
let leadingSpreadLowered = null;
|
|
4472
|
-
// The overwhelmingly common immutable-update form over a record:
|
|
4473
|
-
// `{ ...model, changed, other: value }`. The historic lowering expanded
|
|
4474
|
-
// the spread into one recordGet per untouched field at EVERY site, then
|
|
4475
|
-
// the backends inlined all of those retains and stores into the caller.
|
|
4476
|
-
// A 178-field model updated hundreds of times consequently produced a
|
|
4477
|
-
// half-million-line LLVM function. Keep the same evaluation/ownership
|
|
4478
|
-
// semantics in one compact IR node: source first, explicit overrides in
|
|
4479
|
-
// source order, and one backend clone helper per shape.
|
|
4480
|
-
//
|
|
4481
|
-
// Stay deliberately narrow. Tuple/index/accessor shapes, conditional or
|
|
4482
|
-
// multiple spreads, spread-after-explicit order, and shape-changing width
|
|
4483
|
-
// copies retain their existing lowering and diagnostics.
|
|
4484
|
-
if (!isJsSourceFile(expr.getSourceFile()) &&
|
|
4485
|
-
!shape.indexValue &&
|
|
4486
|
-
!shape.tuple &&
|
|
4487
|
-
!shapeHasAccessorSlots(shape) &&
|
|
4488
|
-
expr.properties.length >= 2 &&
|
|
4489
|
-
ts.isSpreadAssignment(expr.properties[0]) &&
|
|
4490
|
-
!conditionalSpreadOf(expr.properties[0].expression) &&
|
|
4491
|
-
expr.properties.slice(1).every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
|
4492
|
-
p.name !== undefined &&
|
|
4493
|
-
(ts.isIdentifier(p.name) ||
|
|
4494
|
-
ts.isStringLiteral(p.name) ||
|
|
4495
|
-
ts.isNumericLiteral(p.name) ||
|
|
4496
|
-
(ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)))) {
|
|
4497
|
-
const spread = expr.properties[0];
|
|
4498
|
-
const props = expr.properties.slice(1);
|
|
4499
|
-
const names = props.map((p) => propNameText(lowerer, p.name));
|
|
4500
|
-
const unique = new Set(names);
|
|
4501
|
-
const sourceType = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
|
|
4502
|
-
if (sourceType?.kind === "record" &&
|
|
4503
|
-
sourceType.shapeId === type.shapeId &&
|
|
4504
|
-
unique.size === names.length &&
|
|
4505
|
-
names.every((name) => fieldTypes.has(name))) {
|
|
4506
|
-
const source = lowerer.lowerExpr(spread.expression);
|
|
4507
|
-
if (source.type.kind === "record" && source.type.shapeId === type.shapeId) {
|
|
4508
|
-
const overrides = [];
|
|
4509
|
-
for (let i = 0; i < props.length; i++) {
|
|
4510
|
-
const p = props[i];
|
|
4511
|
-
const name = names[i];
|
|
4512
|
-
const fieldType = fieldTypes.get(name);
|
|
4513
|
-
const valueNode = ts.isPropertyAssignment(p) ? p.initializer : p;
|
|
4514
|
-
let value = ts.isPropertyAssignment(p)
|
|
4515
|
-
? lowerer.lowerExpr(p.initializer)
|
|
4516
|
-
: lowerer.lowerShorthandValue(p);
|
|
4517
|
-
value = lowerer.coerceInto(valueNode, value, fieldType);
|
|
4518
|
-
if (!typeEquals(value.type, fieldType))
|
|
4519
|
-
lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
|
4520
|
-
overrides.push({ name, value });
|
|
4521
|
-
}
|
|
4522
|
-
return { kind: "recordClone", source, overrides, type, loc };
|
|
4523
|
-
}
|
|
4524
|
-
leadingSpreadLowered = source;
|
|
4525
|
-
}
|
|
4526
|
-
}
|
|
4527
|
-
// A PURE index-signature target with spreads — `{ ...process.env }`,
|
|
4528
|
-
// `{ ...process.env, ...extraEnv }`, `{ ...env, PATH: p }` (the
|
|
4529
|
-
// spawn-env pattern): the field-by-field desugar below cannot
|
|
4530
|
-
// enumerate runtime overflow keys, so the literal lowers as ONE
|
|
4531
|
-
// interned merge-helper call — contributors apply in literal order
|
|
4532
|
-
// with keyed writes (JS last-write-wins), sources and values evaluate
|
|
4533
|
-
// once each in source order (the call's argument order). A CONDITIONAL
|
|
4534
|
-
// spread `...(c ? { k: v } : {})` contributes its one key as a ternary
|
|
4535
|
-
// — cond once, v lazily, the empty arm holding the value slot's
|
|
4536
|
-
// undefined arm (the explicit-undefined-is-absent stance: JSON and
|
|
4537
|
-
// child-env builders drop it, exactly Node's absent key); targets
|
|
4538
|
-
// with declared fields keep the historic desugar below.
|
|
4539
|
-
const isRuntimeComputedKey = (p) => !ts.isSpreadAssignment(p) &&
|
|
4540
|
-
p.name !== undefined &&
|
|
4541
|
-
ts.isComputedPropertyName(p.name) &&
|
|
4542
|
-
literalComputedKey(lowerer, p.name) === null;
|
|
4543
|
-
if (shape.indexValue &&
|
|
4544
|
-
shape.fields.length === 0 &&
|
|
4545
|
-
!shape.tuple &&
|
|
4546
|
-
(expr.properties.some((p) => ts.isSpreadAssignment(p)) || expr.properties.some(isRuntimeComputedKey))) {
|
|
4547
|
-
const contributors = [];
|
|
4548
|
-
let mergeable = true;
|
|
4549
|
-
for (const prop of expr.properties) {
|
|
4550
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
4551
|
-
const cs = conditionalSpreadOf(prop.expression);
|
|
4552
|
-
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
|
4553
|
-
lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm)");
|
|
4554
|
-
}
|
|
4555
|
-
if (cs) {
|
|
4556
|
-
const absent = lowerer.wrappedUndefined(shape.indexValue, locOf(prop));
|
|
4557
|
-
if (!absent) {
|
|
4558
|
-
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)`);
|
|
4559
|
-
}
|
|
4560
|
-
const csProp = cs.props[0];
|
|
4561
|
-
const cond = lowerer.lowerCondition(cs.cond);
|
|
4562
|
-
const vNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
|
|
4563
|
-
const v = lowerer.intoIndexValueSlot(ts.isPropertyAssignment(csProp) ? lowerer.lowerExpr(csProp.initializer) : lowerer.lowerShorthandValue(csProp), shape.indexValue, vNode);
|
|
4564
|
-
contributors.push({
|
|
4565
|
-
kind: "field",
|
|
4566
|
-
name: csProp.name.text,
|
|
4567
|
-
value: {
|
|
4568
|
-
kind: "ternary",
|
|
4569
|
-
cond,
|
|
4570
|
-
then: cs.whenTrue ? v : absent,
|
|
4571
|
-
else_: cs.whenTrue ? absent : v,
|
|
4572
|
-
type: shape.indexValue,
|
|
4573
|
-
loc: locOf(prop),
|
|
4574
|
-
},
|
|
4575
|
-
});
|
|
4576
|
-
continue;
|
|
4577
|
-
}
|
|
4578
|
-
const src = lowerer.lowerExpr(prop.expression);
|
|
4579
|
-
if (src.type.kind !== "record") {
|
|
4580
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(src.type)}' sources into an index-signature shape (only index-signature records merge — ${NARROW_FIRST})`);
|
|
4581
|
-
}
|
|
4582
|
-
fenceAccessorSpreadSource(lowerer, prop, lowerer.shapes.get(src.type.shapeId));
|
|
4583
|
-
contributors.push({ kind: "spread", shapeId: src.type.shapeId, value: src });
|
|
4584
|
-
continue;
|
|
4585
|
-
}
|
|
4586
|
-
if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
|
|
4587
|
-
prop.name &&
|
|
4588
|
-
(ts.isIdentifier(prop.name) ||
|
|
4589
|
-
ts.isStringLiteral(prop.name) ||
|
|
4590
|
-
ts.isNumericLiteral(prop.name) ||
|
|
4591
|
-
(ts.isComputedPropertyName(prop.name) && literalComputedKey(lowerer, prop.name) !== null))) {
|
|
4592
|
-
const value = ts.isPropertyAssignment(prop)
|
|
4593
|
-
? lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer)
|
|
4594
|
-
: lowerer.intoIndexValueSlot(lowerer.lowerShorthandValue(prop), shape.indexValue, prop);
|
|
4595
|
-
contributors.push({ kind: "field", name: propNameText(lowerer, prop.name), value });
|
|
4596
|
-
continue;
|
|
4597
|
-
}
|
|
4598
|
-
// A RUNTIME-keyed property (`{ ...m, ["a" + "b"]: "" }`, `{ [K]:
|
|
4599
|
-
// v }` where K is a runtime string): the key evaluates before its
|
|
4600
|
-
// value (JS's per-property order — both are helper arguments) and
|
|
4601
|
-
// writes through the signature exactly like a spread's keys.
|
|
4602
|
-
// Number/boolean/unknown keys stringify (ToPropertyKey).
|
|
4603
|
-
if (ts.isPropertyAssignment(prop) && ts.isComputedPropertyName(prop.name)) {
|
|
4604
|
-
let k = lowerer.lowerExpr(prop.name.expression);
|
|
4605
|
-
if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
|
|
4606
|
-
k = { kind: "toString", operand: k, type: STRING, loc: locOf(prop.name) };
|
|
4607
|
-
}
|
|
4608
|
-
if (k.type.kind !== "string") {
|
|
4609
|
-
lowerer.unsupported("SC1090", prop.name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
|
|
4610
|
-
}
|
|
4611
|
-
const value = lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer);
|
|
4612
|
-
contributors.push({ kind: "keyedField", key: k, value });
|
|
4613
|
-
continue;
|
|
4614
|
-
}
|
|
4615
|
-
mergeable = false;
|
|
4616
|
-
break;
|
|
4617
|
-
}
|
|
4618
|
-
if (mergeable) {
|
|
4619
|
-
const helper = lowerIndexMergeHelper(lowerer, type.shapeId, contributors, loc);
|
|
4620
|
-
if (helper === null) {
|
|
4621
|
-
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)`);
|
|
4622
|
-
}
|
|
4623
|
-
return {
|
|
4624
|
-
kind: "call",
|
|
4625
|
-
callee: helper,
|
|
4626
|
-
args: contributors.flatMap((c) => (c.kind === "keyedField" ? [c.key, c.value] : [c.value])),
|
|
4627
|
-
type,
|
|
4628
|
-
loc,
|
|
4629
|
-
};
|
|
4630
|
-
}
|
|
4631
|
-
}
|
|
4632
|
-
// Unfoldable computed keys outside the index-signature merge path:
|
|
4633
|
-
// no record shape can hold a runtime-decided name.
|
|
4634
|
-
for (const prop of expr.properties) {
|
|
4635
|
-
if (isRuntimeComputedKey(prop)) {
|
|
4636
|
-
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)");
|
|
4637
|
-
}
|
|
4638
|
-
}
|
|
4639
|
-
// A DECLARED-fields target built ENTIRELY from index-signature spreads —
|
|
4640
|
-
// `{ ...Object.fromEntries(...), ...Object.fromEntries(...) }` typed
|
|
4641
|
-
// AppConfig (the defaults-merge idiom over runtime-keyed sources): the
|
|
4642
|
-
// field-by-field desugar cannot enumerate runtime keys, so the literal
|
|
4643
|
-
// lowers as ONE interned merge-helper call — sources evaluate once each
|
|
4644
|
-
// in source order (the call's argument order — computed sources
|
|
4645
|
-
// included, no re-read), contributors apply in order with per-key
|
|
4646
|
-
// dispatch onto the declared fields (JS last-write-wins). Divergence 68
|
|
4647
|
-
// has the runtime rules (validated collisions, extra keys dropped).
|
|
4648
|
-
if (!shape.indexValue &&
|
|
4649
|
-
!shape.tuple &&
|
|
4650
|
-
shape.fields.length > 0 &&
|
|
4651
|
-
expr.properties.length > 0 &&
|
|
4652
|
-
expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
|
|
4653
|
-
expr.properties.some((p) => {
|
|
4654
|
-
const t = lowerer.mapTypeOf(lowerer.typeOf(p.expression));
|
|
4655
|
-
return t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue;
|
|
4656
|
-
})) {
|
|
4657
|
-
return lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc);
|
|
4658
|
-
}
|
|
4659
|
-
const shapeMismatch = (node) => {
|
|
4660
|
-
const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
4661
|
-
lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), own ? lowerer.fmt(own) : lowerer.checker.typeToString(lowerer.typeOf(expr)), locOf(node), own?.kind === "record"
|
|
4662
|
-
? (lowerer.describeRecordWidthBlocker(own.shapeId, type.shapeId) ?? undefined)
|
|
4663
|
-
: undefined));
|
|
4664
|
-
throw new PoisonError();
|
|
4665
|
-
};
|
|
4666
|
-
const fields = [];
|
|
4667
|
-
// Field names introduced by conditional spreads: their ternary carries
|
|
4668
|
-
// the spread's whole evaluation (cond once, value lazily), so a LATER
|
|
4669
|
-
// contributor overriding one would silently drop that evaluation —
|
|
4670
|
-
// collisions fence in both directions.
|
|
4671
|
-
const conditionalNames = new Set();
|
|
4672
|
-
// Member names DROPPED from the shape (JS deferral — unlowerable pure
|
|
4673
|
-
// member reads narrow away; see the catch in the property loop).
|
|
4674
|
-
const droppedNames = new Set();
|
|
4675
|
-
for (const prop of expr.properties) {
|
|
4676
|
-
if (ts.isSpreadAssignment(prop)) {
|
|
4677
|
-
const cs = conditionalSpreadOf(prop.expression);
|
|
4678
|
-
if (cs && cs !== "unsupported") {
|
|
4679
|
-
// `...(c ? { k: v } : {})` — ONE conditional field at the
|
|
4680
|
-
// spread's own position: cond evaluates exactly once, `v` only
|
|
4681
|
-
// when the non-empty arm is taken (ternary arms are lazy), and
|
|
4682
|
-
// the empty arm holds the interned undefined arm — exactly the
|
|
4683
|
-
// omitted-optional-field representation, which is also why the
|
|
4684
|
-
// target field must be optional (tsc types the idiom that way).
|
|
4685
|
-
const csProp = cs.props[0]; // single-prop-checked in the fence pass
|
|
4686
|
-
const name = csProp.name.text;
|
|
4687
|
-
const fieldType = fieldTypes.get(name);
|
|
4688
|
-
if (!fieldType) {
|
|
4689
|
-
if (shape.indexValue) {
|
|
4690
|
-
lowerer.unsupported("SC1090", prop, "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)");
|
|
4691
|
-
}
|
|
4692
|
-
throw shapeMismatch(prop);
|
|
4693
|
-
}
|
|
4694
|
-
const absent = lowerer.wrappedUndefined(fieldType, locOf(prop));
|
|
4695
|
-
if (!absent) {
|
|
4696
|
-
lowerer.unsupported("SC1090", prop, `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`);
|
|
4697
|
-
}
|
|
4698
|
-
if (fields.some((f) => f.name === name)) {
|
|
4699
|
-
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)`);
|
|
4700
|
-
}
|
|
4701
|
-
const cond = lowerer.lowerCondition(cs.cond);
|
|
4702
|
-
const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
|
|
4703
|
-
let v = ts.isPropertyAssignment(csProp)
|
|
4704
|
-
? lowerer.lowerExpr(csProp.initializer)
|
|
4705
|
-
: lowerer.lowerShorthandValue(csProp);
|
|
4706
|
-
v = lowerer.coerceInto(valueNode, v, fieldType);
|
|
4707
|
-
if (!typeEquals(v.type, fieldType))
|
|
4708
|
-
lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
|
4709
|
-
conditionalNames.add(name);
|
|
4710
|
-
fields.push({
|
|
4711
|
-
name,
|
|
4712
|
-
value: {
|
|
4713
|
-
kind: "ternary",
|
|
4714
|
-
cond,
|
|
4715
|
-
then: cs.whenTrue ? v : absent,
|
|
4716
|
-
else_: cs.whenTrue ? absent : v,
|
|
4717
|
-
type: fieldType,
|
|
4718
|
-
loc: locOf(prop),
|
|
4719
|
-
},
|
|
4720
|
-
});
|
|
4721
|
-
continue;
|
|
4722
|
-
}
|
|
4723
|
-
// `{ ...base, ... }` — field-by-field copy of a known record
|
|
4724
|
-
// shape. Every source field must land on the target shape with an
|
|
4725
|
-
// equal type (a wider source would silently DROP fields JS keeps —
|
|
4726
|
-
// the width fence, same as literals). Later contributors override
|
|
4727
|
-
// earlier ones (JS last-write-wins; the reads are side-effect-free,
|
|
4728
|
-
// so dropping the earlier read is exact). Identifier sources
|
|
4729
|
-
// re-read per field (historic path); any OTHER source must be a
|
|
4730
|
-
// re-emittable pure read, sharing one lowered node per field.
|
|
4731
|
-
// The desugar's one-entry-per-name list reads spread fields
|
|
4732
|
-
// EAGERLY at the spread's position, so an explicit property
|
|
4733
|
-
// BEFORE a spread would need JS's overwrite — order-fenced (the
|
|
4734
|
-
// index-signature merge path above takes any order).
|
|
4735
|
-
if (expr.properties
|
|
4736
|
-
.slice(0, expr.properties.indexOf(prop))
|
|
4737
|
-
.some((p) => !ts.isSpreadAssignment(p))) {
|
|
4738
|
-
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)");
|
|
4739
|
-
}
|
|
4740
|
-
let srcNode = prop.expression;
|
|
4741
|
-
while (ts.isParenthesizedExpression(srcNode))
|
|
4742
|
-
srcNode = srcNode.expression;
|
|
4743
|
-
const srcLowered = prop === expr.properties[0] && leadingSpreadLowered !== null
|
|
4744
|
-
? leadingSpreadLowered
|
|
4745
|
-
: ts.isIdentifier(srcNode)
|
|
4746
|
-
? null
|
|
4747
|
-
: lowerer.lowerExpr(srcNode);
|
|
4748
|
-
const srcType = srcLowered ? srcLowered.type : lowerer.mapTypeOf(lowerer.typeOf(srcNode));
|
|
4749
|
-
// `...options.installConfig` — a spread of `Partial<X> | undefined`
|
|
4750
|
-
// (the optional-options merge idiom `{ ...DEFAULTS, ...overrides }`):
|
|
4751
|
-
// JS spreads nothing for the unit arm and copies present keys
|
|
4752
|
-
// otherwise. Per target field the desugar builds
|
|
4753
|
-
// `present ? extracted : earlier` — present tests the source's
|
|
4754
|
-
// record tag AND (optional source fields) the field's own value
|
|
4755
|
-
// arm; absent keeps the earlier contributor's value (both reads
|
|
4756
|
-
// are pure, so the reorder into the ternary is unobservable).
|
|
4757
|
-
if (srcType?.kind === "union") {
|
|
4758
|
-
const def = lowerer.unions.get(srcType.unionId);
|
|
4759
|
-
const recArms = def?.arms.filter((a) => a.kind === "record") ?? [];
|
|
4760
|
-
if (!def ||
|
|
4761
|
-
recArms.length !== 1 ||
|
|
4762
|
-
!def.arms.every((a) => a.kind === "record" || isUnitType(a))) {
|
|
4763
|
-
lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources (only known record shapes spread — ${NARROW_FIRST})`);
|
|
4764
|
-
}
|
|
4765
|
-
if (srcLowered && !pureReemittable(srcLowered)) {
|
|
4766
|
-
lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
|
|
4767
|
-
}
|
|
4768
|
-
const recArm = recArms[0];
|
|
4769
|
-
const recTag = def.arms.indexOf(recArm);
|
|
4770
|
-
// A checked-dynamic VALUE under the union-mapped checker type
|
|
4771
|
-
// (a JS dyn-holding binding): the present/absent desugar tests
|
|
4772
|
-
// union tags a dyn box does not carry — fence honestly instead
|
|
4773
|
-
// of the validator's ICE.
|
|
4774
|
-
const probedSrc = srcLowered ?? probeLower(lowerer, srcNode);
|
|
4775
|
-
if (probedSrc?.type.kind === "dyn") {
|
|
4776
|
-
lowerer.unsupported("SC1090", prop, `object spread of a checked-dynamic '${lowerer.fmt(srcType)}' source (${NARROW_FIRST})`);
|
|
4777
|
-
}
|
|
4778
|
-
const srcShape = lowerer.shapes.get(recArm.shapeId);
|
|
4779
|
-
if (!srcShape)
|
|
4780
|
-
throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${recArm.shapeId}`);
|
|
4781
|
-
fenceAccessorSpreadSource(lowerer, prop, srcShape);
|
|
4782
|
-
if (srcShape.indexValue || shape.indexValue) {
|
|
4783
|
-
lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
|
|
4784
|
-
}
|
|
4785
|
-
const laterNames = new Set();
|
|
4786
|
-
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
|
4787
|
-
if (ts.isSpreadAssignment(later)) {
|
|
4788
|
-
if (conditionalSpreadOf(later.expression))
|
|
4789
|
-
continue;
|
|
4790
|
-
const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
|
|
4791
|
-
if (lt?.kind === "record") {
|
|
4792
|
-
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
|
|
4793
|
-
laterNames.add(lf.name);
|
|
4794
|
-
}
|
|
4795
|
-
continue;
|
|
4796
|
-
}
|
|
4797
|
-
if (later.name &&
|
|
4798
|
-
(ts.isIdentifier(later.name) ||
|
|
4799
|
-
ts.isStringLiteral(later.name) ||
|
|
4800
|
-
ts.isNumericLiteral(later.name) ||
|
|
4801
|
-
(ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
|
|
4802
|
-
laterNames.add(propNameText(lowerer, later.name));
|
|
4803
|
-
}
|
|
4804
|
-
}
|
|
4805
|
-
const srcRef = () => srcLowered ?? lowerer.lowerExpr(srcNode);
|
|
4806
|
-
for (const f of srcShape.fields) {
|
|
4807
|
-
if (laterNames.has(f.name))
|
|
4808
|
-
continue;
|
|
4809
|
-
const targetType = fieldTypes.get(f.name);
|
|
4810
|
-
// No slot on the target shape: the copy DROPS the field
|
|
4811
|
-
// (divergence 36's stance, same as the plain-record spread).
|
|
4812
|
-
if (!targetType)
|
|
4813
|
-
continue;
|
|
4814
|
-
if (conditionalNames.has(f.name)) {
|
|
4815
|
-
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)`);
|
|
4816
|
-
}
|
|
4817
|
-
const fRead = () => ({
|
|
4818
|
-
kind: "recordGet",
|
|
4819
|
-
obj: { kind: "unionNarrow", unionId: srcType.unionId, tag: recTag, value: srcRef(), type: recArm, loc: locOf(prop) },
|
|
4820
|
-
shapeId: recArm.shapeId,
|
|
4821
|
-
field: f.name,
|
|
4822
|
-
type: f.type,
|
|
4823
|
-
loc: locOf(prop),
|
|
4824
|
-
});
|
|
4825
|
-
let cond = { kind: "unionIsTag", unionId: srcType.unionId, tag: recTag, negated: false, value: srcRef(), type: BOOL, loc: locOf(prop) };
|
|
4826
|
-
let thenVal;
|
|
4827
|
-
if (typeEquals(f.type, targetType)) {
|
|
4828
|
-
thenVal = fRead();
|
|
4829
|
-
}
|
|
4830
|
-
else if (f.type.kind === "union" &&
|
|
4831
|
-
lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
|
|
4832
|
-
typeEquals(lowerer.stripUndefinedArm(f.type), targetType)) {
|
|
4833
|
-
// Optional source field into a required target slot: present
|
|
4834
|
-
// means the source holds the record AND the field its value
|
|
4835
|
-
// arm — the spread-override completion, union-source form.
|
|
4836
|
-
const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
|
|
4837
|
-
cond = {
|
|
4838
|
-
kind: "logical",
|
|
4839
|
-
op: "&&",
|
|
4840
|
-
left: cond,
|
|
4841
|
-
right: { kind: "unionIsTag", unionId: f.type.unionId, tag: ftUndef, negated: true, value: fRead(), type: BOOL, loc: locOf(prop) },
|
|
4842
|
-
type: BOOL,
|
|
4843
|
-
loc: locOf(prop),
|
|
4844
|
-
};
|
|
4845
|
-
const ftDef = lowerer.unions.get(f.type.unionId);
|
|
4846
|
-
if (targetType.kind === "union") {
|
|
4847
|
-
const retag = lowerer.unionRetagHelper(f.type.unionId, targetType.unionId, locOf(prop));
|
|
4848
|
-
if (!retag) {
|
|
4849
|
-
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`));
|
|
4850
|
-
throw new PoisonError();
|
|
4851
|
-
}
|
|
4852
|
-
thenVal = { kind: "call", callee: retag, args: [fRead()], type: targetType, loc: locOf(prop) };
|
|
4853
|
-
}
|
|
4854
|
-
else if (ftDef && ftDef.arms.length === 2 && lowerer.armTag(f.type.unionId, targetType) >= 0) {
|
|
4855
|
-
thenVal = { kind: "unionNarrow", unionId: f.type.unionId, tag: lowerer.armTag(f.type.unionId, targetType), value: fRead(), type: targetType, loc: locOf(prop) };
|
|
4856
|
-
}
|
|
4857
|
-
else {
|
|
4858
|
-
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`));
|
|
4859
|
-
throw new PoisonError();
|
|
4860
|
-
}
|
|
4861
|
-
}
|
|
4862
|
-
else {
|
|
4863
|
-
// The width-lift fallback (arm wrap, re-tag, nested
|
|
4864
|
-
// reshape) — the same per-field rule the slot coercion
|
|
4865
|
-
// applies. Runs AFTER the optional-completion branch: a
|
|
4866
|
-
// present-test has its own semantics a re-tag's stranded
|
|
4867
|
-
// undefined trap must not shadow.
|
|
4868
|
-
const lift = lowerer.widthLiftPlan(f.type, targetType);
|
|
4869
|
-
if (!lift) {
|
|
4870
|
-
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)}'`));
|
|
4871
|
-
throw new PoisonError();
|
|
4872
|
-
}
|
|
4873
|
-
thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
|
|
4874
|
-
}
|
|
4875
|
-
const at = fields.findIndex((x) => x.name === f.name && !x.drop);
|
|
4876
|
-
const elseVal = at >= 0 ? fields[at].value : lowerer.wrappedUndefined(targetType, locOf(prop));
|
|
4877
|
-
if (!elseVal) {
|
|
4878
|
-
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 })`);
|
|
4879
|
-
}
|
|
4880
|
-
const merged = { kind: "ternary", cond, then: thenVal, else_: elseVal, type: targetType, loc: locOf(prop) };
|
|
4881
|
-
if (at >= 0)
|
|
4882
|
-
fields[at] = { name: f.name, value: merged };
|
|
4883
|
-
else
|
|
4884
|
-
fields.push({ name: f.name, value: merged });
|
|
4885
|
-
}
|
|
4886
|
-
continue;
|
|
4887
|
-
}
|
|
4888
|
-
if (srcType?.kind !== "record") {
|
|
4889
|
-
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})`);
|
|
4890
|
-
}
|
|
4891
|
-
if (srcLowered && !pureReemittable(srcLowered)) {
|
|
4892
|
-
lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
|
|
4893
|
-
}
|
|
4894
|
-
const srcShape = lowerer.shapes.get(srcType.shapeId);
|
|
4895
|
-
if (!srcShape)
|
|
4896
|
-
throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${srcType.shapeId}`);
|
|
4897
|
-
fenceAccessorSpreadSource(lowerer, prop, srcShape);
|
|
4898
|
-
// Index-signature shapes carry runtime-keyed overflow entries the
|
|
4899
|
-
// field-by-field desugar cannot enumerate — fenced on either side.
|
|
4900
|
-
if (srcShape.indexValue || shape.indexValue) {
|
|
4901
|
-
lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
|
|
4902
|
-
}
|
|
4903
|
-
// Names a LATER contributor unconditionally defines: copying such a
|
|
4904
|
-
// source field is dead under JS last-write-wins (and spread reads
|
|
4905
|
-
// are side-effect-free), so it neither lowers nor width-checks —
|
|
4906
|
-
// the spread-then-override completion `{ ...config, stateDir }`
|
|
4907
|
-
// narrows an optional source field into a required target slot
|
|
4908
|
-
// exactly like Node does. Conditional spreads don't count (their
|
|
4909
|
-
// empty arm defines nothing) — their own collision fences hold.
|
|
4910
|
-
const laterNames = new Set();
|
|
4911
|
-
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
|
4912
|
-
if (ts.isSpreadAssignment(later)) {
|
|
4913
|
-
if (conditionalSpreadOf(later.expression))
|
|
4914
|
-
continue;
|
|
4915
|
-
const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
|
|
4916
|
-
if (lt?.kind === "record") {
|
|
4917
|
-
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
|
|
4918
|
-
laterNames.add(lf.name);
|
|
4919
|
-
}
|
|
4920
|
-
continue;
|
|
4921
|
-
}
|
|
4922
|
-
if (later.name &&
|
|
4923
|
-
(ts.isIdentifier(later.name) ||
|
|
4924
|
-
ts.isStringLiteral(later.name) ||
|
|
4925
|
-
ts.isNumericLiteral(later.name) ||
|
|
4926
|
-
(ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
|
|
4927
|
-
laterNames.add(propNameText(lowerer, later.name));
|
|
4928
|
-
}
|
|
4929
|
-
}
|
|
4930
|
-
for (const f of srcShape.fields) {
|
|
4931
|
-
if (laterNames.has(f.name))
|
|
4932
|
-
continue;
|
|
4933
|
-
const targetType = fieldTypes.get(f.name);
|
|
4934
|
-
// A source field with NO slot on the target shape: the copy
|
|
4935
|
-
// DROPS it — a spread of a wider record into a narrower literal
|
|
4936
|
-
// is width subtyping in spread clothing, divergence 36's stance
|
|
4937
|
-
// (Node's object would keep the key); the read is pure, so
|
|
4938
|
-
// skipping evaluates nothing.
|
|
4939
|
-
if (!targetType)
|
|
4940
|
-
continue;
|
|
4941
|
-
const lift = typeEquals(f.type, targetType)
|
|
4942
|
-
? null
|
|
4943
|
-
: lowerer.widthLiftPlan(f.type, targetType);
|
|
4944
|
-
if (!typeEquals(f.type, targetType) && !lift) {
|
|
4945
|
-
// Print the SOURCE shape, not the literal's own type: the
|
|
4946
|
-
// checker's own type already has later overrides applied, so
|
|
4947
|
-
// it can render identically to the target while the spread
|
|
4948
|
-
// source (the thing that actually mismatches) differs — an
|
|
4949
|
-
// invisible difference is a diagnostics bug.
|
|
4950
|
-
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)}'`));
|
|
4951
|
-
throw new PoisonError();
|
|
4952
|
-
}
|
|
4953
|
-
const obj = srcLowered ?? lowerer.lowerExpr(srcNode);
|
|
4954
|
-
// A record-mapped CHECKER type whose VALUE lives in the checked-dynamic tree (a
|
|
4955
|
-
// JS file-scope object-literal global): read each field from
|
|
4956
|
-
// the checked-dynamic tree (dynKeyGet) and VALIDATE it into the source shape's
|
|
4957
|
-
// field type (dynCheck) — the checked-dynamic member-read
|
|
4958
|
-
// discipline. A missing key answers the dyn undefined, exactly
|
|
4959
|
-
// the undefined-armed optional's absent case; a mismatched
|
|
4960
|
-
// runtime value throws the catchable TypeError, never a silent
|
|
4961
|
-
// wrong copy. Runtime-ADDED keys drop — width subtyping in
|
|
4962
|
-
// spread clothing, divergence 36's stance.
|
|
4963
|
-
let value;
|
|
4964
|
-
if (obj.type.kind === "dyn") {
|
|
4965
|
-
if (!canDynCheckTo(f.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
|
|
4966
|
-
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)`);
|
|
4967
|
-
}
|
|
4968
|
-
value = {
|
|
4969
|
-
kind: "dynCheck",
|
|
4970
|
-
value: {
|
|
4971
|
-
kind: "dynKeyGet",
|
|
4972
|
-
key: { kind: "strLit", value: f.name, type: STRING, loc: locOf(prop) },
|
|
4973
|
-
value: obj,
|
|
4974
|
-
type: DYN,
|
|
4975
|
-
loc: locOf(prop),
|
|
4976
|
-
},
|
|
4977
|
-
type: f.type,
|
|
4978
|
-
loc: locOf(prop),
|
|
4979
|
-
};
|
|
4980
|
-
}
|
|
4981
|
-
else {
|
|
4982
|
-
value = {
|
|
4983
|
-
kind: "recordGet",
|
|
4984
|
-
obj,
|
|
4985
|
-
shapeId: srcType.shapeId,
|
|
4986
|
-
field: f.name,
|
|
4987
|
-
type: f.type,
|
|
4988
|
-
loc: locOf(prop),
|
|
4989
|
-
};
|
|
4990
|
-
}
|
|
4991
|
-
// A liftable field widens into the target slot (arm wrap,
|
|
4992
|
-
// re-tag, nested reshape) — the same per-field rule the slot
|
|
4993
|
-
// coercion applies.
|
|
4994
|
-
if (lift)
|
|
4995
|
-
value = lowerer.applyWidthLift(lift, value, targetType, locOf(prop));
|
|
4996
|
-
if (conditionalNames.has(f.name)) {
|
|
4997
|
-
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)`);
|
|
4998
|
-
}
|
|
4999
|
-
const at = fields.findIndex((x) => x.name === f.name);
|
|
5000
|
-
if (at >= 0)
|
|
5001
|
-
fields[at] = { name: f.name, value };
|
|
5002
|
-
else
|
|
5003
|
-
fields.push({ name: f.name, value });
|
|
3627
|
+
const w = lowerer.widthCoerce(src, type);
|
|
3628
|
+
if (w)
|
|
3629
|
+
src = w;
|
|
5004
3630
|
}
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
|
|
5008
|
-
// JS accessor entries carry no storage (narrowed away above); TS
|
|
5009
|
-
// accessors fill the shape's reserved closure slots — the getter
|
|
5010
|
-
// body lowers as an ordinary zero-arg closure invoked per property
|
|
5011
|
-
// READ, the setter as a one-arg closure invoked per WRITE
|
|
5012
|
-
// (fieldGetExpr/fieldSetStmt dispatch on the slots). Creating the
|
|
5013
|
-
// closures is side-effect-free, so their position among the data
|
|
5014
|
-
// fields' source-order evaluation is unobservable — exactly JS,
|
|
5015
|
-
// where accessor definitions evaluate nothing.
|
|
5016
|
-
if (isJsSourceFile(expr.getSourceFile()))
|
|
5017
|
-
continue;
|
|
5018
|
-
const name = propNameText(lowerer, prop.name); // key-form-checked above
|
|
5019
|
-
const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`;
|
|
5020
|
-
const slotT = fieldTypes.get(slotName);
|
|
5021
|
-
if (!slotT || slotT.kind !== "func") {
|
|
5022
|
-
// The contextual shape stores a DATA value under this name
|
|
5023
|
-
// (`const p: { x: number } = { get x() {...} }` — tsc lets a
|
|
5024
|
-
// live accessor satisfy a data member): the slot would freeze
|
|
5025
|
-
// one getter answer where Node keeps the accessor live.
|
|
5026
|
-
if (fieldTypes.has(name)) {
|
|
5027
|
-
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)`);
|
|
5028
|
-
}
|
|
5029
|
-
throw shapeMismatch(prop);
|
|
3631
|
+
if (!typeEquals(src.type, type)) {
|
|
3632
|
+
lowerer.unsupported("SC1090", el, `spreading '${lowerer.fmt(src.type)}' into a '${lowerer.fmt(type)}' literal (only a same-element-type array spreads)`);
|
|
5030
3633
|
}
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
lowerer.badType(prop, lowerer.typeOf(prop));
|
|
5034
|
-
fields.push({ name: slotName, value: closure });
|
|
5035
|
-
continue;
|
|
3634
|
+
spreads.push(i);
|
|
3635
|
+
return src;
|
|
5036
3636
|
}
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
//
|
|
5040
|
-
//
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
// A GENERIC-callable member (a generic method `m<T>(x: T) {...}` or a
|
|
5046
|
-
// generic arrow/function-expression property): excluded from the
|
|
5047
|
-
// record shape (isGenericCallableMemberType — no single closure slot
|
|
5048
|
-
// can hold it), so the literal stores nothing for it. Pure
|
|
5049
|
-
// function-creating forms skip outright (creating a closure has no
|
|
5050
|
-
// side effects; calls resolve statically against this declaration);
|
|
5051
|
-
// a computed initializer would need its evaluation kept — fenced.
|
|
5052
|
-
if (!fieldType && !ts.isSpreadAssignment(prop)) {
|
|
5053
|
-
const memberSym = prop.name && lowerer.checker.getSymbolAtLocation(prop.name);
|
|
5054
|
-
const memberT = memberSym ? lowerer.checker.getTypeOfSymbol(memberSym) : undefined;
|
|
5055
|
-
if (memberT && isGenericCallableMemberType(memberT, lowerer.checker)) {
|
|
5056
|
-
const pureInit = (() => {
|
|
5057
|
-
if (ts.isMethodDeclaration(prop))
|
|
5058
|
-
return true;
|
|
5059
|
-
if (ts.isShorthandPropertyAssignment(prop))
|
|
5060
|
-
return true; // a pure read
|
|
5061
|
-
if (!ts.isPropertyAssignment(prop))
|
|
5062
|
-
return false;
|
|
5063
|
-
let init = prop.initializer;
|
|
5064
|
-
while (ts.isParenthesizedExpression(init))
|
|
5065
|
-
init = init.expression;
|
|
5066
|
-
return ts.isArrowFunction(init) || ts.isFunctionExpression(init) || ts.isIdentifier(init);
|
|
5067
|
-
})();
|
|
5068
|
-
if (!pureInit) {
|
|
5069
|
-
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)`);
|
|
5070
|
-
}
|
|
5071
|
-
continue;
|
|
5072
|
-
}
|
|
3637
|
+
// A HOLE (`[,]` — an elision) remains absent. Ordinary reads still
|
|
3638
|
+
// answer undefined, while presence-sensitive methods and concat keep
|
|
3639
|
+
// the hole. The state-building path below advances length without
|
|
3640
|
+
// writing a value into this slot.
|
|
3641
|
+
if (ts.isOmittedExpression(el)) {
|
|
3642
|
+
needsStateBuild = true;
|
|
3643
|
+
holes.add(i);
|
|
3644
|
+
return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc: locOf(el) };
|
|
5073
3645
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
3646
|
+
// An ARRAY-LITERAL element under a UNION element slot whose own type
|
|
3647
|
+
// has no lift into the slot builds against the union's single array
|
|
3648
|
+
// arm instead (`['pwd', []]` as (string | string[])[] — tsc pushes no
|
|
3649
|
+
// contextual type through the unannotated chain, so the empty nested
|
|
3650
|
+
// literal types never[], whose f64-element representation re-tags
|
|
3651
|
+
// into nothing). One array arm is unambiguous — the union-slot rule's
|
|
3652
|
+
// stance; several keep the fence.
|
|
3653
|
+
if (type.elem.kind === "union" || type.elem.kind === "array") {
|
|
3654
|
+
let x = el;
|
|
3655
|
+
while (ts.isParenthesizedExpression(x))
|
|
3656
|
+
x = x.expression;
|
|
3657
|
+
if (ts.isArrayLiteralExpression(x)) {
|
|
3658
|
+
const ownT = lowerer.mapTypeOf(lowerer.checker.getContextualType(x) ?? lowerer.typeOf(x));
|
|
3659
|
+
if (ownT === null || lowerer.widthLiftPlan(ownT, type.elem) === null) {
|
|
3660
|
+
// An ARRAY-typed slot takes the literal directly; a union slot
|
|
3661
|
+
// routes through its single array arm and wraps.
|
|
3662
|
+
if (type.elem.kind === "array") {
|
|
3663
|
+
return lowerer.lowerArrayLiteral(x, type.elem);
|
|
3664
|
+
}
|
|
3665
|
+
const def = lowerer.unions.get(type.elem.unionId);
|
|
3666
|
+
const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
|
|
3667
|
+
if (arrayArms.length === 1) {
|
|
3668
|
+
const built = lowerer.lowerArrayLiteral(x, arrayArms[0]);
|
|
3669
|
+
return lowerer.coerceInto(el, built, type.elem);
|
|
5089
3670
|
}
|
|
5090
3671
|
}
|
|
5091
|
-
continue;
|
|
5092
|
-
}
|
|
5093
|
-
throw shapeMismatch(prop);
|
|
5094
|
-
}
|
|
5095
|
-
let value;
|
|
5096
|
-
let valueNode = prop;
|
|
5097
|
-
const propDiagsBefore = lowerer.diags.length;
|
|
5098
|
-
try {
|
|
5099
|
-
if (ts.isPropertyAssignment(prop)) {
|
|
5100
|
-
valueNode = prop.initializer;
|
|
5101
|
-
// ARRAY-LITERAL initializers route through the expected-type
|
|
5102
|
-
// lowering: a union field with one array-family arm builds the
|
|
5103
|
-
// literal AS that arm (lowerExprExpecting's IR-directed rule —
|
|
5104
|
-
// the option-table `default: [{ value: [] }]` shape), where the
|
|
5105
|
-
// bare lowering would take the JS dyn fallback and fence.
|
|
5106
|
-
let init = prop.initializer;
|
|
5107
|
-
while (ts.isParenthesizedExpression(init))
|
|
5108
|
-
init = init.expression;
|
|
5109
|
-
value =
|
|
5110
|
-
fenceClosureProbe(lowerer, prop.initializer, fieldType, () => lowerer.lowerExpr(prop.initializer)) ??
|
|
5111
|
-
(fieldType !== undefined && ts.isArrayLiteralExpression(init)
|
|
5112
|
-
? lowerer.lowerExprExpecting(prop.initializer, fieldType)
|
|
5113
|
-
: lowerer.lowerExpr(prop.initializer));
|
|
5114
|
-
}
|
|
5115
|
-
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
5116
|
-
value = lowerer.lowerShorthandValue(prop);
|
|
5117
|
-
}
|
|
5118
|
-
else if (ts.isMethodDeclaration(prop)) {
|
|
5119
|
-
value =
|
|
5120
|
-
fenceClosureProbe(lowerer, prop, fieldType, () => lowerer.lowerLambda(prop)) ?? lowerer.lowerLambda(prop);
|
|
5121
3672
|
}
|
|
5122
|
-
else {
|
|
5123
|
-
lowerer.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
|
|
5124
|
-
}
|
|
5125
|
-
}
|
|
5126
|
-
catch (err) {
|
|
5127
|
-
// A member VALUE a JS file cannot lower (a namespace object in an
|
|
5128
|
-
// export aggregate — the sharedWithCli `errors` member): the
|
|
5129
|
-
// member NARROWS AWAY like a CJS export-table accessor entry —
|
|
5130
|
-
// the diagnostics defer to the runtime-fence ledger, the shape
|
|
5131
|
-
// drops the field, and each READ of it meets its own per-site
|
|
5132
|
-
// fence. Pure member forms only (identifier/shorthand reads);
|
|
5133
|
-
// TypeScript, probe mode, and ICEs keep the poison.
|
|
5134
|
-
const pureMember = ts.isShorthandPropertyAssignment(prop) ||
|
|
5135
|
-
(ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer));
|
|
5136
|
-
if (!(err instanceof PoisonError) ||
|
|
5137
|
-
!pureMember ||
|
|
5138
|
-
!isJsSourceFile(expr.getSourceFile()) ||
|
|
5139
|
-
lowerer.diagSink !== null ||
|
|
5140
|
-
lowerer.diags.length <= propDiagsBefore ||
|
|
5141
|
-
lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
|
|
5142
|
-
throw err;
|
|
5143
|
-
}
|
|
5144
|
-
lowerer.runtimeFences.push(...lowerer.diags.splice(propDiagsBefore));
|
|
5145
|
-
droppedNames.add(name);
|
|
5146
|
-
continue;
|
|
5147
|
-
}
|
|
5148
|
-
if (!fieldType) {
|
|
5149
|
-
// Overflow entry: the value flows into the index signature's value
|
|
5150
|
-
// slot — dyn slots take a dyn conversion (dynFrom), typed slots the
|
|
5151
|
-
// ordinary coercion path. Later duplicates override (map semantics
|
|
5152
|
-
// are last-write-wins already; a duplicate literal key is a tsc
|
|
5153
|
-
// error anyway).
|
|
5154
|
-
const slotted = lowerer.intoIndexValueSlot(value, shape.indexValue, valueNode);
|
|
5155
|
-
fields.push({ name, value: slotted, overflow: true });
|
|
5156
|
-
continue;
|
|
5157
|
-
}
|
|
5158
|
-
value = lowerer.coerceInto(valueNode, value, fieldType); // union-typed fields wrap arm values
|
|
5159
|
-
if (!typeEquals(value.type, fieldType))
|
|
5160
|
-
lowerer.badType(valueNode, lowerer.typeOf(valueNode));
|
|
5161
|
-
// An explicit property overrides a spread-copied field: JS
|
|
5162
|
-
// last-write-wins. The entry moves to the END so explicit property
|
|
5163
|
-
// values keep their source-order evaluation among themselves. A
|
|
5164
|
-
// conditional-spread entry cannot be overridden (its ternary IS the
|
|
5165
|
-
// spread's evaluation; splicing it out would drop that).
|
|
5166
|
-
if (conditionalNames.has(name)) {
|
|
5167
|
-
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)`);
|
|
5168
|
-
}
|
|
5169
|
-
const at = fields.findIndex((x) => x.name === name);
|
|
5170
|
-
if (at >= 0)
|
|
5171
|
-
fields.splice(at, 1);
|
|
5172
|
-
fields.push({ name, value });
|
|
5173
|
-
}
|
|
5174
|
-
// Optional fields (undefined-armed union slots) may be omitted: the
|
|
5175
|
-
// absent field holds the interned undefined arm, exactly like writing
|
|
5176
|
-
// `a: undefined` (without exactOptionalPropertyTypes tsc treats the two
|
|
5177
|
-
// the same, and only optional fields may be omitted — tsc rejects
|
|
5178
|
-
// omission of required fields before lowering, undefined-armed or not).
|
|
5179
|
-
// A REQUIRED missing field keeps the shape-mismatch rejection (possible
|
|
5180
|
-
// through `as`: the cast smuggles a narrower literal past freshness).
|
|
5181
|
-
if (droppedNames.size > 0) {
|
|
5182
|
-
const narrowed = shape.fields.filter((f) => !droppedNames.has(f.name));
|
|
5183
|
-
const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
|
|
5184
|
-
type = { kind: "record", shapeId: narrowedId };
|
|
5185
|
-
shape = lowerer.shapes.get(narrowedId);
|
|
5186
|
-
}
|
|
5187
|
-
if (fields.filter((f) => !f.overflow).length !== shape.fields.length) {
|
|
5188
|
-
const provided = new Set(fields.filter((f) => !f.overflow).map((f) => f.name));
|
|
5189
|
-
for (const f of shape.fields) {
|
|
5190
|
-
if (provided.has(f.name))
|
|
5191
|
-
continue;
|
|
5192
|
-
// 'unknown' fields complete with the dyn undefined — the absent
|
|
5193
|
-
// property reads as undefined in Node, and a dyn slot holds
|
|
5194
|
-
// exactly that (the options-record call shape against
|
|
5195
|
-
// `{ plugins: unknown, ... }` — a JS caller the checker admits).
|
|
5196
|
-
const absent = lowerer.wrappedUndefined(f.type, loc) ?? (f.type.kind === "dyn" ? dynUndefinedExpr(loc) : null);
|
|
5197
|
-
if (!absent)
|
|
5198
|
-
throw shapeMismatch(expr); // only optional (undefined-armed) and 'unknown' fields may be omitted
|
|
5199
|
-
fields.push({ name: f.name, value: absent });
|
|
5200
3673
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
if (!ts.isConditionalExpression(e))
|
|
5217
|
-
return null;
|
|
5218
|
-
const unwrap = (a) => {
|
|
5219
|
-
let x = a;
|
|
5220
|
-
while (ts.isParenthesizedExpression(x))
|
|
5221
|
-
x = x.expression;
|
|
5222
|
-
return x;
|
|
5223
|
-
};
|
|
5224
|
-
const whenTrue = unwrap(e.whenTrue);
|
|
5225
|
-
const whenFalse = unwrap(e.whenFalse);
|
|
5226
|
-
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
5227
|
-
return "unsupported";
|
|
5228
|
-
}
|
|
5229
|
-
const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0;
|
|
5230
|
-
const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0;
|
|
5231
|
-
if (!trueCarries && !falseCarries)
|
|
5232
|
-
return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar
|
|
5233
|
-
const carrier = trueCarries ? whenTrue : whenFalse;
|
|
5234
|
-
const props = [];
|
|
5235
|
-
for (const p of carrier.properties) {
|
|
5236
|
-
if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
|
5237
|
-
p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
|
|
5238
|
-
props.push(p);
|
|
3674
|
+
// Elements flow into the element slot like an assignment: union
|
|
3675
|
+
// elements wrap plain arm values (`[1, "a"]` as (number | string)[]),
|
|
3676
|
+
// holes reject inside lowerExpr.
|
|
3677
|
+
let lowered;
|
|
3678
|
+
const directOptionalCandidate = runtimeOptionalElements.has(i) ||
|
|
3679
|
+
ts.isElementAccessExpression(el) ||
|
|
3680
|
+
(ts.isCallExpression(el) && ts.isPropertyAccessExpression(el.expression) &&
|
|
3681
|
+
(el.expression.name.text === "pop" || el.expression.name.text === "shift"));
|
|
3682
|
+
if (directOptionalCandidate) {
|
|
3683
|
+
const raw = lowerer.lowerExpr(el);
|
|
3684
|
+
if (!typeEquals(raw.type, type.elem)) {
|
|
3685
|
+
needsStateBuild = true;
|
|
3686
|
+
return raw;
|
|
3687
|
+
}
|
|
3688
|
+
lowered = raw;
|
|
5239
3689
|
}
|
|
5240
3690
|
else {
|
|
5241
|
-
|
|
5242
|
-
}
|
|
5243
|
-
}
|
|
5244
|
-
return { cond: e.condition, props, whenTrue: trueCarries };
|
|
5245
|
-
}
|
|
5246
|
-
/** `{ ...idx, ...idx2 }` typed a DECLARED shape (no index signature) — the
|
|
5247
|
-
* defaults-merge idiom over runtime-keyed sources (`{ ...fromEntries(a),
|
|
5248
|
-
* ...fromEntries(b) }` typed AppConfig). Lowers to ONE interned helper
|
|
5249
|
-
* call: sources evaluate once each as arguments (source order — computed
|
|
5250
|
-
* sources included, JS's evaluate-once), the result starts all-undefined
|
|
5251
|
-
* (every target field must be optional — the runtime keys decide
|
|
5252
|
-
* presence), and each source's keys apply in JS own-key order with a
|
|
5253
|
-
* per-key dispatch onto the declared fields: a matching key writes the
|
|
5254
|
-
* field — identity when the source's value slot IS the field type, a
|
|
5255
|
-
* validated extraction otherwise (a dyn slot dynChecks; a union slot
|
|
5256
|
-
* re-tags arm-by-arm, and a value outside the field's arms throws the
|
|
5257
|
-
* catchable TypeError — divergence 34's keyed-write stance, where Node's
|
|
5258
|
-
* untyped copy would store the lie) — and a key naming NO declared field
|
|
5259
|
-
* is DROPPED (the shape cannot represent it; Node keeps it invisibly —
|
|
5260
|
-
* divergence 68). Later contributors overwrite earlier ones
|
|
5261
|
-
* (last-write-wins). Sources must be PURE index-signature records. */
|
|
5262
|
-
function lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc) {
|
|
5263
|
-
const srcs = [];
|
|
5264
|
-
for (const prop of expr.properties) {
|
|
5265
|
-
const spread = prop; // caller-checked: all spreads
|
|
5266
|
-
const value = lowerer.lowerExpr(spread.expression);
|
|
5267
|
-
const srcShape = value.type.kind === "record" ? lowerer.shapes.get(value.type.shapeId) : undefined;
|
|
5268
|
-
if (value.type.kind !== "record" || !srcShape?.indexValue || srcShape.tuple || srcShape.fields.length > 0) {
|
|
5269
|
-
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)`);
|
|
5270
|
-
}
|
|
5271
|
-
srcs.push({ value, shapeId: value.type.shapeId, iv: srcShape.indexValue });
|
|
5272
|
-
}
|
|
5273
|
-
// Every target field must be optional: absent keys leave the undefined
|
|
5274
|
-
// arm, exactly the unset-optional representation.
|
|
5275
|
-
for (const f of shape.fields) {
|
|
5276
|
-
if (f.type.kind !== "union" || lowerer.armTag(f.type.unionId, UNDEFINED_T) < 0) {
|
|
5277
|
-
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)`);
|
|
5278
|
-
}
|
|
5279
|
-
}
|
|
5280
|
-
// Per (source value slot → field) conversion, checked up front so the
|
|
5281
|
-
// fence fires at the literal, not inside the interned helper.
|
|
5282
|
-
const conv = (iv, f, v) => {
|
|
5283
|
-
if (typeEquals(iv, f.type))
|
|
5284
|
-
return { value: v };
|
|
5285
|
-
if (iv.kind === "dyn") {
|
|
5286
|
-
return { value: { kind: "dynCheck", value: v, type: f.type, loc } };
|
|
5287
|
-
}
|
|
5288
|
-
if (iv.kind === "union" && f.type.kind === "union") {
|
|
5289
|
-
const fUnion = f.type;
|
|
5290
|
-
const def = lowerer.unions.get(iv.unionId);
|
|
5291
|
-
const fDef = lowerer.unions.get(fUnion.unionId);
|
|
5292
|
-
if (def && fDef && def.arms.some((a) => lowerer.armTag(fUnion.unionId, a) >= 0)) {
|
|
5293
|
-
// Arm-by-arm validated re-tag, inline in the helper: matching
|
|
5294
|
-
// arms map by identity (unit arms re-wrap, value arms narrow and
|
|
5295
|
-
// wrap); anything else throws the catchable TypeError.
|
|
5296
|
-
return {
|
|
5297
|
-
stmts: (write) => {
|
|
5298
|
-
const chain = (i) => {
|
|
5299
|
-
if (i >= def.arms.length) {
|
|
5300
|
-
return [{
|
|
5301
|
-
kind: "throw",
|
|
5302
|
-
value: {
|
|
5303
|
-
kind: "libCall",
|
|
5304
|
-
fn: "error.new",
|
|
5305
|
-
args: [{ kind: "strLit", value: `expected ${lowerer.fmt(fUnion)} at $.${f.name}`, type: STRING, loc }],
|
|
5306
|
-
type: { kind: "object", className: "%TypeError" },
|
|
5307
|
-
loc,
|
|
5308
|
-
},
|
|
5309
|
-
loc,
|
|
5310
|
-
}];
|
|
5311
|
-
}
|
|
5312
|
-
const arm = def.arms[i];
|
|
5313
|
-
const toTag = lowerer.armTag(fUnion.unionId, arm);
|
|
5314
|
-
if (toTag < 0)
|
|
5315
|
-
return chain(i + 1);
|
|
5316
|
-
const extracted = isUnitType(arm)
|
|
5317
|
-
? { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }, type: fUnion, loc }
|
|
5318
|
-
: { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unionNarrow", unionId: iv.unionId, tag: i, value: v, type: arm, loc }, type: fUnion, loc };
|
|
5319
|
-
return [{
|
|
5320
|
-
kind: "if",
|
|
5321
|
-
cond: { kind: "unionIsTag", unionId: iv.unionId, tag: i, negated: false, value: v, type: BOOL, loc },
|
|
5322
|
-
then: [write(extracted)],
|
|
5323
|
-
else_: chain(i + 1),
|
|
5324
|
-
loc,
|
|
5325
|
-
}];
|
|
5326
|
-
};
|
|
5327
|
-
return chain(0);
|
|
5328
|
-
},
|
|
5329
|
-
};
|
|
5330
|
-
}
|
|
3691
|
+
lowered = lowerer.lowerExprExpecting(el, type.elem);
|
|
5331
3692
|
}
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
if (!
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
const thenBody = "value" in c ? [write(c.value)] : c.stmts(write);
|
|
5368
|
-
return [{
|
|
5369
|
-
kind: "if",
|
|
5370
|
-
cond: { kind: "strEq", negated: false, left: kRef, right: { kind: "strLit", value: f.name, type: STRING, loc }, type: BOOL, loc },
|
|
5371
|
-
then: thenBody,
|
|
5372
|
-
else_: rest.length > 0 ? rest : null,
|
|
5373
|
-
loc,
|
|
5374
|
-
}];
|
|
5375
|
-
}, []);
|
|
5376
|
-
body.push({ kind: "varDecl", localId: `ks${j}.0`, init: { kind: "recordOvfKeys", obj: sRef, shapeId: s.shapeId, type: ksT, loc }, loc }, {
|
|
5377
|
-
kind: "for",
|
|
5378
|
-
init: { kind: "varDecl", localId: `i${j}.0`, init: numLit(0, loc), loc },
|
|
5379
|
-
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 },
|
|
5380
|
-
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 },
|
|
5381
|
-
body: [
|
|
5382
|
-
{ 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 },
|
|
5383
|
-
{ kind: "varDecl", localId: `v${j}.0`, init: { kind: "recordKeyGet", obj: sRef, shapeId: s.shapeId, key: kRef, overflowOnly: true, type: s.iv, loc }, loc },
|
|
5384
|
-
...dispatch,
|
|
5385
|
-
],
|
|
3693
|
+
if (!typeEquals(lowered.type, type.elem))
|
|
3694
|
+
lowerer.badType(el, lowerer.typeOf(el));
|
|
3695
|
+
return lowered;
|
|
3696
|
+
});
|
|
3697
|
+
if (!needsStateBuild) {
|
|
3698
|
+
return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
|
|
3699
|
+
}
|
|
3700
|
+
const out = lowerer.declareHiddenLocal("%arrayLit", type);
|
|
3701
|
+
const outRef = varRef(out.id, type, loc);
|
|
3702
|
+
const body = [
|
|
3703
|
+
{ kind: "varDecl", localId: out.id, init: { kind: "arrayLit", elems: [], type, loc }, loc },
|
|
3704
|
+
];
|
|
3705
|
+
const spreadSet = new Set(spreads);
|
|
3706
|
+
const currentLength = () => ({
|
|
3707
|
+
kind: "arrIntrinsic",
|
|
3708
|
+
method: "length",
|
|
3709
|
+
receiver: outRef,
|
|
3710
|
+
args: [],
|
|
3711
|
+
type: F64,
|
|
3712
|
+
loc,
|
|
3713
|
+
});
|
|
3714
|
+
const nextLength = () => ({
|
|
3715
|
+
kind: "bin",
|
|
3716
|
+
op: "+",
|
|
3717
|
+
left: currentLength(),
|
|
3718
|
+
right: { kind: "numLit", value: 1, type: F64, loc },
|
|
3719
|
+
type: F64,
|
|
3720
|
+
loc,
|
|
3721
|
+
});
|
|
3722
|
+
for (let i = 0; i < elems.length; i++) {
|
|
3723
|
+
const el = elems[i];
|
|
3724
|
+
if (spreadSet.has(i)) {
|
|
3725
|
+
body.push({
|
|
3726
|
+
kind: "exprStmt",
|
|
3727
|
+
expr: { kind: "arrIntrinsic", method: "pushSpread", receiver: outRef, args: [el], type: F64, loc },
|
|
5386
3728
|
loc,
|
|
5387
3729
|
});
|
|
5388
|
-
|
|
5389
|
-
body.push({ kind: "return", value: outRef, loc });
|
|
5390
|
-
lowerer.liftedFns.push({
|
|
5391
|
-
name: helper,
|
|
5392
|
-
params,
|
|
5393
|
-
returnType: type,
|
|
5394
|
-
locals,
|
|
5395
|
-
body,
|
|
5396
|
-
loc,
|
|
5397
|
-
});
|
|
5398
|
-
// Registered only after a fence-free build: a conv() fence mid-build
|
|
5399
|
-
// must not leave a phantom helper behind for the next literal.
|
|
5400
|
-
lowerer.widthHelpers.set(key, helper);
|
|
5401
|
-
}
|
|
5402
|
-
return { kind: "call", callee: helper, args: srcs.map((s) => s.value), type, loc };
|
|
5403
|
-
}
|
|
5404
|
-
/** The value of a shorthand property (`{ x }`): the binding `x` refers to.
|
|
5405
|
-
* The property name's own symbol is the PROPERTY, so resolution goes
|
|
5406
|
-
* through getShorthandAssignmentValueSymbol. */
|
|
5407
|
-
export function lowerShorthandValue(lowerer, prop) {
|
|
5408
|
-
// 7 types the shorthand's name as PropertyName; it is always an
|
|
5409
|
-
// Identifier (the grammar allows nothing else in shorthand position).
|
|
5410
|
-
const propName = prop.name;
|
|
5411
|
-
const loc = locOf(propName);
|
|
5412
|
-
const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
|
|
5413
|
-
if (symbol) {
|
|
5414
|
-
if (lowerer.ctx.selfSymbol === symbol) {
|
|
5415
|
-
return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
|
|
5416
|
-
}
|
|
5417
|
-
const local = lowerer.resolveKey(symbol, propName);
|
|
5418
|
-
if (local) {
|
|
5419
|
-
return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, propName);
|
|
5420
|
-
}
|
|
5421
|
-
const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
|
5422
|
-
lowerer.flushDeferred(resolved);
|
|
5423
|
-
const g = lowerer.globalsBySymbol.get(resolved);
|
|
5424
|
-
if (g) {
|
|
5425
|
-
return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName);
|
|
3730
|
+
continue;
|
|
5426
3731
|
}
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
(ts.isSourceFile(decl.parent) || lowerer.nsBlocks.get(decl.parent) === "flattened")) {
|
|
5431
|
-
lowerer.noteEdge(sig.name);
|
|
5432
|
-
const funcType = {
|
|
5433
|
-
kind: "func",
|
|
5434
|
-
params: sig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type),
|
|
5435
|
-
ret: sig.returnType,
|
|
5436
|
-
...(sig.params.some((p) => p.mode === "dynRest") ? { rest: true } : {}),
|
|
5437
|
-
};
|
|
5438
|
-
lowerer.requireExactArityValue(prop, propName, sig.params, funcType);
|
|
5439
|
-
return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
|
|
3732
|
+
if (holes.has(i)) {
|
|
3733
|
+
body.push({ kind: "arraySetLength", arr: outRef, length: nextLength(), loc });
|
|
3734
|
+
continue;
|
|
5440
3735
|
}
|
|
5441
|
-
|
|
5442
|
-
|
|
3736
|
+
let value = el;
|
|
3737
|
+
if (!typeEquals(value.type, type.elem)) {
|
|
3738
|
+
const temp = lowerer.declareHiddenLocal("%arrayValue", value.type);
|
|
3739
|
+
body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
|
3740
|
+
value = varRef(temp.id, temp.type, loc);
|
|
5443
3741
|
}
|
|
3742
|
+
body.push(arrayValueStore(lowerer, outRef, currentLength(), value, type.elem, loc));
|
|
5444
3743
|
}
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
|
5453
|
-
lowerer.unsupported("SC1090", node, "references to 'this' in object literal methods");
|
|
5454
|
-
}
|
|
5455
|
-
ts.forEachChild(node, (child) => lowerer.rejectThisInObjectMethod(child));
|
|
3744
|
+
return {
|
|
3745
|
+
kind: "seqExpr",
|
|
3746
|
+
stmts: body,
|
|
3747
|
+
result: outRef,
|
|
3748
|
+
type,
|
|
3749
|
+
loc,
|
|
3750
|
+
};
|
|
5456
3751
|
}
|
|
5457
|
-
/**
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
3752
|
+
/** Convert an undefined-armed numeric value at an arithmetic use. Ordinary
|
|
3753
|
+
* reads preserve their tagged value; JavaScript's ToNumber(undefined) is NaN,
|
|
3754
|
+
* so compounds and binary arithmetic use this checked one-time extraction. */
|
|
3755
|
+
export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
|
|
3756
|
+
if (operand.type.kind !== "union" || lowerer.armTag(operand.type.unionId, UNDEFINED_T) < 0)
|
|
3757
|
+
return operand;
|
|
3758
|
+
const directNumber = lowerer.stripUndefinedArm(operand.type).kind === "f64";
|
|
3759
|
+
const checkerNumber = narrowedNode !== undefined && lowerer.mapTypeOf(lowerer.typeOf(narrowedNode))?.kind === "f64";
|
|
3760
|
+
if (!directNumber && (!checkerNumber || lowerer.armTag(operand.type.unionId, F64) < 0))
|
|
3761
|
+
return operand;
|
|
3762
|
+
const undefTag = lowerer.armTag(operand.type.unionId, UNDEFINED_T);
|
|
3763
|
+
const valueTag = lowerer.armTag(operand.type.unionId, F64);
|
|
3764
|
+
if (undefTag < 0 || valueTag < 0)
|
|
3765
|
+
return operand;
|
|
3766
|
+
const checked = directNumber ? null : lowerer.narrowedArmHelper(operand.type.unionId, F64, loc);
|
|
3767
|
+
if (!directNumber && !checked)
|
|
3768
|
+
return operand;
|
|
3769
|
+
let stable = operand;
|
|
3770
|
+
let prefix = [];
|
|
3771
|
+
if (!isSafeToRepeat(operand)) {
|
|
3772
|
+
const tmp = lowerer.declareHiddenLocal("%arith", operand.type);
|
|
3773
|
+
stable = varRef(tmp.id, operand.type, loc);
|
|
3774
|
+
prefix = [{ kind: "varDecl", localId: tmp.id, init: operand, loc }];
|
|
3775
|
+
}
|
|
3776
|
+
const value = {
|
|
3777
|
+
kind: "ternary",
|
|
3778
|
+
cond: { kind: "unionIsTag", unionId: operand.type.unionId, tag: undefTag, negated: false, value: stable, type: BOOL, loc },
|
|
3779
|
+
then: { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc },
|
|
3780
|
+
else_: checked
|
|
3781
|
+
? { kind: "call", callee: checked, args: [stable], type: F64, loc }
|
|
3782
|
+
: { kind: "unionNarrow", unionId: operand.type.unionId, tag: valueTag, value: stable, type: F64, loc },
|
|
3783
|
+
type: F64,
|
|
3784
|
+
loc,
|
|
3785
|
+
};
|
|
3786
|
+
return prefix.length === 0 ? value : { kind: "seqExpr", stmts: prefix, result: value, type: F64, loc };
|
|
5468
3787
|
}
|
|
5469
3788
|
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
|
|
5470
3789
|
* string-key element access (`a["length"]`) and string indexing (`s[0]`
|
|
@@ -5477,7 +3796,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5477
3796
|
}
|
|
5478
3797
|
// `a?.b[i]` — the tail of a chain whose token sits deeper: the whole
|
|
5479
3798
|
// tail short-circuits with the guard.
|
|
5480
|
-
if (!expr.questionDotToken &&
|
|
3799
|
+
if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
|
|
5481
3800
|
return lowerer.lowerOptionalChain(expr);
|
|
5482
3801
|
}
|
|
5483
3802
|
// Enum accesses in element clothing — `E["A"]` forward reads and
|
|
@@ -5488,6 +3807,9 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5488
3807
|
if (en)
|
|
5489
3808
|
return en;
|
|
5490
3809
|
}
|
|
3810
|
+
const cachedModule = lowerRequireCacheElement(lowerer, expr);
|
|
3811
|
+
if (cachedModule)
|
|
3812
|
+
return cachedModule;
|
|
5491
3813
|
// Native Web handles use the same supported surface for bracket and dot
|
|
5492
3814
|
// spellings. Fence a statically-known unsupported key before the generic
|
|
5493
3815
|
// checked-dynamic element-read path can turn it into a runtime missing
|
|
@@ -5584,7 +3906,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5584
3906
|
? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
|
|
5585
3907
|
: null;
|
|
5586
3908
|
if (key) {
|
|
5587
|
-
const opt =
|
|
3909
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
5588
3910
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc: locOf(expr) }, expr);
|
|
5589
3911
|
}
|
|
5590
3912
|
}
|
|
@@ -5592,7 +3914,16 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5592
3914
|
// result; any invalid index traps (the array discipline; JS would
|
|
5593
3915
|
// read undefined — divergence 4's policy).
|
|
5594
3916
|
if (receiverIr?.kind === "bytes") {
|
|
5595
|
-
|
|
3917
|
+
let recv = lowerer.lowerExpr(expr.expression);
|
|
3918
|
+
if (recv.type.kind === "union" && lowerer.armTag(recv.type.unionId, UNDEFINED_T) >= 0) {
|
|
3919
|
+
const present = lowerer.stripUndefinedArm(recv.type);
|
|
3920
|
+
const helper = present.kind === "bytes"
|
|
3921
|
+
? lowerer.narrowedArmHelper(recv.type.unionId, present, locOf(expr.expression))
|
|
3922
|
+
: null;
|
|
3923
|
+
recv = helper
|
|
3924
|
+
? { kind: "call", callee: helper, args: [recv], type: present, loc: locOf(expr.expression) }
|
|
3925
|
+
: lowerer.maybeNarrow(recv, expr.expression);
|
|
3926
|
+
}
|
|
5596
3927
|
// A typed-array .d.ts surface whose VALUE is an island handle
|
|
5597
3928
|
// (`data()[0]` on a declared Uint8Array return — call results only
|
|
5598
3929
|
// exit primitives eagerly): the engine element read, exiting at the
|
|
@@ -5612,7 +3943,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5612
3943
|
if (receiverIr?.kind === "record") {
|
|
5613
3944
|
const shape = lowerer.shapes.get(receiverIr.shapeId);
|
|
5614
3945
|
if (shape?.tuple) {
|
|
5615
|
-
|
|
3946
|
+
let obj = lowerer.lowerExpr(expr.expression);
|
|
5616
3947
|
// A tuple-typed .d.ts surface whose VALUE is an island handle
|
|
5617
3948
|
// (`pair()[0]` on a declared `[number, string]` return — the
|
|
5618
3949
|
// anonymous tuple type has no npm symbol, so the checker maps it
|
|
@@ -5621,22 +3952,38 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5621
3952
|
// like the array path.
|
|
5622
3953
|
if (obj.type.kind === "jsval")
|
|
5623
3954
|
return islandElementRead(lowerer, expr, obj);
|
|
3955
|
+
if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
|
|
3956
|
+
// The checker sees the outer `arrays[i]` as a tuple when
|
|
3957
|
+
// noUncheckedIndexedAccess is disabled, but the lowered read is
|
|
3958
|
+
// still tuple | undefined. A nested tuple field must validate the
|
|
3959
|
+
// outer tag before recordGet; an unchecked unionNarrow would let a
|
|
3960
|
+
// missing outer element reach the record backend as a bad pointer.
|
|
3961
|
+
const present = lowerer.stripUndefinedArm(obj.type);
|
|
3962
|
+
const helper = present.kind === "record"
|
|
3963
|
+
? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(expr.expression))
|
|
3964
|
+
: null;
|
|
3965
|
+
obj = helper
|
|
3966
|
+
? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(expr.expression) }
|
|
3967
|
+
: lowerer.maybeNarrow(obj, expr.expression);
|
|
3968
|
+
}
|
|
5624
3969
|
const idx = tupleLiteralIndex(expr.argumentExpression);
|
|
5625
3970
|
if (idx === null) {
|
|
5626
3971
|
lowerer.unsupported("SC1090", expr.argumentExpression, "tuple indexing with a non-literal index (tuples are fixed-shape — use t[0], t[1], ...)");
|
|
5627
3972
|
}
|
|
5628
|
-
const
|
|
3973
|
+
const shapeId = obj.type.kind === "record" ? obj.type.shapeId : receiverIr.shapeId;
|
|
3974
|
+
const actualShape = lowerer.shapes.get(shapeId) ?? shape;
|
|
3975
|
+
const fieldType = actualShape.fields.find((f) => f.name === String(idx))?.type;
|
|
5629
3976
|
if (fieldType === undefined)
|
|
5630
3977
|
lowerer.badType(expr, lowerer.typeOf(expr)); // OOB smuggled past tsc
|
|
5631
3978
|
const get = {
|
|
5632
3979
|
kind: "recordGet",
|
|
5633
3980
|
obj,
|
|
5634
|
-
shapeId
|
|
3981
|
+
shapeId,
|
|
5635
3982
|
field: String(idx),
|
|
5636
3983
|
type: fieldType,
|
|
5637
3984
|
loc: locOf(expr),
|
|
5638
3985
|
};
|
|
5639
|
-
return lowerer.maybeNarrow(get, expr);
|
|
3986
|
+
return lowerer.isRuntimeOptionalField(shapeId, String(idx)) ? get : lowerer.maybeNarrow(get, expr);
|
|
5640
3987
|
}
|
|
5641
3988
|
if (shape && !shape.tuple) {
|
|
5642
3989
|
// A record-typed .d.ts surface whose VALUE is an island handle
|
|
@@ -5673,7 +4020,7 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5673
4020
|
? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
|
|
5674
4021
|
: null;
|
|
5675
4022
|
if (key) {
|
|
5676
|
-
const opt =
|
|
4023
|
+
const opt = hasOptionalChainGuard(expr.expression);
|
|
5677
4024
|
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: obj, type: DYN, loc: locOf(expr) }, expr);
|
|
5678
4025
|
}
|
|
5679
4026
|
}
|
|
@@ -5695,6 +4042,17 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5695
4042
|
const value = lowerer.lowerExpr(expr.expression);
|
|
5696
4043
|
if (value.type.kind === "union") {
|
|
5697
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
|
+
}
|
|
5698
4056
|
if (key.type.kind === "string") {
|
|
5699
4057
|
const lit = ts.isStringLiteral(expr.argumentExpression)
|
|
5700
4058
|
? expr.argumentExpression.text
|
|
@@ -5751,6 +4109,65 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5751
4109
|
lowerer.unsupported("SC1090", expr, "element access on non-array values");
|
|
5752
4110
|
}
|
|
5753
4111
|
let arr = lowerer.lowerExpr(expr.expression);
|
|
4112
|
+
const runtimeOptionalSource = lowerer.runtimeOptionalSourceValue(expr.expression, arr);
|
|
4113
|
+
if (runtimeOptionalSource?.kind === "varRef" &&
|
|
4114
|
+
runtimeOptionalSource.type.kind === "union" &&
|
|
4115
|
+
arr.type.kind === "array") {
|
|
4116
|
+
// lowerer.lowerElementAccess has already recognized a captured
|
|
4117
|
+
// array | undefined receiver and supplied its checked extraction as
|
|
4118
|
+
// the expression override. Rebuild that check here so a missing
|
|
4119
|
+
// receiver throws the member-access error Node exposes, rather than
|
|
4120
|
+
// the generic target-union narrowing error. The stored receiver is a
|
|
4121
|
+
// varRef, so both the tag test and extraction read the same value
|
|
4122
|
+
// without repeating an effect.
|
|
4123
|
+
const stored = runtimeOptionalSource;
|
|
4124
|
+
if (stored.type.kind !== "union")
|
|
4125
|
+
throw new InternalCompilerError("runtime-optional array source lost its union storage");
|
|
4126
|
+
const unionId = stored.type.unionId;
|
|
4127
|
+
const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
|
|
4128
|
+
const valueTag = lowerer.armTag(unionId, arr.type);
|
|
4129
|
+
const property = runtimeOptionalElementKey(expr.argumentExpression);
|
|
4130
|
+
if (undefTag >= 0 && valueTag >= 0 && property !== null) {
|
|
4131
|
+
arr = {
|
|
4132
|
+
kind: "ternary",
|
|
4133
|
+
cond: {
|
|
4134
|
+
kind: "unionIsTag",
|
|
4135
|
+
unionId,
|
|
4136
|
+
tag: undefTag,
|
|
4137
|
+
negated: false,
|
|
4138
|
+
value: stored,
|
|
4139
|
+
type: BOOL,
|
|
4140
|
+
loc: locOf(expr.expression),
|
|
4141
|
+
},
|
|
4142
|
+
then: nodeThrowExpr(1, "", `Cannot read properties of undefined (reading '${property}')`, arr.type, locOf(expr.expression)),
|
|
4143
|
+
else_: {
|
|
4144
|
+
kind: "unionNarrow",
|
|
4145
|
+
unionId,
|
|
4146
|
+
tag: valueTag,
|
|
4147
|
+
value: stored,
|
|
4148
|
+
type: arr.type,
|
|
4149
|
+
loc: locOf(expr.expression),
|
|
4150
|
+
},
|
|
4151
|
+
type: arr.type,
|
|
4152
|
+
loc: locOf(expr.expression),
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
if (arr.type.kind === "union" &&
|
|
4157
|
+
lowerer.armTag(arr.type.unionId, UNDEFINED_T) >= 0 &&
|
|
4158
|
+
receiverIr.kind === "array") {
|
|
4159
|
+
// As with tuple receivers above, an unchecked outer read can be
|
|
4160
|
+
// undefined even when the checker reports an array. Extract the
|
|
4161
|
+
// present array arm with a runtime tag check before arrayGet or the
|
|
4162
|
+
// safe indexed-read helper runs.
|
|
4163
|
+
const present = lowerer.stripUndefinedArm(arr.type);
|
|
4164
|
+
const helper = present.kind === "array"
|
|
4165
|
+
? lowerer.narrowedArmHelper(arr.type.unionId, present, locOf(expr.expression))
|
|
4166
|
+
: null;
|
|
4167
|
+
arr = helper
|
|
4168
|
+
? { kind: "call", callee: helper, args: [arr], type: present, loc: locOf(expr.expression) }
|
|
4169
|
+
: lowerer.maybeNarrow(arr, expr.expression);
|
|
4170
|
+
}
|
|
5754
4171
|
// The checker sees an array but the VALUE is an island handle (an
|
|
5755
4172
|
// array-typed .d.ts surface: `parts()[0]` on a declared `string[]`
|
|
5756
4173
|
// return, `issue.path[0]` on a declared member — arrays never exit
|
|
@@ -5782,12 +4199,22 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5782
4199
|
// signature where the .d.ts says Function[], which would flatten the
|
|
5783
4200
|
// element to a zero-parameter closure).
|
|
5784
4201
|
const elemT = arr.type.kind === "array" ? arr.type.elem : receiverIr.elem;
|
|
5785
|
-
//
|
|
5786
|
-
//
|
|
5787
|
-
//
|
|
5788
|
-
//
|
|
5789
|
-
//
|
|
5790
|
-
|
|
4202
|
+
// Every ordinary array read is a JavaScript property read: an invalid
|
|
4203
|
+
// index answers undefined, even when the checker was built without
|
|
4204
|
+
// noUncheckedIndexedAccess. Keep the explicit optional union in the IR
|
|
4205
|
+
// so it can flow through templates, locals, calls, returns, operators,
|
|
4206
|
+
// and narrowing. The helper performs one length/proof check and then
|
|
4207
|
+
// uses the dense getter for the proven-present path, so this does not
|
|
4208
|
+
// change the storage representation or the low-level getter itself.
|
|
4209
|
+
if (arr.type.kind === "array") {
|
|
4210
|
+
// A non-null assertion is the explicit proven-present form. Preserve
|
|
4211
|
+
// the dense getter's established bounds trap here; turning `xs[i]!`
|
|
4212
|
+
// into an optional union would change library ABI checks and the
|
|
4213
|
+
// existing catchable RangeError contract for an out-of-bounds assert.
|
|
4214
|
+
const provenPresent = ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr;
|
|
4215
|
+
if (provenPresent) {
|
|
4216
|
+
return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
|
|
4217
|
+
}
|
|
5791
4218
|
const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr));
|
|
5792
4219
|
if (safe)
|
|
5793
4220
|
return safe;
|
|
@@ -5807,16 +4234,6 @@ export function lowerElementAccess(lowerer, expr) {
|
|
|
5807
4234
|
// narrows literal-index element accesses).
|
|
5808
4235
|
return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
|
|
5809
4236
|
}
|
|
5810
|
-
function isFreshArrayProbe(node) {
|
|
5811
|
-
let current = node;
|
|
5812
|
-
while (ts.isParenthesizedExpression(current))
|
|
5813
|
-
current = current.expression;
|
|
5814
|
-
return (ts.isCallExpression(current) &&
|
|
5815
|
-
ts.isPropertyAccessExpression(current.expression) &&
|
|
5816
|
-
(current.expression.name.text === "filter" ||
|
|
5817
|
-
current.expression.name.text === "slice" ||
|
|
5818
|
-
current.expression.name.text === "sort"));
|
|
5819
|
-
}
|
|
5820
4237
|
/** `o[k]` where the RECEIVER is an island value — a jsval-mapped checker
|
|
5821
4238
|
* type, or an array/tuple-typed .d.ts surface whose lowered value is a
|
|
5822
4239
|
* handle. The key marshals in (any JS key kind — the engine does its
|
|
@@ -5883,8 +4300,7 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
|
|
|
5883
4300
|
// Number AND checked-dynamic keys ride the JS-exact formatter —
|
|
5884
4301
|
// property keys ARE strings (o[k] is o[String(k)] in JS), and a dyn
|
|
5885
4302
|
// key (agent.sockets[agent.getName(...)]) stringifies the same way.
|
|
5886
|
-
|
|
5887
|
-
dk = lowerer.ensureString(dk, keyNode);
|
|
4303
|
+
dk = lowerRecordPropertyKey(lowerer, dk, keyNode);
|
|
5888
4304
|
if (dk.type.kind !== "string") {
|
|
5889
4305
|
lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
|
|
5890
4306
|
}
|
|
@@ -5914,11 +4330,23 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
|
|
|
5914
4330
|
: lowerer.lowerExpr(keyNode);
|
|
5915
4331
|
// Runtime NUMBER keys ride the JS-exact formatter (o[n] is o[String(n)]
|
|
5916
4332
|
// in JS — the number-keyed-signature access path).
|
|
5917
|
-
|
|
5918
|
-
key = lowerer.ensureString(key, keyNode);
|
|
4333
|
+
key = lowerRecordPropertyKey(lowerer, key, keyNode);
|
|
5919
4334
|
if (key.type.kind !== "string") {
|
|
5920
4335
|
lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
|
|
5921
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
|
+
}
|
|
5922
4350
|
// The DECLARED result type of the access — the index signature's value
|
|
5923
4351
|
// type (armed with undefined under noUncheckedIndexedAccess), or the
|
|
5924
4352
|
// declared fields' one common type on signature-free shapes (tsc's
|
|
@@ -5958,6 +4386,16 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
|
|
|
5958
4386
|
};
|
|
5959
4387
|
return includeUndefined ? read : lowerer.maybeNarrow(read, expr);
|
|
5960
4388
|
}
|
|
4389
|
+
/** ToPropertyKey for represented record keys. Missing array-derived string
|
|
4390
|
+
* values remain real undefined values, whose JavaScript property spelling
|
|
4391
|
+
* is the string "undefined". */
|
|
4392
|
+
function lowerRecordPropertyKey(lowerer, key, node) {
|
|
4393
|
+
if (key.type.kind === "f64" || key.type.kind === "bool" || key.type.kind === "dyn" ||
|
|
4394
|
+
lowerer.runtimeOptionalWidening(key.type, STRING) !== null) {
|
|
4395
|
+
return lowerer.ensureString(key, node);
|
|
4396
|
+
}
|
|
4397
|
+
return key;
|
|
4398
|
+
}
|
|
5961
4399
|
/** A keyed read used only to observe absence (`if (map[k])`, or the left
|
|
5962
4400
|
* side of `||`/`??`). JavaScript answers undefined for missing record keys
|
|
5963
4401
|
* and array indices even when noUncheckedIndexedAccess is disabled. Keep
|
|
@@ -6000,6 +4438,9 @@ export function lowerAbsenceProbe(lowerer, node) {
|
|
|
6000
4438
|
}
|
|
6001
4439
|
if (!ts.isElementAccessExpression(expr))
|
|
6002
4440
|
return null;
|
|
4441
|
+
const cachedModule = lowerRequireCacheElement(lowerer, expr);
|
|
4442
|
+
if (cachedModule)
|
|
4443
|
+
return cachedModule;
|
|
6003
4444
|
const header = lowerer.lowerHttpHeadersElement(expr);
|
|
6004
4445
|
if (header)
|
|
6005
4446
|
return header;
|
|
@@ -6097,6 +4538,7 @@ function tupleLiteralIndex(node) {
|
|
|
6097
4538
|
* assignment, produce no value in our subset). */
|
|
6098
4539
|
export function lowerElementWrite(lowerer, expr) {
|
|
6099
4540
|
const target = expr.left;
|
|
4541
|
+
fenceNodeModuleMutation(lowerer, target, "assignment");
|
|
6100
4542
|
// `process.env[key] = v` — the computed twin of the dotted env write:
|
|
6101
4543
|
// setenv(3), string keys and string values only.
|
|
6102
4544
|
if (lowerer.isProcessEnv(target.expression) && !target.questionDotToken) {
|
|
@@ -6229,8 +4671,7 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6229
4671
|
let key = litKey !== null
|
|
6230
4672
|
? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
|
|
6231
4673
|
: lowerer.lowerExpr(target.argumentExpression);
|
|
6232
|
-
|
|
6233
|
-
key = lowerer.ensureString(key, target.argumentExpression);
|
|
4674
|
+
key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
|
|
6234
4675
|
if (key.type.kind !== "string") {
|
|
6235
4676
|
lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
|
|
6236
4677
|
}
|
|
@@ -6308,11 +4749,7 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6308
4749
|
: lowerer.lowerExpr(target.argumentExpression);
|
|
6309
4750
|
// Runtime number/boolean/dyn keys stringify — ToPropertyKey,
|
|
6310
4751
|
// exactly the index-signature path's rule.
|
|
6311
|
-
|
|
6312
|
-
key = lowerer.ensureString(key, target.argumentExpression);
|
|
6313
|
-
if (key.type.kind === "bool" || key.type.kind === "dyn") {
|
|
6314
|
-
key = { kind: "toString", operand: key, type: STRING, loc: locOf(target.argumentExpression) };
|
|
6315
|
-
}
|
|
4752
|
+
key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
|
|
6316
4753
|
if (key.type.kind !== "string") {
|
|
6317
4754
|
lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
|
|
6318
4755
|
}
|
|
@@ -6341,8 +4778,7 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6341
4778
|
: lowerer.lowerExpr(target.argumentExpression);
|
|
6342
4779
|
// Runtime NUMBER keys canonicalize through the JS-exact formatter
|
|
6343
4780
|
// (o[n] = v writes o[String(n)] — the number-keyed-signature path).
|
|
6344
|
-
|
|
6345
|
-
key = lowerer.ensureString(key, target.argumentExpression);
|
|
4781
|
+
key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
|
|
6346
4782
|
if (key.type.kind !== "string") {
|
|
6347
4783
|
lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
|
|
6348
4784
|
}
|
|
@@ -6369,18 +4805,14 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6369
4805
|
// probes the receiver's own lowered world: a dyn value takes the same
|
|
6370
4806
|
// write, anything else falls through to the fences.
|
|
6371
4807
|
if (receiverIr?.kind === "dyn" || receiverIr === null) {
|
|
6372
|
-
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) :
|
|
4808
|
+
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : tryLowerExpression(lowerer, target.expression);
|
|
6373
4809
|
if (obj !== null && obj.type.kind === "dyn") {
|
|
6374
4810
|
const loc = locOf(expr);
|
|
6375
4811
|
const litKey = recordKeyLiteralText(target.argumentExpression);
|
|
6376
4812
|
let key = litKey !== null
|
|
6377
4813
|
? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
|
|
6378
4814
|
: lowerer.lowerExpr(target.argumentExpression);
|
|
6379
|
-
|
|
6380
|
-
key = lowerer.ensureString(key, target.argumentExpression);
|
|
6381
|
-
if (key.type.kind === "bool" || key.type.kind === "dyn") {
|
|
6382
|
-
key = { kind: "toString", operand: key, type: STRING, loc: locOf(target.argumentExpression) };
|
|
6383
|
-
}
|
|
4815
|
+
key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
|
|
6384
4816
|
if (key.type.kind !== "string") {
|
|
6385
4817
|
lowerer.unsupported("SC1090", target.argumentExpression, "indexing checked-dynamic values with non-string or non-number keys");
|
|
6386
4818
|
}
|
|
@@ -6394,7 +4826,7 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6394
4826
|
if (receiverIr?.kind !== "array") {
|
|
6395
4827
|
lowerer.unsupported("SC1090", target, "assignment to non-array elements");
|
|
6396
4828
|
}
|
|
6397
|
-
|
|
4829
|
+
let arr = lowerer.lowerExpr(target.expression);
|
|
6398
4830
|
// The write twin of the island element read: an array-typed .d.ts
|
|
6399
4831
|
// surface whose VALUE is a handle takes the engine keyed write, never
|
|
6400
4832
|
// a static arraySet over a jsval.
|
|
@@ -6404,17 +4836,29 @@ export function lowerElementWrite(lowerer, expr) {
|
|
|
6404
4836
|
const loc = locOf(expr);
|
|
6405
4837
|
return { kind: "exprStmt", expr: { kind: "jsOp", op: "setIdx", args: [arr, key, value], type: VOID, loc }, loc };
|
|
6406
4838
|
}
|
|
4839
|
+
if (arr.type.kind === "union" &&
|
|
4840
|
+
lowerer.armTag(arr.type.unionId, UNDEFINED_T) >= 0 &&
|
|
4841
|
+
receiverIr.kind === "array") {
|
|
4842
|
+
const present = lowerer.stripUndefinedArm(arr.type);
|
|
4843
|
+
const helper = present.kind === "array"
|
|
4844
|
+
? lowerer.narrowedArmHelper(arr.type.unionId, present, locOf(target.expression))
|
|
4845
|
+
: null;
|
|
4846
|
+
if (helper) {
|
|
4847
|
+
arr = { kind: "call", callee: helper, args: [arr], type: present, loc: locOf(target.expression) };
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
if (arr.type.kind !== "array") {
|
|
4851
|
+
lowerer.unsupported("SC1090", target.expression, "assignment through a possibly missing nested array receiver");
|
|
4852
|
+
}
|
|
6407
4853
|
const index = lowerer.lowerExpr(target.argumentExpression);
|
|
6408
4854
|
if (index.type.kind !== "f64") {
|
|
6409
4855
|
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
|
|
6410
4856
|
}
|
|
6411
|
-
//
|
|
6412
|
-
//
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
}
|
|
6417
|
-
return { kind: "arraySet", arr, index, value, loc: locOf(expr) };
|
|
4857
|
+
// Optional array reads carry the source array's payload type plus an
|
|
4858
|
+
// undefined arm. arrayValueStore turns that arm into the runtime's
|
|
4859
|
+
// UNDEFINED state while keeping number[]/string[] payload storage scalar.
|
|
4860
|
+
const value = lowerer.lowerExpr(expr.right);
|
|
4861
|
+
return arrayValueStore(lowerer, arr, index, value, receiverIr.elem, locOf(expr));
|
|
6418
4862
|
}
|
|
6419
4863
|
export function ensureString(lowerer, e, node) {
|
|
6420
4864
|
if (e.type.kind === "string")
|
|
@@ -6679,6 +5123,12 @@ export function lowerAsExpression(lowerer, expr) {
|
|
|
6679
5123
|
if (target.kind === "bytes" && target.elem === "u8") {
|
|
6680
5124
|
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
|
|
6681
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
|
+
}
|
|
6682
5132
|
// ADAPTABLE function targets (`u as (x: number) => number` — the
|
|
6683
5133
|
// checked-dynamic function boundary): kind check, then exact unwrap
|
|
6684
5134
|
// or the per-target adapter shim. Non-adaptable signatures keep the
|
|
@@ -6717,10 +5167,11 @@ export function lowerPrefixUnary(lowerer, expr) {
|
|
|
6717
5167
|
const loc = locOf(expr);
|
|
6718
5168
|
switch (expr.operator) {
|
|
6719
5169
|
case ts.SyntaxKind.MinusToken: {
|
|
6720
|
-
const
|
|
6721
|
-
if (
|
|
6722
|
-
return { kind: "jsOp", op: "neg", args: [
|
|
5170
|
+
const raw = lowerer.lowerExpr(expr.operand);
|
|
5171
|
+
if (raw.type.kind === "jsval") {
|
|
5172
|
+
return { kind: "jsOp", op: "neg", args: [raw], type: JSVAL, loc };
|
|
6723
5173
|
}
|
|
5174
|
+
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
|
6724
5175
|
if (operand.type.kind !== "f64")
|
|
6725
5176
|
lowerer.unsupported("SC1043", expr);
|
|
6726
5177
|
if (operand.kind === "numLit")
|
|
@@ -6728,16 +5179,17 @@ export function lowerPrefixUnary(lowerer, expr) {
|
|
|
6728
5179
|
return { kind: "unary", op: "-", operand, type: F64, loc };
|
|
6729
5180
|
}
|
|
6730
5181
|
case ts.SyntaxKind.PlusToken: {
|
|
6731
|
-
const
|
|
5182
|
+
const raw = lowerer.lowerExpr(expr.operand);
|
|
6732
5183
|
// Unary + is ToNumber; on an already-number operand it's identity,
|
|
6733
5184
|
// and a STRING operand runs the runtime's ECMA-exact StringToNumber
|
|
6734
5185
|
// (num.fromString — Number(aString)'s lowering, scr_string.c).
|
|
6735
|
-
if (
|
|
6736
|
-
return { kind: "jsOp", op: "plus", args: [
|
|
5186
|
+
if (raw.type.kind === "jsval") {
|
|
5187
|
+
return { kind: "jsOp", op: "plus", args: [raw], type: JSVAL, loc };
|
|
6737
5188
|
}
|
|
6738
|
-
if (
|
|
6739
|
-
return { kind: "libCall", fn: "num.fromString", args: [
|
|
5189
|
+
if (raw.type.kind === "string") {
|
|
5190
|
+
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
|
|
6740
5191
|
}
|
|
5192
|
+
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
|
6741
5193
|
if (operand.type.kind !== "f64")
|
|
6742
5194
|
lowerer.unsupported("SC1043", expr);
|
|
6743
5195
|
return operand;
|
|
@@ -6749,7 +5201,7 @@ export function lowerPrefixUnary(lowerer, expr) {
|
|
|
6749
5201
|
}
|
|
6750
5202
|
case ts.SyntaxKind.TildeToken: {
|
|
6751
5203
|
// `~x`: ToInt32, complement, back to f64 (JS-exact, incl. NaN → -1).
|
|
6752
|
-
const operand = lowerer.lowerExpr(expr.operand);
|
|
5204
|
+
const operand = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.operand), loc, expr.operand);
|
|
6753
5205
|
if (operand.type.kind !== "f64")
|
|
6754
5206
|
lowerer.unsupported("SC1043", expr);
|
|
6755
5207
|
return { kind: "unary", op: "~", operand, type: F64, loc };
|
|
@@ -6815,9 +5267,9 @@ function lowerIncDec(lowerer, expr, prefix) {
|
|
|
6815
5267
|
lowerer.unsupported("SC1043", expr);
|
|
6816
5268
|
return { kind: "incDec", op, prefix, localId: target.id, type: F64, loc };
|
|
6817
5269
|
}
|
|
6818
|
-
/** Whether a lowered statement can live inside a seqExpr
|
|
6819
|
-
*
|
|
6820
|
-
*
|
|
5270
|
+
/** Whether a lowered statement can live inside a seqExpr. These statements
|
|
5271
|
+
* do not leave the expression through a jump; the native emitters can place
|
|
5272
|
+
* their writes and local state branches before evaluating the final result.
|
|
6821
5273
|
* Blocks check their bodies recursively. */
|
|
6822
5274
|
function seqExprSafeStmt(s) {
|
|
6823
5275
|
switch (s.kind) {
|
|
@@ -6828,18 +5280,59 @@ function seqExprSafeStmt(s) {
|
|
|
6828
5280
|
case "recordSet":
|
|
6829
5281
|
case "recordKeySet":
|
|
6830
5282
|
case "arraySet":
|
|
5283
|
+
case "arraySetLength":
|
|
6831
5284
|
case "bytesSet":
|
|
5285
|
+
case "arraySetUndefined":
|
|
5286
|
+
case "arrayDelete":
|
|
6832
5287
|
return true;
|
|
5288
|
+
case "if":
|
|
5289
|
+
return s.then.every(seqExprSafeStmt) && (s.else_?.every(seqExprSafeStmt) ?? true);
|
|
6833
5290
|
case "block":
|
|
6834
5291
|
return s.body.every(seqExprSafeStmt);
|
|
6835
5292
|
default:
|
|
6836
5293
|
return false;
|
|
6837
5294
|
}
|
|
6838
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
|
+
}
|
|
6839
5326
|
export function lowerBinary(lowerer, expr) {
|
|
6840
5327
|
const loc = locOf(expr);
|
|
6841
5328
|
const op = expr.operatorToken.kind;
|
|
5329
|
+
const cacheHas = lowerRequireCacheHas(lowerer, expr);
|
|
5330
|
+
if (cacheHas)
|
|
5331
|
+
return cacheHas;
|
|
6842
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
|
+
}
|
|
6843
5336
|
// `x = e` in EXPRESSION position (`while ((idx = s.indexOf("\n")) !== -1)`,
|
|
6844
5337
|
// `f(x = v)`): evaluate e once, write the binding, yield the assigned
|
|
6845
5338
|
// value — JS evaluation order. Variable targets only (locals and module
|
|
@@ -6950,7 +5443,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
6950
5443
|
// RHS value is the expression's value — its OWN type, so the
|
|
6951
5444
|
// consumer sees exactly what the checker typed.
|
|
6952
5445
|
if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) {
|
|
6953
|
-
const recv =
|
|
5446
|
+
const recv = tryLowerExpression(lowerer, expr.left.expression);
|
|
6954
5447
|
if (recv && recv.type.kind === "dyn") {
|
|
6955
5448
|
const recvTmp = lowerer.declareHiddenLocal("%setRecv", DYN);
|
|
6956
5449
|
const rhsVal = lowerer.lowerExpr(expr.right);
|
|
@@ -7075,8 +5568,74 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7075
5568
|
// Everything else (`u && flag`, whose value would need a wider
|
|
7076
5569
|
// re-tagged union) stays fenced — in CONDITION position those
|
|
7077
5570
|
// shapes lower through lowerCondition's bool descent instead.
|
|
7078
|
-
|
|
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
|
+
}
|
|
7079
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
|
+
}
|
|
7080
5639
|
// `||` whose left does NOT fit the result union: the checker built
|
|
7081
5640
|
// that result by DROPPING the left's falsy arms (`process.env.X ||
|
|
7082
5641
|
// null` is `string | null`, `|| 3000` is `string | number` — the
|
|
@@ -7084,9 +5643,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7084
5643
|
// the shared shape below does, retags an arm the test is about to
|
|
7085
5644
|
// rule out and throws where Node yields the default. Single-eval
|
|
7086
5645
|
// instead: test the left in its OWN union and retag only on the
|
|
7087
|
-
// truthy side, where the dropped arms are unreachable.
|
|
7088
|
-
// the eager shape — there the falsy left IS the result, so the
|
|
7089
|
-
// checker keeps those arms in the target and they must coerce.
|
|
5646
|
+
// truthy side, where the dropped arms are unreachable.
|
|
7090
5647
|
if (op === ts.SyntaxKind.BarBarToken && left.type.kind === "union" && !typeEquals(left.type, target)) {
|
|
7091
5648
|
const retag = lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc);
|
|
7092
5649
|
if (retag) {
|
|
@@ -7299,6 +5856,14 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7299
5856
|
// retry lifts the site. Genuine unknown keeps the SC1100 story.
|
|
7300
5857
|
if ((left.type.kind === "dyn" && lowerer.anyOrigin(expr.left)) ||
|
|
7301
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);
|
|
7302
5867
|
lowerer.anyOpFence(`the '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' operator`, expr);
|
|
7303
5868
|
}
|
|
7304
5869
|
// tsc allows ===/!== on unknown (arithmetic/comparisons it rejects
|
|
@@ -7310,37 +5875,120 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7310
5875
|
// computes. Comparisons come back as static bools; arithmetic stays
|
|
7311
5876
|
// an island value ('1 as any + "x"' is a string over there).
|
|
7312
5877
|
if (left.type.kind === "jsval" || right.type.kind === "jsval") {
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
5878
|
+
return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
|
|
5879
|
+
}
|
|
5880
|
+
// An unchecked array read keeps its undefined arm in the IR even when
|
|
5881
|
+
// the checker typed the binding as `string`. An optional string is not
|
|
5882
|
+
// itself a string operand for `+`: `undefined + 1` and
|
|
5883
|
+
// `undefined + undefined` are NaN, while a present string concatenates.
|
|
5884
|
+
// Keep this predicate exact so a nullable string does not silently take
|
|
5885
|
+
// the string path either.
|
|
5886
|
+
const optionalStringType = (type) => {
|
|
5887
|
+
if (type.kind !== "union")
|
|
5888
|
+
return false;
|
|
5889
|
+
const arms = lowerer.unions.get(type.unionId)?.arms;
|
|
5890
|
+
return !!arms && arms.length === 2 &&
|
|
5891
|
+
arms.some((arm) => arm.kind === "string") &&
|
|
5892
|
+
arms.some((arm) => arm.kind === "undefinedT");
|
|
5893
|
+
};
|
|
5894
|
+
const arithmeticNumber = (operand, node) => lowerOptionalNumber(lowerer, operand, loc, node);
|
|
5895
|
+
const arithmeticLeft = strictEquality ? left : arithmeticNumber(left, expr.left);
|
|
5896
|
+
const arithmeticRight = strictEquality ? right : arithmeticNumber(right, expr.right);
|
|
5897
|
+
const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
|
|
5898
|
+
const bothNum = arithmeticLeft.type.kind === "f64" && arithmeticRight.type.kind === "f64";
|
|
5899
|
+
const bothStr = left.type.kind === "string" && right.type.kind === "string";
|
|
5900
|
+
const lowerOptionalStringPlus = () => {
|
|
5901
|
+
const leftOptional = optionalStringType(left.type);
|
|
5902
|
+
const rightOptional = optionalStringType(right.type);
|
|
5903
|
+
if (!leftOptional && !rightOptional)
|
|
5904
|
+
return null;
|
|
5905
|
+
const allowed = (type) => type.kind === "f64" || type.kind === "string" || optionalStringType(type);
|
|
5906
|
+
if (!allowed(left.type) || !allowed(right.type))
|
|
5907
|
+
return null;
|
|
5908
|
+
// The tag tests and the chosen concatenation branch must see the same
|
|
5909
|
+
// operand values. This matters for `take()[0] + 1` and for two reads
|
|
5910
|
+
// with observable getters/calls.
|
|
5911
|
+
const prefix = [];
|
|
5912
|
+
const stable = (operand) => {
|
|
5913
|
+
if (isSafeToRepeat(operand) || isSafeToDiscard(operand))
|
|
5914
|
+
return operand;
|
|
5915
|
+
const tmp = lowerer.declareHiddenLocal("%plus", operand.type);
|
|
5916
|
+
prefix.push({ kind: "varDecl", localId: tmp.id, init: operand, loc });
|
|
5917
|
+
return varRef(tmp.id, operand.type, loc);
|
|
7326
5918
|
};
|
|
7327
|
-
const
|
|
7328
|
-
|
|
7329
|
-
|
|
5919
|
+
const stableLeft = stable(left);
|
|
5920
|
+
const stableRight = stable(right);
|
|
5921
|
+
const leftString = lowerer.ensureString(stableLeft, expr.left);
|
|
5922
|
+
const rightString = lowerer.ensureString(stableRight, expr.right);
|
|
5923
|
+
const concat = { kind: "strConcat", left: leftString, right: rightString, type: STRING, loc };
|
|
5924
|
+
const nan = { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc };
|
|
5925
|
+
const resultT = { kind: "union", unionId: lowerer.unions.intern([F64, STRING]) };
|
|
5926
|
+
lowerer.runtimeOptionalArithmeticTypes.set(expr, resultT);
|
|
5927
|
+
const numberTag = lowerer.armTag(resultT.unionId, F64);
|
|
5928
|
+
const stringTag = lowerer.armTag(resultT.unionId, STRING);
|
|
5929
|
+
if (numberTag < 0 || stringTag < 0)
|
|
5930
|
+
throw new InternalCompilerError("lowerer bug: optional string plus result union");
|
|
5931
|
+
const number = () => ({
|
|
5932
|
+
kind: "unionWrap", unionId: resultT.unionId, tag: numberTag, value: nan, type: resultT, loc,
|
|
5933
|
+
});
|
|
5934
|
+
const string = () => ({
|
|
5935
|
+
kind: "unionWrap", unionId: resultT.unionId, tag: stringTag, value: concat, type: resultT, loc,
|
|
5936
|
+
});
|
|
5937
|
+
const undefinedTest = (operand) => ({
|
|
5938
|
+
kind: "unionIsTag",
|
|
5939
|
+
unionId: operand.type.kind === "union" ? operand.type.unionId : "",
|
|
5940
|
+
tag: operand.type.kind === "union" ? lowerer.armTag(operand.type.unionId, UNDEFINED_T) : -1,
|
|
5941
|
+
negated: false,
|
|
5942
|
+
value: operand,
|
|
5943
|
+
type: BOOL,
|
|
5944
|
+
loc,
|
|
5945
|
+
});
|
|
5946
|
+
let result;
|
|
5947
|
+
if (leftOptional && rightOptional) {
|
|
5948
|
+
const rightMissing = undefinedTest(stableRight);
|
|
5949
|
+
const bothMissing = {
|
|
5950
|
+
kind: "ternary",
|
|
5951
|
+
cond: rightMissing,
|
|
5952
|
+
then: number(),
|
|
5953
|
+
else_: string(),
|
|
5954
|
+
type: resultT,
|
|
5955
|
+
loc,
|
|
5956
|
+
};
|
|
5957
|
+
result = {
|
|
5958
|
+
kind: "ternary",
|
|
5959
|
+
cond: undefinedTest(stableLeft),
|
|
5960
|
+
then: bothMissing,
|
|
5961
|
+
else_: string(),
|
|
5962
|
+
type: resultT,
|
|
5963
|
+
loc,
|
|
5964
|
+
};
|
|
7330
5965
|
}
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
5966
|
+
else if (leftOptional) {
|
|
5967
|
+
result = {
|
|
5968
|
+
kind: "ternary",
|
|
5969
|
+
cond: undefinedTest(stableLeft),
|
|
5970
|
+
then: number(),
|
|
5971
|
+
else_: string(),
|
|
5972
|
+
type: resultT,
|
|
5973
|
+
loc,
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
else {
|
|
5977
|
+
result = {
|
|
5978
|
+
kind: "ternary",
|
|
5979
|
+
cond: undefinedTest(stableRight),
|
|
5980
|
+
then: number(),
|
|
5981
|
+
else_: string(),
|
|
5982
|
+
type: resultT,
|
|
5983
|
+
loc,
|
|
5984
|
+
};
|
|
5985
|
+
}
|
|
5986
|
+
return prefix.length === 0 ? result : { kind: "seqExpr", stmts: prefix, result, type: resultT, loc };
|
|
5987
|
+
};
|
|
7340
5988
|
switch (op) {
|
|
7341
5989
|
case ts.SyntaxKind.PlusToken:
|
|
7342
5990
|
if (bothNum)
|
|
7343
|
-
return { kind: "bin", op: "+", left, right, type: F64, loc };
|
|
5991
|
+
return { kind: "bin", op: "+", left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
|
|
7344
5992
|
if (left.type.kind === "string" || right.type.kind === "string") {
|
|
7345
5993
|
return {
|
|
7346
5994
|
kind: "strConcat",
|
|
@@ -7349,6 +5997,9 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7349
5997
|
type: STRING, loc,
|
|
7350
5998
|
};
|
|
7351
5999
|
}
|
|
6000
|
+
const optionalStringPlus = lowerOptionalStringPlus();
|
|
6001
|
+
if (optionalStringPlus)
|
|
6002
|
+
return optionalStringPlus;
|
|
7352
6003
|
lowerer.unsupported("SC1043", expr);
|
|
7353
6004
|
break;
|
|
7354
6005
|
case ts.SyntaxKind.MinusToken:
|
|
@@ -7362,7 +6013,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7362
6013
|
: op === ts.SyntaxKind.AsteriskToken ? "*"
|
|
7363
6014
|
: op === ts.SyntaxKind.SlashToken ? "/"
|
|
7364
6015
|
: op === ts.SyntaxKind.PercentToken ? "%" : "**";
|
|
7365
|
-
return { kind: "bin", op: binOp, left, right, type: F64, loc };
|
|
6016
|
+
return { kind: "bin", op: binOp, left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
|
|
7366
6017
|
}
|
|
7367
6018
|
// The bitwise six, JS-exact: operands through ToInt32/ToUint32
|
|
7368
6019
|
// (NaN/±Infinity → 0, truncate, wrap mod 2^32), the operation in
|
|
@@ -7381,12 +6032,12 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7381
6032
|
: op === ts.SyntaxKind.CaretToken ? "^"
|
|
7382
6033
|
: op === ts.SyntaxKind.LessThanLessThanToken ? "<<"
|
|
7383
6034
|
: op === ts.SyntaxKind.GreaterThanGreaterThanToken ? ">>" : ">>>";
|
|
7384
|
-
return { kind: "bin", op: bitOp, left, right, type: F64, loc };
|
|
6035
|
+
return { kind: "bin", op: bitOp, left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
|
|
7385
6036
|
}
|
|
7386
6037
|
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
|
7387
6038
|
case ts.SyntaxKind.ExclamationEqualsEqualsToken: {
|
|
7388
6039
|
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
|
7389
|
-
if (
|
|
6040
|
+
if (plainBothNum)
|
|
7390
6041
|
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
|
|
7391
6042
|
if (bothStr)
|
|
7392
6043
|
return { kind: "strEq", negated, left, right, type: BOOL, loc };
|
|
@@ -7445,6 +6096,38 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7445
6096
|
loc,
|
|
7446
6097
|
};
|
|
7447
6098
|
}
|
|
6099
|
+
if (left.type.kind === "union" && right.type.kind === "union") {
|
|
6100
|
+
const leftDef = lowerer.unions.get(left.type.unionId);
|
|
6101
|
+
const rightDef = lowerer.unions.get(right.type.unionId);
|
|
6102
|
+
if (leftDef && rightDef) {
|
|
6103
|
+
const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
|
|
6104
|
+
const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
|
|
6105
|
+
const sameValueArms = leftValues.length === rightValues.length &&
|
|
6106
|
+
leftValues.every((arm) => rightValues.some((candidate) => typeEquals(candidate, arm)));
|
|
6107
|
+
if (sameValueArms) {
|
|
6108
|
+
const arms = [...leftDef.arms];
|
|
6109
|
+
for (const arm of rightDef.arms) {
|
|
6110
|
+
if (!arms.some((candidate) => typeEquals(candidate, arm)))
|
|
6111
|
+
arms.push(arm);
|
|
6112
|
+
}
|
|
6113
|
+
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
|
6114
|
+
const common = {
|
|
6115
|
+
kind: "union",
|
|
6116
|
+
unionId: lowerer.unions.intern(arms),
|
|
6117
|
+
};
|
|
6118
|
+
return {
|
|
6119
|
+
kind: "unionEq",
|
|
6120
|
+
unionId: common.unionId,
|
|
6121
|
+
negated,
|
|
6122
|
+
sameValue: false,
|
|
6123
|
+
left: lowerer.coerceInto(expr.left, left, common),
|
|
6124
|
+
right: lowerer.coerceInto(expr.right, right, common),
|
|
6125
|
+
type: BOOL,
|
|
6126
|
+
loc,
|
|
6127
|
+
};
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
}
|
|
7448
6131
|
lowerer.unsupported("SC1090", expr, `comparisons of union-typed values (${NARROW_FIRST})`);
|
|
7449
6132
|
}
|
|
7450
6133
|
// Arrays, maps, functions, class instances and records compare by
|
|
@@ -7509,7 +6192,7 @@ export function lowerBinary(lowerer, expr) {
|
|
|
7509
6192
|
: op === ts.SyntaxKind.LessThanEqualsToken ? "<="
|
|
7510
6193
|
: op === ts.SyntaxKind.GreaterThanToken ? ">" : ">=";
|
|
7511
6194
|
if (bothNum)
|
|
7512
|
-
return { kind: "bin", op: cmpOp, left, right, type: BOOL, loc };
|
|
6195
|
+
return { kind: "bin", op: cmpOp, left: arithmeticLeft, right: arithmeticRight, type: BOOL, loc };
|
|
7513
6196
|
if (bothStr)
|
|
7514
6197
|
return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
|
|
7515
6198
|
lowerer.unsupported("SC1043", expr);
|
|
@@ -7885,7 +6568,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
|
|
|
7885
6568
|
continue;
|
|
7886
6569
|
const tags = answers.flatMap((s, i) => (s === b.text ? [i] : []));
|
|
7887
6570
|
if (tags.length === 0 || tags.length === def.arms.length) {
|
|
7888
|
-
if (!
|
|
6571
|
+
if (!isSafeToRepeat(value)) {
|
|
7889
6572
|
lowerer.unsupported("SC1090", expr, "statically-decided 'typeof' tests over effectful operands (bind the value to a const first)");
|
|
7890
6573
|
}
|
|
7891
6574
|
return { kind: "boolLit", value: (tags.length !== 0) !== negated, type: BOOL, loc };
|
|
@@ -7895,7 +6578,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
|
|
|
7895
6578
|
});
|
|
7896
6579
|
if (tags.length === 1)
|
|
7897
6580
|
return isTag(tags[0]);
|
|
7898
|
-
if (!
|
|
6581
|
+
if (!isSafeToRepeat(value)) {
|
|
7899
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)");
|
|
7900
6583
|
}
|
|
7901
6584
|
// Tag-in-set as a short-circuit chain (De Morgan for `!==`).
|
|
@@ -7950,7 +6633,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
|
|
|
7950
6633
|
const recv = unparen(pn.expression);
|
|
7951
6634
|
if (ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) {
|
|
7952
6635
|
const inner = unparen(recv.expression);
|
|
7953
|
-
const obj =
|
|
6636
|
+
const obj = tryLowerExpression(lowerer, inner);
|
|
7954
6637
|
if (obj?.type.kind === "dyn") {
|
|
7955
6638
|
keyed = {
|
|
7956
6639
|
kind: "dynKeyGet",
|
|
@@ -7973,7 +6656,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
|
|
|
7973
6656
|
// "string") ... }`, the validateConfig idiom). Probe the lowering
|
|
7974
6657
|
// and claim exactly the dyn results; anything else keeps its
|
|
7975
6658
|
// sibling lowerings and fences untouched.
|
|
7976
|
-
const probed =
|
|
6659
|
+
const probed = tryLowerExpression(lowerer, a.expression);
|
|
7977
6660
|
if (probed?.type.kind !== "dyn")
|
|
7978
6661
|
continue;
|
|
7979
6662
|
value = probed;
|
|
@@ -8137,33 +6820,6 @@ function lowerCjsExportTableThisMember(lowerer, expr) {
|
|
|
8137
6820
|
return null;
|
|
8138
6821
|
return cjsAccessorCall(lowerer, sibling, locOf(expr));
|
|
8139
6822
|
}
|
|
8140
|
-
/** Lowers an expression, converting a fence (PoisonError) into a null
|
|
8141
|
-
* DECLINE — the probing dispatch for shapes where only the lowered
|
|
8142
|
-
* value's type can say whether a lowering applies. unsupported() pushes
|
|
8143
|
-
* its diagnostic BEFORE throwing, so the probe runs behind a diagnostic
|
|
8144
|
-
* sink: a declined probe's diagnostics vanish (the sibling lowerings
|
|
8145
|
-
* own the site), a successful probe's flush through afterwards. */
|
|
8146
|
-
export function probeLower(lowerer, node) {
|
|
8147
|
-
const saved = lowerer.diagSink;
|
|
8148
|
-
const captured = [];
|
|
8149
|
-
lowerer.diagSink = captured;
|
|
8150
|
-
let result;
|
|
8151
|
-
try {
|
|
8152
|
-
result = lowerer.lowerExpr(node);
|
|
8153
|
-
}
|
|
8154
|
-
catch (e) {
|
|
8155
|
-
if (e instanceof PoisonError) {
|
|
8156
|
-
lowerer.diagSink = saved;
|
|
8157
|
-
return null;
|
|
8158
|
-
}
|
|
8159
|
-
lowerer.diagSink = saved;
|
|
8160
|
-
throw e;
|
|
8161
|
-
}
|
|
8162
|
-
lowerer.diagSink = saved;
|
|
8163
|
-
for (const d of captured)
|
|
8164
|
-
lowerer.pushDiag(d);
|
|
8165
|
-
return result;
|
|
8166
|
-
}
|
|
8167
6823
|
/** A read of a catch binding — the caught analog of maybeNarrow. Where
|
|
8168
6824
|
* tsc's control-flow narrowing has proven a supported test (`instanceof`
|
|
8169
6825
|
* over a hierarchy class, `typeof` over a primitive), the read bridges
|
|
@@ -8643,7 +7299,7 @@ function lowerInExpression(lowerer, expr, loc) {
|
|
|
8643
7299
|
// stringified like every property key (o[k] is o[String(k)] in JS;
|
|
8644
7300
|
// `in` shares the coercion).
|
|
8645
7301
|
{
|
|
8646
|
-
const probed =
|
|
7302
|
+
const probed = tryLowerExpression(lowerer, expr.right);
|
|
8647
7303
|
if (probed?.type.kind === "dyn") {
|
|
8648
7304
|
let k = lowerer.lowerExpr(expr.left); // JS order: the key evaluates first
|
|
8649
7305
|
if (k.type.kind === "f64" || k.type.kind === "string" || k.type.kind === "dyn") {
|
|
@@ -9473,6 +8129,34 @@ export function lowerUnionProperty(lowerer, expr) {
|
|
|
9473
8129
|
if (!def)
|
|
9474
8130
|
throw new InternalCompilerError(`lowerer bug: unknown union ${value.type.unionId}`);
|
|
9475
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
|
+
}
|
|
9476
8160
|
let common = null;
|
|
9477
8161
|
for (const arm of def.arms) {
|
|
9478
8162
|
let ft;
|
|
@@ -9638,8 +8322,21 @@ export function fieldTarget(lowerer, access) {
|
|
|
9638
8322
|
return null;
|
|
9639
8323
|
}
|
|
9640
8324
|
const fieldType = info.fields.get(access.name.text);
|
|
8325
|
+
const lowerObjectReceiver = () => {
|
|
8326
|
+
let obj = lowerer.lowerExpr(access.expression);
|
|
8327
|
+
if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
|
|
8328
|
+
const present = lowerer.stripUndefinedArm(obj.type);
|
|
8329
|
+
const helper = present.kind === "object"
|
|
8330
|
+
? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
|
|
8331
|
+
: null;
|
|
8332
|
+
obj = helper
|
|
8333
|
+
? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
|
|
8334
|
+
: lowerer.maybeNarrow(obj, access.expression);
|
|
8335
|
+
}
|
|
8336
|
+
return obj;
|
|
8337
|
+
};
|
|
9641
8338
|
if (fieldType) {
|
|
9642
|
-
const obj =
|
|
8339
|
+
const obj = lowerObjectReceiver();
|
|
9643
8340
|
return { container: "class", obj, className: receiverIr.className, field: access.name.text, fieldType };
|
|
9644
8341
|
}
|
|
9645
8342
|
// Accessor property: either half declared anywhere on the chain
|
|
@@ -9648,7 +8345,7 @@ export function fieldTarget(lowerer, access) {
|
|
|
9648
8345
|
const getF = lowerer.findMethodOn(info, `get:${access.name.text}`);
|
|
9649
8346
|
const setF = lowerer.findMethodOn(info, `set:${access.name.text}`);
|
|
9650
8347
|
if (getF || setF) {
|
|
9651
|
-
const obj =
|
|
8348
|
+
const obj = lowerObjectReceiver();
|
|
9652
8349
|
return {
|
|
9653
8350
|
container: "accessor",
|
|
9654
8351
|
obj,
|
|
@@ -9663,13 +8360,35 @@ export function fieldTarget(lowerer, access) {
|
|
|
9663
8360
|
const shape = lowerer.shapes.get(receiverIr.shapeId);
|
|
9664
8361
|
const fieldType = shape?.fields.find((f) => f.name === access.name.text)?.type;
|
|
9665
8362
|
if (fieldType) {
|
|
9666
|
-
|
|
8363
|
+
let obj = lowerer.lowerExpr(access.expression);
|
|
9667
8364
|
// A checker-record receiver whose VALUE stayed dyn (the erased
|
|
9668
8365
|
// all-unknown-fields cast — `(err as { code?: unknown }).code`):
|
|
9669
8366
|
// decline, and the dyn keyed-read fallback answers.
|
|
8367
|
+
if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
|
|
8368
|
+
// A nested property read such as `rows[i].value` has the same
|
|
8369
|
+
// unchecked outer array-read issue as tuple indexing. Validate
|
|
8370
|
+
// the record arm before fieldGet/recordGet so a missing outer
|
|
8371
|
+
// value raises a catchable TypeError rather than reaching the
|
|
8372
|
+
// backend with a union receiver.
|
|
8373
|
+
const present = lowerer.stripUndefinedArm(obj.type);
|
|
8374
|
+
const helper = present.kind === "record"
|
|
8375
|
+
? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
|
|
8376
|
+
: null;
|
|
8377
|
+
obj = helper
|
|
8378
|
+
? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
|
|
8379
|
+
: lowerer.maybeNarrow(obj, access.expression);
|
|
8380
|
+
}
|
|
9670
8381
|
if (obj.type.kind !== "record")
|
|
9671
8382
|
return null;
|
|
9672
|
-
|
|
8383
|
+
const actualShape = lowerer.shapes.get(obj.type.shapeId);
|
|
8384
|
+
const actualField = actualShape?.fields.find((f) => f.name === access.name.text);
|
|
8385
|
+
return {
|
|
8386
|
+
container: "record",
|
|
8387
|
+
obj,
|
|
8388
|
+
shapeId: obj.type.shapeId,
|
|
8389
|
+
field: access.name.text,
|
|
8390
|
+
fieldType: actualField?.type ?? fieldType,
|
|
8391
|
+
};
|
|
9673
8392
|
}
|
|
9674
8393
|
// A RECORD accessor property: either slot present makes the name an
|
|
9675
8394
|
// accessor target (get/set share the property namespace — tsc
|
|
@@ -10102,7 +8821,7 @@ export function lowerFieldCompound(lowerer, access, op, rhsNode, loc) {
|
|
|
10102
8821
|
// receiver is an identifier (checked above), so evaluating it for
|
|
10103
8822
|
// read and write matches JS's once-evaluation observably.
|
|
10104
8823
|
if (ts.isPropertyAccessExpression(access)) {
|
|
10105
|
-
const probed =
|
|
8824
|
+
const probed = tryLowerExpression(lowerer, access.expression);
|
|
10106
8825
|
if (probed?.type.kind === "dyn") {
|
|
10107
8826
|
const key = { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access.name) };
|
|
10108
8827
|
const read = { kind: "dynKeyGet", key, value: probed, type: DYN, loc };
|