mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
Read Number.prototype and Boolean.prototype as prototype objects
A JavaScript source reading Number.prototype got undefined: the checker types it with the wrapper interface, the type mapper maps that interface to f64, and nothing built a prototype object. The read now lowers, in JavaScript sources only, to dyn.builtinPrototype, served by a new gated unit that builds the two prototype objects the way the language defines them: an ordinary object on Object.prototype carrying the kind's methods as non-enumerable, writable, configurable data properties, answered from one box per thread so repeated reads are the same object. A method runs on the prototype's primitive (+0 / false), which is its [[PrimitiveValue]] in Node. Two follow-on gaps the read exposed are fixed with it: narrowing no longer extracts a scalar from a prototype value, and a call through the prototype no longer takes the dyn.toString/dyn.valueOf/primitive-method specializations, which assume a primitive receiver. The methods are not generic: a receiver that is neither the kind's primitive nor the prototype object — the wrapper around +0 / false, which is its [[PrimitiveValue]] — is Node's TypeError, so a copied `Number.prototype.toString` called on a plain object throws instead of answering 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6cea7a348f
commit
9558fcddf3
@@ -388,6 +388,15 @@ export function lowerTernary(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `expr` is the checked-dynamic Number/Boolean prototype value a
|
||||
* JavaScript source read (jsPrimitivePrototypeValue), or one of its
|
||||
* members: the value is the prototype OBJECT, not the wrapper's primitive,
|
||||
* so the checker's wrapper type must not extract a scalar from it. */
|
||||
function isPrimitivePrototypeValue(expr: IrExpr): boolean {
|
||||
if (expr.kind === "libCall") return expr.fn === "dyn.builtinPrototype";
|
||||
return expr.kind === "dynKeyGet" && isPrimitivePrototypeValue(expr.value);
|
||||
}
|
||||
|
||||
/** Checker-driven union narrowing. tsc's control-flow analysis narrows a
|
||||
* union-typed reference at use sites (`if (r.kind === "ok") { ...r... }`
|
||||
* types `r` as the ok-arm inside the branch); the IR value is still the
|
||||
@@ -429,6 +438,10 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
// instead of misreading the payload (the dyn boundary's usual stance).
|
||||
// Object/array narrowings stay dyn-typed and keep their fences.
|
||||
if (expr.type.kind === "dyn") {
|
||||
// Number.prototype and Boolean.prototype (and their members) are the
|
||||
// prototype OBJECTS, though @types/node declares them with the wrapper
|
||||
// interface: extracting the wrapper's numeric value would throw.
|
||||
if (isPrimitivePrototypeValue(expr)) return expr;
|
||||
// A process.versions entry declared as a string can be absent here.
|
||||
if (isOptionalProcessVersionsRead(lowerer, node))
|
||||
return lowerer.coerceToExpected(expr, lowerer.withUndefinedArm(STRING));
|
||||
|
||||
@@ -1914,6 +1914,8 @@ export function lowerIntrinsicProperty(
|
||||
lowerer.stdlibGlobalMember(expr.expression, "Object") === "prototype"
|
||||
)
|
||||
return builtinGlobalValue("Object", locOf(expr));
|
||||
const primitivePrototype = jsPrimitivePrototypeValue(lowerer, expr);
|
||||
if (primitivePrototype) return primitivePrototype;
|
||||
const builtinPrototype = builtinPrototypeMethod(lowerer, expr);
|
||||
if (builtinPrototype)
|
||||
return {
|
||||
@@ -5011,3 +5013,56 @@ function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set<stri
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** `Number.prototype` / `Boolean.prototype` in a JavaScript source: the
|
||||
* prototype object the checked-dynamic world builds (with its methods), or
|
||||
* one of its members read as a value. Typed sources keep their own
|
||||
* lowering, so no typed program links the unit that builds it. */
|
||||
function jsPrimitivePrototypeValue(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
const loc = locOf(expr);
|
||||
const value = (kind: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.builtinPrototype",
|
||||
args: [{ kind: "strLit", value: kind, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const receiver = expr.expression;
|
||||
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "prototype") {
|
||||
const kind = primitivePrototypeKind(lowerer, receiver);
|
||||
if (!kind) return null;
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: value(kind),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const kind = primitivePrototypeKind(lowerer, expr);
|
||||
return kind ? value(kind) : null;
|
||||
}
|
||||
|
||||
/** Whether `expr` names `Number.prototype` or `Boolean.prototype` in a
|
||||
* JavaScript source, which lowers to the prototype object above rather than
|
||||
* to the wrapper interface's numeric/boolean value. */
|
||||
export function isPrimitivePrototypeRead(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
return primitivePrototypeKind(lowerer, expr) !== null;
|
||||
}
|
||||
|
||||
/** The kind whose prototype `expr` names (`Number.prototype`) in a
|
||||
* JavaScript source, if any. */
|
||||
function primitivePrototypeKind(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): "Number" | "Boolean" | null {
|
||||
if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "prototype") return null;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
if (lowerer.stdlibGlobalMember(expr, "Number") === "prototype") return "Number";
|
||||
if (lowerer.stdlibGlobalMember(expr, "Boolean") === "prototype") return "Boolean";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ import {
|
||||
} from "./containers/indexed-objects.js";
|
||||
import { lowerTupleReadMethodCall } from "./containers/array-methods.js";
|
||||
import { tryFuseIndexRead } from "./array-values.js";
|
||||
import { isPrimitivePrototypeRead } from "./expressions/property-access.js";
|
||||
import {
|
||||
lowerBufferConstruct,
|
||||
lowerBytesPrototypeSetCall,
|
||||
@@ -3954,6 +3955,12 @@ function lowerDynReceiverMethodCall(
|
||||
)
|
||||
recv = lowerer.coerceToExpected(recv, DYN);
|
||||
if (recv.type.kind !== "dyn") return null;
|
||||
// `Number.prototype.m(...)` reads the prototype OBJECT, whose `m` is its
|
||||
// own member, so every primitive specialization below — which assumes a
|
||||
// primitive receiver and coerces the value it is given — would be wrong.
|
||||
// The shared dispatch reads the member and calls it with the prototype as
|
||||
// `this`, which is Node's answer for the wrapper around +0 / false.
|
||||
const prototypeReceiver = isPrimitivePrototypeRead(lowerer, access.expression);
|
||||
if (
|
||||
access.name.text === "normalize" &&
|
||||
call.arguments.nodes.length <= 1 &&
|
||||
@@ -4064,7 +4071,7 @@ function lowerDynReceiverMethodCall(
|
||||
// prototype declares (Array carries includes/indexOf/slice too), so no
|
||||
// other kind inherits them. Shared names would need a receiver-kind
|
||||
// dispatch — they keep the fence.
|
||||
if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) {
|
||||
if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable && !prototypeReceiver) {
|
||||
// A checked-dynamic object can still carry its OWN member of that
|
||||
// name: zod's schemas assign `trim`, `toLowerCase` and `startsWith`
|
||||
// in their constructors (`z.string().trim().min(1)`), an object's
|
||||
@@ -4094,6 +4101,7 @@ function lowerDynReceiverMethodCall(
|
||||
// runtime dispatch.
|
||||
if (
|
||||
!ownCallable &&
|
||||
!prototypeReceiver &&
|
||||
!call.questionDotToken &&
|
||||
!access.questionDotToken &&
|
||||
isDynPrimitiveKindMethod(access.name.text) &&
|
||||
@@ -4116,6 +4124,7 @@ function lowerDynReceiverMethodCall(
|
||||
access.name.text === "toString" &&
|
||||
call.arguments.nodes.length <= 1 &&
|
||||
!ownCallable &&
|
||||
!prototypeReceiver &&
|
||||
!(checkerUntyped && jsAssignsOwnToString(lowerer, access.expression))
|
||||
) {
|
||||
const argument = call.arguments.nodes[0];
|
||||
@@ -4134,7 +4143,7 @@ function lowerDynReceiverMethodCall(
|
||||
// Every dyn kind's prototype chain declares valueOf, and the runtime
|
||||
// answers each kind's own: primitives and plain objects themselves, a
|
||||
// Date its time value, an own or inherited valueOf member its result.
|
||||
if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) {
|
||||
if (access.name.text === "valueOf" && call.arguments.nodes.length === 0 && !prototypeReceiver) {
|
||||
const webAssembly = lowerWebAssemblyValueOf(lowerer, access.expression, recv, locOf(call));
|
||||
if (webAssembly !== null) return webAssembly;
|
||||
return {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-primitive-prototype-values.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Number.prototype and Boolean.prototype are objects in JavaScript: the
|
||||
// wrappers around +0 and false, carrying their methods as non-enumerable
|
||||
// data properties. Reading one used to answer undefined.
|
||||
console.log(typeof Number.prototype, typeof Boolean.prototype);
|
||||
console.log(Number.prototype === Number.prototype, Boolean.prototype === Boolean.prototype);
|
||||
console.log(Number.prototype.toString(), Number.prototype.toString(2), Number.prototype.toFixed(2));
|
||||
console.log(Number.prototype.valueOf(), Boolean.prototype.valueOf(), Boolean.prototype.toString());
|
||||
console.log(Object.getPrototypeOf(Number.prototype) === Object.prototype);
|
||||
console.log(Object.getPrototypeOf(Boolean.prototype) === Object.prototype);
|
||||
console.log(Number.prototype.hasOwnProperty('toString'), Number.prototype.hasOwnProperty('nope'));
|
||||
console.log(Object.keys(Number.prototype).length, Object.keys(Boolean.prototype).length);
|
||||
console.log(typeof Number.prototype.toString, Number.prototype.toString.name, Number.prototype.toString.length);
|
||||
console.log(Boolean.prototype.valueOf.name, Boolean.prototype.toString.name, Boolean.prototype.toString.length);
|
||||
|
||||
// The methods are not generic: only a primitive of the kind, or the
|
||||
// prototype object itself (the wrapper around +0 / false), is a `this`.
|
||||
const attempt = (label, run) => {
|
||||
try {
|
||||
console.log(label, 'no-throw', JSON.stringify(run()));
|
||||
} catch (error) {
|
||||
console.log(label, error.constructor.name, error.message);
|
||||
}
|
||||
};
|
||||
attempt('num-null', () => Number.prototype.toString.call(null));
|
||||
attempt('num-object', () => Number.prototype.toString.call({}));
|
||||
attempt('num-string', () => Number.prototype.valueOf.call('5'));
|
||||
attempt('num-bool', () => Number.prototype.toFixed.call(true));
|
||||
attempt('num-number', () => Number.prototype.toString.call(255, 16));
|
||||
attempt('bool-object', () => Boolean.prototype.valueOf.call({}));
|
||||
attempt('bool-number', () => Boolean.prototype.toString.call(1));
|
||||
attempt('bool-boolean', () => Boolean.prototype.valueOf.call(true));
|
||||
const holder = new Object();
|
||||
holder.toString = Number.prototype.toString;
|
||||
attempt('holder', () => holder.toString());
|
||||
const other = new Object();
|
||||
other.myValueOf = Number.prototype.valueOf;
|
||||
attempt('holder-valueOf', () => other.myValueOf());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 4600 }
|
||||
Reference in New Issue
Block a user