mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Compile literal-key 'in' over index-signature records, fresh array spread insertions, and any-annotated dictionaries (#1514)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/* 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 an unexported const
|
||||
* initialized by one that this spread is the only reference to, and that
|
||||
* the spread reads at most once per evaluation of the initializer. 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;
|
||||
// Other modules can import an exported binding and reach its elements.
|
||||
const statement = list.parent;
|
||||
if (
|
||||
ts.isVariableStatement(statement) &&
|
||||
statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
||||
)
|
||||
return false;
|
||||
if (!freshSpreadSource(lowerer, decl.initializer)) return false;
|
||||
// The binding's only use is this spread; an export specifier naming it
|
||||
// (`export { name }`) is another.
|
||||
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.isExportSpecifier(n) && (n.propertyName ?? n.name).text === e.text) {
|
||||
uses = 2;
|
||||
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 && runsOnceInScope(node, scope);
|
||||
}
|
||||
|
||||
/** Whether `node` runs at most once each time `scope` runs: no loop body,
|
||||
* loop test or update, function, or class field initializer lies between
|
||||
* them. A spread that runs again inserts the same elements again, and
|
||||
* copies would no longer be the same objects. */
|
||||
function runsOnceInScope(node: ts.Node, scope: ts.Node): boolean {
|
||||
let child = node;
|
||||
for (let n = node.parent; n && n !== scope; child = n, n = n.parent) {
|
||||
if (ts.isFunctionLike(n) || ts.isPropertyDeclaration(n)) return false;
|
||||
if (ts.isForOfStatement(n) || ts.isForInStatement(n)) {
|
||||
if (child !== n.expression) return false;
|
||||
} else if (ts.isForStatement(n)) {
|
||||
if (child !== n.initializer) return false;
|
||||
} else if (ts.isWhileStatement(n) || ts.isDoStatement(n)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -142,6 +143,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",
|
||||
|
||||
@@ -1380,6 +1380,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;
|
||||
|
||||
@@ -5501,7 +5501,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": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/fresh-spread-exported/items.ts",
|
||||
"<repo>/tests/diagnostics/fresh-spread-exported/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/fresh-spread-repeated.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,19 @@
|
||||
// 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]));
|
||||
// A const declared in the loop body is a new array on every iteration.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const step = [{ message: `step${i}`, file: undefined, noisy: false }];
|
||||
results.push(...step);
|
||||
}
|
||||
console.log(results.map((r) => r.message).join(" "), results[results.length - 1] === results[results.length - 2]);
|
||||
@@ -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"]);
|
||||
@@ -0,0 +1,10 @@
|
||||
// Exported consts: the importing module reaches their elements, so the
|
||||
// spreads here keep the element-layout refusal. Copying the elements
|
||||
// would hide main.ts's write from `results`.
|
||||
export type Result = { message: string; file: { path: string } | undefined; noisy: boolean };
|
||||
export const results: Result[] = [];
|
||||
export const shared = [{ message: "shared", file: undefined, noisy: false }];
|
||||
results.push(...shared);
|
||||
const listed = [{ message: "listed", file: undefined, noisy: true }];
|
||||
export { listed };
|
||||
results.unshift(...listed);
|
||||
@@ -0,0 +1,6 @@
|
||||
// An exported const array spread once in its own module: in Node, writes
|
||||
// through the importer's binding show in the array the spread filled.
|
||||
import { listed, results, shared } from "./items.js";
|
||||
shared[0].message = "changed";
|
||||
listed[0].noisy = false;
|
||||
console.log(results.map((r) => `${r.message}:${r.noisy}`).join(" "));
|
||||
@@ -0,0 +1,16 @@
|
||||
// A const array spread that runs more than once per evaluation of its
|
||||
// initializer inserts the same elements each time: in a loop body or in a
|
||||
// function, `results[1] === results[2]` holds in Node. Converting the
|
||||
// elements into the destination's layout would copy them on each run, so
|
||||
// these spreads keep the element-layout refusal.
|
||||
type Result = { message: string; file: { path: string } | undefined; noisy: boolean };
|
||||
const results: Result[] = [];
|
||||
const looped = [{ message: "loop", file: undefined, noisy: false }];
|
||||
for (let i = 0; i < 2; i++) results.push(...looped);
|
||||
const called = [{ message: "call", file: undefined, noisy: true }];
|
||||
function addCalled(): void {
|
||||
results.unshift(...called);
|
||||
}
|
||||
addCalled();
|
||||
addCalled();
|
||||
console.log(results.length, results[0] === results[1]);
|
||||
@@ -0,0 +1,13 @@
|
||||
fresh-spread-exported/items.ts:7:14 - error SC1090: pushing a spread of '{ file: null | undefined; message: string; noisy: boolean }[]' onto a '{ file: { path: string } | undefined; message: string; noisy: boolean }[]' array (only a same-element-type array spreads) is not supported yet
|
||||
|
||||
6 | export const shared = [{ message: "shared", file: undefined, noisy: false }];
|
||||
7 | results.push(...shared);
|
||||
| ^~~~~~~~~
|
||||
8 | const listed = [{ message: "listed", file: undefined, noisy: true }];
|
||||
|
||||
fresh-spread-exported/items.ts:10:17 - error SC1090: unshifting a spread of '{ file: null | undefined; message: string; noisy: boolean }[]' onto a '{ file: { path: string } | undefined; message: string; noisy: boolean }[]' array (only a same-element-type array spreads) is not supported yet
|
||||
|
||||
9 | export { listed };
|
||||
10 | results.unshift(...listed);
|
||||
| ^~~~~~~~~
|
||||
11 |
|
||||
@@ -0,0 +1,13 @@
|
||||
fresh-spread-repeated.ts:9:42 - error SC1090: pushing a spread of '{ file: null | undefined; message: string; noisy: boolean }[]' onto a '{ file: { path: string } | undefined; message: string; noisy: boolean }[]' array (only a same-element-type array spreads) is not supported yet
|
||||
|
||||
8 | const looped = [{ message: "loop", file: undefined, noisy: false }];
|
||||
9 | for (let i = 0; i < 2; i++) results.push(...looped);
|
||||
| ^~~~~~~~~
|
||||
10 | const called = [{ message: "call", file: undefined, noisy: true }];
|
||||
|
||||
fresh-spread-repeated.ts:12:19 - error SC1090: unshifting a spread of '{ file: null | undefined; message: string; noisy: boolean }[]' onto a '{ file: { path: string } | undefined; message: string; noisy: boolean }[]' array (only a same-element-type array spreads) is not supported yet
|
||||
|
||||
11 | function addCalled(): void {
|
||||
12 | results.unshift(...called);
|
||||
| ^~~~~~~~~
|
||||
13 | }
|
||||
Reference in New Issue
Block a user