Compile literal-key 'in' over index-signature records, fresh array spread insertions, and any-annotated dictionaries

- '"k" in record' with a literal key naming no declared member of an
  index-signature record (a match's named groups) takes the keyed
  presence probe a runtime key takes.
- Spreading a fresh array literal (directly, through a conditional, or
  through a const used only there) into push/unshift or an insertion
  converts its elements into the destination's element layout; nothing
  else can reach them, so the copy is unobservable.
- A const annotated with a type the checker reads as 'any' and
  initialized by an empty object literal holds the checked-dynamic
  object, so keyed writes fill it (the empty struct could hold nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 03:09:53 -07:00
co-authored by Claude Opus 5.5
parent d968975f34
commit 7bb72e1e17
11 changed files with 194 additions and 0 deletions
@@ -0,0 +1,65 @@
/* Array spreads whose elements nothing else can reach. */
import * as ts from "../../ts7/adapter.js";
import type { IrExpr, IrType, SrcLoc } from "../../../ir/ir.js";
import type { Lowerer } from "../lowerer.js";
/** Whether the elements a spread inserts are reachable only through it: an
* array literal (`...(cond ? [{ ... }] : [])`), or a const initialized by
* one that this spread is the only reference to. Converting such elements
* into the destination's element layout copies them, which nothing can
* observe. */
export function freshSpreadSource(lowerer: Lowerer, node: ts.Expression): boolean {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (ts.isArrayLiteralExpression(e))
return e.elements.nodes.every(
(x) => !ts.isSpreadElement(x) && (ts.isObjectLiteralExpression(x) || !isReference(x)),
);
if (ts.isConditionalExpression(e))
return freshSpreadSource(lowerer, e.whenTrue) && freshSpreadSource(lowerer, e.whenFalse);
if (!ts.isIdentifier(e)) return false;
const symbol = lowerer.resolveValueSymbol(e);
const decl = symbol?.valueDeclaration;
if (!symbol || !decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return false;
const list = decl.parent;
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return false;
if (!freshSpreadSource(lowerer, decl.initializer)) return false;
// The binding's only use is this spread.
let scope: ts.Node = list;
while (scope.parent && !ts.isBlock(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
let uses = 0;
const visit = (n: ts.Node): void => {
if (uses > 1) return;
if (ts.isIdentifier(n) && n !== decl.name && n.text === e.text) {
if (lowerer.resolveValueSymbol(n) === symbol) uses++;
}
ts.forEachChild(n, visit);
};
visit(scope);
return uses === 1;
}
function isReference(e: ts.Expression): boolean {
return !(
ts.isStringLiteral(e) ||
ts.isNumericLiteral(e) ||
e.kind === ts.SyntaxKind.TrueKeyword ||
e.kind === ts.SyntaxKind.FalseKeyword ||
e.kind === ts.SyntaxKind.NullKeyword
);
}
/** A fresh array spread converted into the destination's array layout by
* the structural width lift, or null when the layouts do not lift. */
export function liftedFreshSpread(
lowerer: Lowerer,
node: ts.Expression,
source: IrExpr,
arrType: IrType,
loc: SrcLoc,
): IrExpr | null {
if (source.type.kind !== "array" || arrType.kind !== "array") return null;
if (!freshSpreadSource(lowerer, node)) return null;
const lift = lowerer.widthLiftPlan(source.type, arrType);
return lift ? lowerer.applyWidthLift(lift, source, arrType, loc) : null;
}
@@ -1,5 +1,6 @@
import { generatorDrain } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { InternalCompilerError } from "../../../errors.js";
import {
dynUndefinedExpr,
@@ -141,6 +142,16 @@ export function lowerArraySpreadItems(
if (source.type.kind === "record" && lowerer.shapes.get(source.type.shapeId)?.tuple) {
source = lowerer.widthCoerce(source, arrType) ?? source;
}
if (
!typeEquals(source.type, arrType) &&
!(
source.type.kind === "array" &&
identityPreservingWidening(lowerer, source.type.elem, elem)
)
) {
// Fresh elements (an array literal) convert into the element layout.
source = liftedFreshSpread(lowerer, node.expression, source, arrType, loc) ?? source;
}
if (!typeEquals(source.type, arrType)) {
if (
source.type.kind !== "array" ||
@@ -1,3 +1,4 @@
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { lowerArrayIteratorValue } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { arrayConcatHelper } from "./array-concat.js";
@@ -793,6 +794,10 @@ export function lowerArrayMethodCall(
loc,
};
}
if (!typeEquals(src.type, receiverIr)) {
// Fresh elements (an array literal) convert into the element layout.
src = liftedFreshSpread(lowerer, spreadArg.expression, src, receiverIr, loc) ?? src;
}
if (!typeEquals(src.type, receiverIr)) {
lowerer.unsupported(
"SC1090",
@@ -1251,6 +1251,24 @@ export function lowerInExpression(
if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
refuseExecExceptionCode(lowerer, expr);
}
// A literal key that names no declared member of an index-signature
// record (`"id" in match.groups`): the same keyed presence probe a
// runtime key takes — declared fields by name, then the overflow keys.
{
const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.right));
const shape = recvT?.kind === "record" ? lowerer.shapes.get(recvT.shapeId) : undefined;
if (
shape?.indexValue &&
!shape.tuple &&
!OBJECT_PROTOTYPE_KEYS.has(key) &&
!shape.fields.some(
(f) => f.name === key || f.name === `%get:${key}` || f.name === `%set:${key}`,
)
) {
const probed = lowerRuntimeKeyIn(lowerer, expr, loc);
if (probed) return probed;
}
}
let recv = lowerer.lowerExpr(expr.right);
const classInfo =
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
@@ -5331,7 +5331,29 @@ export function lowerVarDecl(
// `let` may be reassigned a different shape later; genuine `any`
// only — every other unmappable keeps its own diagnostic.
const checkerAny = (lowerer.typeOf(decl.name).flags & ts.TypeFlags.Any) !== 0;
// An annotation the checker could not resolve (a package type alias that
// reads as `any`) over an empty object literal: the binding is a
// dictionary the code fills by key (`files[name] = entry`), which the
// empty struct the literal would adopt cannot hold. It holds the
// checked-dynamic object instead.
let emptyAnyDictionary = false;
if (checkerAny && decl.type && !isLet && init.type.kind === "record") {
let lit: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(lit)) lit = lit.expression;
const shape = lowerer.shapes.get(init.type.shapeId);
if (
ts.isObjectLiteralExpression(lit) &&
lit.properties.nodes.length === 0 &&
shape &&
shape.fields.length === 0 &&
!shape.indexValue
) {
type = DYN;
emptyAnyDictionary = true;
}
}
if (
!emptyAnyDictionary &&
(type === null || (type.kind === "dyn" && checkerAny)) &&
!isLet &&
(checkerAny ||
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6476-in-literal-key-index-records.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6477-fresh-array-spread-insertion.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6478-any-annotated-dictionary.ts"
],
"diags": []
}
@@ -0,0 +1,18 @@
// `"k" in record` with a literal key over an index-signature record (a
// match's named groups, a dictionary) answers the record's own keys.
function source(header: string): string | undefined {
const result = /^x-meta-source: (?<id>dpl_.*)/m.exec(header);
if (result?.groups && "id" in result.groups) {
return result.groups.id;
}
return undefined;
}
console.log(source("x-meta-source: dpl_123\nother: 1"), source("nothing"));
const both = /(?<key>\w+)=(?<value>\w*)/.exec("mode=fast");
if (both?.groups) {
console.log("key" in both.groups, "value" in both.groups, "other" in both.groups);
}
const dict: Record<string, number> = { a: 1 };
dict["late"] = 2;
delete dict["a"];
console.log("a" in dict, "late" in dict, "missing" in dict, "toString" in dict);
@@ -0,0 +1,13 @@
// Spreading a fresh array literal whose elements are typed by their own
// literal (an omitted member reads `undefined`) into an array of the
// declared element type converts the elements; nothing else can reach
// them.
type Result = { message: string; file: { path: string } | undefined; noisy: boolean };
const results: Result[] = [{ message: "first", file: { path: "/a" }, noisy: false }];
const spam = results.length > 0 ? [{ message: "spam", file: undefined, noisy: true }] : [];
const more: Result[] = [{ message: "m", file: undefined, noisy: false }];
results.unshift(...spam, ...more);
const tail = results.length > 5 ? [] : [{ message: "tail", file: undefined, noisy: false }];
results.push(...tail);
console.log(results.map((r) => `${r.message}:${r.file?.path ?? "-"}:${r.noisy}`).join(" "));
console.log(JSON.stringify(results[1]));
@@ -0,0 +1,24 @@
// A binding annotated with a type the checker reads as `any`, initialized
// with an empty object literal, is a dictionary the code fills by key.
class Ref {
digest: string;
constructor(digest: string) {
this.digest = digest;
}
}
class Blob {
data: string;
constructor(data: string) {
this.data = data;
}
}
function collect(entries: { name: string; sha?: string }[]): unknown {
const files: any = {};
for (const entry of entries) {
files[entry.name] = entry.sha ? new Ref(`sha:${entry.sha}`) : new Blob("");
}
files["count"] = entries.length;
return files;
}
const out = collect([{ name: "a.txt", sha: "abc" }, { name: "dir" }]) as Record<string, unknown>;
console.log(Object.keys(out), out["a.txt"] instanceof Ref, out["dir"] instanceof Blob, out["count"]);