mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
co-authored by
Claude Opus 5.5
parent
d968975f34
commit
7bb72e1e17
@@ -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 ||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6476-in-literal-key-index-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6477-fresh-array-spread-insertion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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"]);
|
||||
Reference in New Issue
Block a user