mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Keep the fromEntries assertion bridge to entries without native arrays or records
Object.fromEntries(...) as T passed the entries through the checked-dynamic bridge, which copies native arrays and records: an array shared by two entries, or held elsewhere, became separate copies, and writes through the asserted object no longer reached it. The bridge now applies only when the entries' values are scalars or checked-dynamic values, which pass by value or by identity; other entries keep the exact-shape fence (SC2002). Corpus 6467 checks that entries held as unknown keep their identity; diagnostics fixture fromentries-aliased-entries.ts pins the fence.
This commit is contained in:
@@ -1194,9 +1194,11 @@ function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
|
||||
* fresh object nothing else references, so the checked-dynamic bridge,
|
||||
* which keeps keys and their order the same way `JSON.parse(text) as T`
|
||||
* does, cannot be told apart from the original object; a value outside
|
||||
* T throws the catchable TypeError of every checked cast. Other sources
|
||||
* may be aliased, where a rebuilt object would sever mutation, so they
|
||||
* keep the exact-shape fence. */
|
||||
* T throws the catchable TypeError of every checked cast. The entries'
|
||||
* values must be scalars or checked-dynamic values, which the bridge
|
||||
* passes by value or by identity; native arrays and records would be
|
||||
* copied. Other sources may be aliased, where a rebuilt object would sever
|
||||
* mutation, so they keep the exact-shape fence. */
|
||||
function freshEntriesRecordAssertion(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.AsExpression | ts.TypeAssertion,
|
||||
@@ -1208,6 +1210,10 @@ function freshEntriesRecordAssertion(
|
||||
const dest = lowerer.shapes.get(target.shapeId);
|
||||
if (!source?.indexValue || source.tuple || source.fields.length > 0) return null;
|
||||
if (!dest || dest.tuple || dest.indexValue || shapeHasAccessorSlots(dest)) return null;
|
||||
// Native arrays and records in the entries would be copied on the way
|
||||
// through the checked-dynamic bridge, severing their identity: only
|
||||
// slots of scalars or checked-dynamic values pass.
|
||||
if (extractionCopiesReference(lowerer, source.indexValue)) return null;
|
||||
let operand: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
if (
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/fromentries-aliased-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// `Object.fromEntries(...) as T` for a declared record T keeps exactly
|
||||
// the keys the entries created, in their order, as Node keeps the object
|
||||
// itself.
|
||||
// itself, as long as the entries hold scalars or checked-dynamic values.
|
||||
type Tags = { "error.type": string; "error.msg": string; "error.cause"?: string };
|
||||
|
||||
function scrub(input: string): string {
|
||||
@@ -27,3 +27,17 @@ const p = Object.fromEntries([
|
||||
["z", 3],
|
||||
]) as Point;
|
||||
console.log(p, p.x + p.y, Object.keys(p), JSON.stringify(p));
|
||||
|
||||
// Entries held as unknown keep their identity: the object's members are
|
||||
// the same array and record, and writes through either reach both.
|
||||
type Holder = { list: number[]; point: { x: number } };
|
||||
const list = [1, 2];
|
||||
const point = { x: 1 };
|
||||
const entries: [string, unknown][] = [
|
||||
["list", list],
|
||||
["point", point],
|
||||
];
|
||||
const held = Object.fromEntries(entries) as Holder;
|
||||
held.list.push(3);
|
||||
held.point.x = 5;
|
||||
console.log(list.length, point.x, held.list === list, held.point === point);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// `Object.fromEntries(...) as T` keeps the object only when the entries'
|
||||
// values are scalars or checked-dynamic values. Native arrays and records
|
||||
// in the entries would be copied, severing their identity, so the
|
||||
// assertion keeps the exact-shape fence (corpus 6467 covers the cases that
|
||||
// lower).
|
||||
type Scores = { a: number[]; b: number[] };
|
||||
const shared = [10];
|
||||
const scores = Object.fromEntries([
|
||||
["a", shared],
|
||||
["b", shared],
|
||||
]) as Scores;
|
||||
scores.a.push(11);
|
||||
console.log(shared.length, scores.b.length);
|
||||
@@ -0,0 +1,8 @@
|
||||
fromentries-aliased-entries.ts:8:16 - error SC2002: record shapes must match exactly or width-coerce: expected '{ a: number[]; b: number[] }', got '{ [key: string]: number[] }' — 'a' is not a declared field of the source, and the source's index signature could hold it at runtime (a completed undefined would drop that value)
|
||||
|
||||
7 | const shared = [10];
|
||||
8 | const scores = Object.fromEntries([
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
9 | ["a", shared],
|
||||
|
||||
hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead
|
||||
Reference in New Issue
Block a user