mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Keep the new lowering paths off already-typed code
The conditional-spread, asserted-probe, and asserted-element-probe commits each answered a shape that previously refused, but three paths also re-lowered code that already compiled: - A checked-dynamic literal's conditional source is now lowered arm by arm only when the literal is built for a declared record target, so a literal already built as a plain dynamic object keeps its established lowering. - An asserted member probe counts arms the way the extraction threshold always has, plus getter-backed arms beside an arm with no read at all: a union whose arms all declare the member keeps the assertion's extraction. - An asserted element probe checks the stored value's checker type before speculatively lowering the receiver, so a non-union receiver leaves no synthesized helpers behind. The zero-cost gate reports no difference for corpus/7020-conditional- spread-fields.ts, corpus/6474-index-record-spread-literals.ts, corpus/6475-dynamic-spread-literals.ts, corpus/asserted-arm-probe.ts, and corpus/8014-foreign-element-callbacks.ts; the new corpus programs (8501-8504, 8510-8513, tuple-dynamic-index, record-subset-keyed-read, union-mixed-field-write) pin the behavior both ways. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
378901df95
commit
c86b3a153b
@@ -120,33 +120,32 @@ export function assertedProbeAccessOf(
|
||||
}
|
||||
|
||||
/** True when the union's record arms read `member` unevenly: at least one
|
||||
* declares it — as a data field or as a getter, which Node's read calls —
|
||||
* and at least one does not (an index-signature arm may hold it, which the
|
||||
* read answers per value; an arm with only a setter of that name reads
|
||||
* undefined). Those are exactly the arms assertedMemberRead answers per
|
||||
* arm; anything else keeps the assertion's extraction (and with it the
|
||||
* checked-narrow fence for a lying one). */
|
||||
* declares it as a data field and at least one does not (it declares a
|
||||
* getter instead, or nothing), or at least one answers it through a getter
|
||||
* while another arm has no read of it at all. Those are exactly the arms
|
||||
* assertedMemberRead answers per arm; anything else keeps the assertion's
|
||||
* extraction (and with it the checked-narrow fence for a lying one). An arm
|
||||
* with only a setter of that name reads undefined (Node never invokes a
|
||||
* setter on a read), so it counts as an arm with no read. */
|
||||
export function partialRecordMember(lowerer: Lowerer, union: IrType, member: string): boolean {
|
||||
if (union.kind !== "union") return false;
|
||||
const arms = lowerer.unions.get(union.unionId)?.arms ?? [];
|
||||
let records = 0;
|
||||
let declaring = 0;
|
||||
let dataFields = 0;
|
||||
let getters = 0;
|
||||
let absent = 0;
|
||||
for (const arm of arms) {
|
||||
if (isUnitType(arm)) continue;
|
||||
if (arm.kind !== "record") return false;
|
||||
const shape = lowerer.shapes.get(arm.shapeId);
|
||||
if (!shape || shape.tuple) return false;
|
||||
records++;
|
||||
// A setter-only arm is not a declaring arm (the read answers undefined,
|
||||
// Node never invokes a setter on a read), but it no longer disqualifies
|
||||
// the union: the per-arm answer names it.
|
||||
if (
|
||||
shape.fields.some((field) => field.name === member) ||
|
||||
shape.fields.some((field) => field.name === `%get:${member}`)
|
||||
)
|
||||
declaring++;
|
||||
if (shape.fields.some((field) => field.name === member)) dataFields++;
|
||||
else if (shape.fields.some((field) => field.name === `%get:${member}`)) getters++;
|
||||
else absent++;
|
||||
}
|
||||
return records > 0 && declaring > 0 && declaring < records;
|
||||
if (records === 0) return false;
|
||||
return (dataFields > 0 && dataFields < records) || (getters > 0 && absent > 0);
|
||||
}
|
||||
|
||||
/** The asserted record type whose checked extraction is deferred for the
|
||||
|
||||
@@ -109,8 +109,13 @@ function assertedProbeElementRead(
|
||||
if (assertion === null || assertedProbeAccessOf(assertion) !== expr) return null;
|
||||
const name = probeMemberName(expr);
|
||||
if (name === null) return null;
|
||||
// Only a union of arms has a per-arm answer. Check the checker type first:
|
||||
// speculatively lowering a receiver that is not a union would leave the
|
||||
// helpers it synthesized behind even though the probe declines.
|
||||
if ((lowerer.typeOf(assertion.expression).flags & ts.TypeFlags.Union) === 0) return null;
|
||||
const recv = tryLowerExpression(lowerer, expr.expression);
|
||||
if (recv === null) return null;
|
||||
if (recv.type.kind !== "union") return null;
|
||||
return assertedMemberRead(lowerer, expr.expression, name, recv, locOf(expr));
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ export function lowerDynObjectLiteral(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ObjectLiteralExpression,
|
||||
boxValue?: (node: ts.Expression, value: IrExpr) => IrExpr,
|
||||
conditionalArms = false,
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
let fields: { key: IrExpr; value: IrExpr }[] = [];
|
||||
@@ -308,6 +309,7 @@ export function lowerDynObjectLiteral(
|
||||
builtin === "path" || builtin === "path/posix" || builtin === "path/win32"
|
||||
? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop))
|
||||
: !boxValue &&
|
||||
conditionalArms &&
|
||||
(conditionalSpreadOf(prop.expression) !== null ||
|
||||
andSpreadOf(lowerer, prop.expression) !== null)
|
||||
? lowerConditionalDynSource(lowerer, prop.expression)
|
||||
@@ -3009,7 +3011,11 @@ function dynSpreadLiteral(
|
||||
)
|
||||
)
|
||||
return null;
|
||||
return lowerer.coerceToExpected(lowerDynObjectLiteral(lowerer, expr), type);
|
||||
// The target is a declared record, so a conditional source's arm has the
|
||||
// record as its checker context: lower the arms as the dyn values whose
|
||||
// own keys the copy reads, not against the record one arm need not
|
||||
// complete.
|
||||
return lowerer.coerceToExpected(lowerDynObjectLiteral(lowerer, expr, undefined, true), type);
|
||||
}
|
||||
|
||||
/** A conditional spread source in a checked-dynamic literal, as the dyn
|
||||
|
||||
Reference in New Issue
Block a user