mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Compile primitive wrapper objects as ordinary dynamic objects
new String/Number/Boolean and Object(primitive) refused in JavaScript. A wrapper is now an ordinary dictionary-mode object whose prototype is the new gated unit's String, Number or Boolean prototype, with the [[PrimitiveValue]] slot on the object's source identity. Coercions, property reads, method calls, key order, identity and typeof reuse the dynamic tier; the prototype methods read the slot. Once a program creates wrappers, the boxed types map to the dynamic value, instanceof tests the prototype chain, Object.prototype.toString and util.types read the slot, and the consumers that cannot read it yet (JSON, inspect, structuredClone, deep equality) refuse a wrapper loudly. Programs without a wrapper site are unchanged. The npm suite's node24-text-utilities fixture builds wrappers, so that case now runs instead of refusing with SC2020; it records the divergence it now shows (the error formatter names a wrapper's class Object, and function values print without their source) instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fd7be9993d
commit
ad449b53ea
@@ -1,3 +1,4 @@
|
||||
import { lowerWrapperToObject, programCreatesWrappers } from "../primitive-wrappers.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js";
|
||||
import { countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
@@ -50,6 +51,13 @@ export function lowerObjectConstructor(
|
||||
}
|
||||
if (args.length !== 1 || ts.isSpreadElement(args[0]!)) return null;
|
||||
const value = lowerer.lowerExpr(args[0]!);
|
||||
// ToObject of a value that may be a primitive, once the program creates
|
||||
// wrapper objects (programCreatesWrappers counts this very call).
|
||||
if (
|
||||
["dyn", "string", "f64", "bool"].includes(value.type.kind) &&
|
||||
(lowerer.createsWrappers ??= programCreatesWrappers(lowerer))
|
||||
)
|
||||
return lowerWrapperToObject(lowerer, lowerer.coerceToExpected(value, DYN), loc);
|
||||
if (value.kind === "dynObjLit" || value.kind === "dynArrLit") return value;
|
||||
if (
|
||||
[
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* instanceof and in, including private-name and runtime-key in. */
|
||||
import { programCreatesWrappers, WRAPPER_KIND_NUMBER } from "../primitive-wrappers.js";
|
||||
import { lowerArrayClassInstanceOf } from "../builtins/array-classes.js";
|
||||
import { dateRootOf } from "../builtins/date-classes.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
@@ -201,6 +202,23 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
// (typebox's IsBoolean/IsNumber/IsString guards). The operand still runs.
|
||||
if (PRIMITIVE_WRAPPERS.some((name) => lowerer.isStdlibGlobal(expr.right, name))) {
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
// In a program that creates wrappers, a checked-dynamic value may be
|
||||
// one: the kind's prototype on its chain decides.
|
||||
const wrapperKind = (["String", "Number", "Boolean"] as const).find((name) =>
|
||||
lowerer.isStdlibGlobal(expr.right, name),
|
||||
);
|
||||
if (
|
||||
wrapperKind &&
|
||||
value.type.kind === "dyn" &&
|
||||
(lowerer.createsWrappers ??= programCreatesWrappers(lowerer))
|
||||
)
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "wrapper.instanceOf",
|
||||
args: [value, { kind: "numLit", value: WRAPPER_KIND_NUMBER[wrapperKind], type: F64, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: value, loc }],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { programCreatesWrappers } from "./primitive-wrappers.js";
|
||||
import {
|
||||
arrayClassMemberOwner,
|
||||
arrayClassReceiver,
|
||||
@@ -4045,7 +4046,9 @@ function lowerDynReceiverMethodCall(
|
||||
// else calls its member as Node does.
|
||||
if (
|
||||
(checkerUntyped ||
|
||||
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) &&
|
||||
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 ||
|
||||
// A receiver of the boxed String type may be a wrapper object.
|
||||
(lowerer.createsWrappers ??= programCreatesWrappers(lowerer))) &&
|
||||
!call.questionDotToken &&
|
||||
!access.questionDotToken &&
|
||||
!call.arguments.nodes.some(ts.isSpreadElement)
|
||||
@@ -4289,11 +4292,50 @@ function lowerStringOrMemberCall(
|
||||
const loc = locOf(call);
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
|
||||
const receiver = varRef(receiverLocal.id, DYN, loc);
|
||||
const isString: IrExpr = { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc };
|
||||
const nameLit: IrExpr = { kind: "strLit", value: access.name.text, type: STRING, loc };
|
||||
const receiverIsString: IrExpr = {
|
||||
kind: "dynTest",
|
||||
test: "string",
|
||||
value: receiver,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
// In a program that creates wrapper objects, a receiver whose member is
|
||||
// the builtin String.prototype method runs the string lowering on
|
||||
// ToString of the receiver, after the arguments, as the builtin does.
|
||||
const wrappers = (lowerer.createsWrappers ??= programCreatesWrappers(lowerer));
|
||||
const builtinLocal = wrappers ? lowerer.declareHiddenLocal("%dynCallStringMethod", BOOL) : null;
|
||||
const isString: IrExpr = builtinLocal
|
||||
? { kind: "varRef", localId: builtinLocal.id, type: BOOL, loc }
|
||||
: receiverIsString;
|
||||
const stringLocal = wrappers ? lowerer.declareHiddenLocal("%dynCallString", DYN) : null;
|
||||
const calleeLocal = lowerer.declareHiddenLocal("%dynCallCallee", DYN);
|
||||
const callee = varRef(calleeLocal.id, DYN, loc);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc },
|
||||
...(builtinLocal
|
||||
? [
|
||||
{
|
||||
kind: "varDecl" as const,
|
||||
localId: builtinLocal.id,
|
||||
init: {
|
||||
kind: "logical" as const,
|
||||
op: "||" as const,
|
||||
left: receiverIsString,
|
||||
right: {
|
||||
kind: "libCall" as const,
|
||||
fn: "wrapper.isStringMethod" as const,
|
||||
args: [receiver, nameLit],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: calleeLocal.id,
|
||||
@@ -4315,14 +4357,28 @@ function lowerStringOrMemberCall(
|
||||
},
|
||||
];
|
||||
const args = call.arguments.nodes.map((node) => {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
const lowered = lowerer.lowerExpr(node);
|
||||
// A unit-typed argument (`f()` of a void function, a literal undefined
|
||||
// or null) has no local storage of its own: hold it as the dynamic
|
||||
// value both branches receive.
|
||||
const value =
|
||||
lowered.type.kind === "void" ||
|
||||
lowered.type.kind === "undefinedT" ||
|
||||
lowered.type.kind === "nullT"
|
||||
? lowerer.coerceToExpected(lowered, DYN)
|
||||
: lowered;
|
||||
const local = lowerer.declareHiddenLocal("%dynCallArg", value.type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
|
||||
return { node, value: varRef(local.id, value.type, value.loc) };
|
||||
});
|
||||
// String.prototype methods ignore arguments past the ones they read.
|
||||
const arity = DYN_STRING_ONLY_ARITY[access.name.text] ?? call.arguments.nodes.length;
|
||||
const stringReceiver = (): IrExpr => ({ kind: "dynCheck", value: receiver, type: STRING, loc });
|
||||
const stringReceiver = (): IrExpr => ({
|
||||
kind: "dynCheck",
|
||||
value: stringLocal ? varRef(stringLocal.id, DYN, loc) : receiver,
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
const lowerString = (): IrExpr | null =>
|
||||
(call.arguments.nodes.length <= arity
|
||||
? lowerRegexMethodCall(lowerer, call, access, stringReceiver)
|
||||
@@ -4351,7 +4407,35 @@ function lowerStringOrMemberCall(
|
||||
kind: "ternary",
|
||||
cond: isString,
|
||||
then: stringCall
|
||||
? lowerer.coerceToExpected(stringCall, DYN)
|
||||
? stringLocal
|
||||
? {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: stringLocal.id,
|
||||
init: {
|
||||
kind: "ternary",
|
||||
cond: receiverIsString,
|
||||
then: receiver,
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "wrapper.thisString",
|
||||
args: [receiver, nameLit],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: lowerer.coerceToExpected(stringCall, DYN),
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: lowerer.coerceToExpected(stringCall, DYN)
|
||||
: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerWrapperNew } from "./primitive-wrappers.js";
|
||||
import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js";
|
||||
import { shapeOverlayAnnotated } from "../shape-inference/index.js";
|
||||
import {
|
||||
@@ -10189,6 +10190,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr));
|
||||
}
|
||||
}
|
||||
const wrapper = lowerWrapperNew(lowerer, expr);
|
||||
if (wrapper) return wrapper;
|
||||
const worker = lowerWorkerNew(lowerer, expr);
|
||||
if (worker) return worker;
|
||||
const readlineUi = lowerReadlineUiNew(lowerer, expr) ?? lowerReadlineInterfaceNew(lowerer, expr);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { programCreatesWrappers, routeWrapperConsumers } from "./primitive-wrappers.js";
|
||||
import type { DiagnosticCode } from "../../diagnostics/codes.js";
|
||||
import { markInferredFactChecks, shapeOverlayFileNames } from "../shape-inference/index.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
@@ -3485,6 +3486,9 @@ export class Lowerer {
|
||||
* interned here so references under one source-world ABI are the SAME
|
||||
* zero-capture closure (see primitiveCtorClosure). */
|
||||
readonly primitiveCtorFns = new Map<string, string>();
|
||||
/** Whether a compiled source creates primitive wrapper objects
|
||||
* (programCreatesWrappers), computed on first use. */
|
||||
createsWrappers: boolean | undefined = undefined;
|
||||
/** Builtin-module FUNCTIONS admitted as values by their surface-table
|
||||
* entries. The resolved runtime function plus ABI is the identity key,
|
||||
* so named imports, namespace reads, CommonJS bindings, and alias chains
|
||||
@@ -5134,6 +5138,7 @@ export class Lowerer {
|
||||
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
|
||||
classImplementedProtocol: (type) => this.classImplementedProtocol(type),
|
||||
isStdlibFile: (sf) => this.isStdlibFile(sf),
|
||||
primitiveWrappers: () => (this.createsWrappers ??= programCreatesWrappers(this)),
|
||||
isNpmFile: (sf) => this.isNpmFile(sf),
|
||||
isExperimentalFile: (sf) => isExperimentalDeclarationFile(sf, this.experimentalFiles),
|
||||
// The declaration-typed boundary's generated types (decl-projection.ts)
|
||||
@@ -9655,6 +9660,7 @@ export class Lowerer {
|
||||
if (this.diags.length === 0) fuseArrayPipelines(this, functions);
|
||||
finalizeClassMethodValues(this, functions);
|
||||
finalizePrivateBrands(this, functions);
|
||||
routeWrapperConsumers(this, functions);
|
||||
trackClassFieldCreation(this, functions);
|
||||
boxClassInstancesExactly(this, functions);
|
||||
addUntypedIntegrityWords(this);
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Primitive wrapper objects (`new String(s)`, `new Number(n)`, `new
|
||||
* Boolean(b)`, `Object(primitive)`) in JavaScript sources. A wrapper is an
|
||||
* ordinary dynamic object (scr_js_wrapper.c): its prototype is the
|
||||
* wrapper unit's String/Number/Boolean prototype object, whose valueOf,
|
||||
* toString and methods read the [[PrimitiveValue]] slot, and a String
|
||||
* wrapper owns its index properties and `length`. Every generic dynamic
|
||||
* operation (ToPrimitive, property reads, method calls, key listing,
|
||||
* identity, typeof) already handles that shape; this module builds the
|
||||
* values and routes the few consumers that must see the slot itself. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { transformStmtList } from "../../ir/traverse.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { primitiveCtorClosure } from "./lower-exprs.js";
|
||||
|
||||
export type WrapperKind = "String" | "Number" | "Boolean";
|
||||
|
||||
/** The runtime's kind numbers (scr_js_wrapper.c SCR_JS_WRAPPER_*). */
|
||||
export const WRAPPER_KIND_NUMBER: Record<WrapperKind, number> = {
|
||||
String: 0,
|
||||
Number: 1,
|
||||
Boolean: 2,
|
||||
};
|
||||
|
||||
/** Whether any compiled source creates a primitive wrapper: a `new` of the
|
||||
* stdlib String, Number, Boolean or Object constructor with an argument, or
|
||||
* a call of the stdlib Object with an argument. Until a program has one, no
|
||||
* value is a wrapper, so the boxed interface types keep their primitive
|
||||
* mapping and every fold that relies on it stays. */
|
||||
export function programCreatesWrappers(lowerer: Lowerer): boolean {
|
||||
for (const sf of lowerer.program.getSourceFiles()) {
|
||||
if (sf.isDeclarationFile || !isJsSourceFile(sf)) continue;
|
||||
const text = sf.text;
|
||||
if (
|
||||
!text.includes("String(") &&
|
||||
!text.includes("Number(") &&
|
||||
!text.includes("Boolean(") &&
|
||||
!text.includes("Object(")
|
||||
)
|
||||
continue;
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
(ts.isNewExpression(node) || ts.isCallExpression(node)) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
(node.arguments?.nodes.length ?? 0) > 0
|
||||
) {
|
||||
const name = node.expression.text;
|
||||
const isNew = ts.isNewExpression(node);
|
||||
if (
|
||||
((isNew && (name === "String" || name === "Number" || name === "Boolean")) ||
|
||||
name === "Object") &&
|
||||
lowerer.isStdlibGlobal(node.expression, name)
|
||||
) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sf);
|
||||
if (found) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** `new String(x)` / `new Number(x)` / `new Boolean(x)` in a JavaScript
|
||||
* source: the wrapper over ToString/ToNumber/ToBoolean of the argument
|
||||
* (the runtime converts, with the object hooks), or over "", 0, false
|
||||
* without one. Null outside JavaScript sources and for other callees. */
|
||||
export function lowerWrapperNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
if (!ts.isIdentifier(expr.expression)) return null;
|
||||
const kind = expr.expression.text;
|
||||
if (kind !== "String" && kind !== "Number" && kind !== "Boolean") return null;
|
||||
if (!lowerer.isStdlibGlobal(expr.expression, kind)) return null;
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
if (args.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(expr);
|
||||
// Arguments past the first are evaluated and ignored.
|
||||
const value: IrExpr =
|
||||
args.length > 0
|
||||
? lowerer.lowerExprExpecting(args[0]!, DYN)
|
||||
: lowerer.coerceToExpected(
|
||||
kind === "String"
|
||||
? { kind: "strLit", value: "", type: STRING, loc }
|
||||
: kind === "Number"
|
||||
? { kind: "numLit", value: 0, type: F64, loc }
|
||||
: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
DYN,
|
||||
);
|
||||
const rest = args.slice(1).map((arg) => ({
|
||||
kind: "exprStmt" as const,
|
||||
expr: lowerer.lowerExpr(arg),
|
||||
loc: locOf(arg),
|
||||
}));
|
||||
const created: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "wrapper.new",
|
||||
args: [
|
||||
{ kind: "numLit", value: WRAPPER_KIND_NUMBER[kind], type: F64, loc },
|
||||
value,
|
||||
...wrapperConstructors(lowerer, loc),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
if (rest.length === 0) return created;
|
||||
const local = lowerer.declareHiddenLocal("%wrapperArg", DYN);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }, ...rest],
|
||||
result: {
|
||||
...created,
|
||||
args: [
|
||||
created.args[0]!,
|
||||
{ kind: "varRef", localId: local.id, type: DYN, loc },
|
||||
...created.args.slice(2),
|
||||
],
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The String, Number and Boolean constructor values, for the prototypes'
|
||||
* `constructor` property (the interned closures `x === String` compares). */
|
||||
export function wrapperConstructors(lowerer: Lowerer, loc: SrcLoc): IrExpr[] {
|
||||
return (["String", "Number", "Boolean"] as const).map((name) =>
|
||||
lowerer.coerceToExpected(primitiveCtorClosure(lowerer, name, loc, true), DYN),
|
||||
);
|
||||
}
|
||||
|
||||
/** Consumers that walk a dynamic value's whole graph and must see the
|
||||
* [[PrimitiveValue]] slot (JSON's unwrapping, util.inspect's `[String:
|
||||
* 'x']`, structuredClone's wrapper clone, deep equality's primitive
|
||||
* compare). Until they know the slot, a wrapper reaching one refuses: each
|
||||
* dynamic argument passes through wrapper.guard, which throws the
|
||||
* not-supported Error when the value's own data graph holds a wrapper. */
|
||||
const GUARDED_CONSUMERS: ReadonlySet<string> = new Set([
|
||||
"json.stringifyValue",
|
||||
"json.stringifyReplacer",
|
||||
"util.isDeepStrictEqual",
|
||||
"dyn.structuredClone",
|
||||
"assert.looseDeepDyn",
|
||||
"assert.partialDeepDyn",
|
||||
"console.dirDyn",
|
||||
"insp.format",
|
||||
"insp.formatPrimitives",
|
||||
"insp.formatList",
|
||||
"insp.formatWithOptions",
|
||||
"insp.inspect",
|
||||
"insp.dynS",
|
||||
"insp.dynFormatS",
|
||||
]);
|
||||
|
||||
/** util.types brands that read the [[PrimitiveValue]] slot. */
|
||||
const WRAPPER_TYPE_PROBES: ReadonlySet<string> = new Set([
|
||||
"isStringObject",
|
||||
"isNumberObject",
|
||||
"isBooleanObject",
|
||||
"isBoxedPrimitive",
|
||||
]);
|
||||
|
||||
/** The consumer as the refusal names it. */
|
||||
function consumerName(fn: string): string {
|
||||
if (fn.startsWith("json.")) return "JSON.stringify";
|
||||
if (fn === "dyn.structuredClone") return "structuredClone";
|
||||
if (fn === "util.isDeepStrictEqual") return "util.isDeepStrictEqual";
|
||||
if (fn.startsWith("assert.")) return "assert's deep equality";
|
||||
return "console.log/util.inspect";
|
||||
}
|
||||
|
||||
function guarded(arg: IrExpr, fn: string): IrExpr {
|
||||
const consumer = consumerName(fn);
|
||||
if (arg.type.kind !== "dyn" && !(arg.type.kind === "array" && arg.type.elem.kind === "dyn"))
|
||||
return arg;
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: arg.type.kind === "dyn" ? "wrapper.guard" : "wrapper.guardList",
|
||||
args: [arg, { kind: "strLit", value: consumer, type: STRING, loc: arg.loc }],
|
||||
type: arg.type,
|
||||
loc: arg.loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** In a program that creates wrappers, route the consumers that read
|
||||
* internal slots: Object.prototype.toString's tag and the util.types
|
||||
* brands answer from the slot (wrapper.objectTag, wrapper.typeIs), and the
|
||||
* graph-walking consumers above guard their dynamic arguments. Programs
|
||||
* without a wrapper site keep every node unchanged. */
|
||||
export function routeWrapperConsumers(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
if (!(lowerer.createsWrappers ??= programCreatesWrappers(lowerer))) return;
|
||||
const transform = {
|
||||
expr: (expr: IrExpr): IrExpr => {
|
||||
if (expr.kind === "libCall") {
|
||||
if (expr.fn === "dyn.objectTag") return { ...expr, fn: "wrapper.objectTag" };
|
||||
if (
|
||||
expr.fn === "util.typeIs" &&
|
||||
expr.args[1]?.kind === "strLit" &&
|
||||
WRAPPER_TYPE_PROBES.has(expr.args[1].value)
|
||||
)
|
||||
return { ...expr, fn: "wrapper.typeIs" };
|
||||
if (GUARDED_CONSUMERS.has(expr.fn))
|
||||
return { ...expr, args: expr.args.map((arg) => guarded(arg, expr.fn)) };
|
||||
}
|
||||
if (
|
||||
expr.kind === "intrinsic" &&
|
||||
(expr.name === "console.log" || expr.name === "console.error")
|
||||
)
|
||||
return { ...expr, args: expr.args.map((arg) => guarded(arg, expr.name)) };
|
||||
return expr;
|
||||
},
|
||||
stmt: (stmt: IrStmt): IrStmt => stmt,
|
||||
};
|
||||
for (const fn of functions) fn.body = transformStmtList(fn.body, transform);
|
||||
}
|
||||
|
||||
/** `Object(x)` / `new Object(x)` over a checked-dynamic `x` in a program
|
||||
* that creates wrappers: ToObject (wrapper.toObject), a wrapper for a
|
||||
* primitive, the value itself for an object, a fresh object for null and
|
||||
* undefined. */
|
||||
export function lowerWrapperToObject(lowerer: Lowerer, value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const ctors = wrapperConstructors(lowerer, loc);
|
||||
return { kind: "libCall", fn: "wrapper.toObject", args: [value, ...ctors], type: DYN, loc };
|
||||
}
|
||||
@@ -1528,8 +1528,16 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean {
|
||||
// runtime meaning in a JS file.
|
||||
if (d.code >= 8000 && d.code <= 8999) return true;
|
||||
if (JS_ANY_OPERATOR_CODES.has(d.code)) {
|
||||
return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'");
|
||||
const text = ts.flattenDiagnosticMessageText(d, "\n");
|
||||
// A primitive wrapper operand (`new Number(1) * 2`): the operator
|
||||
// converts it with ToPrimitive, as for any object.
|
||||
return text.includes("'any'") || /'(Number|String|Boolean)'/.test(text);
|
||||
}
|
||||
// An index write on a String wrapper (`new String("ab")[5] = "q"`): Node
|
||||
// adds the property past the end and ignores writes to the read-only
|
||||
// index properties (sloppy) or throws (strict), as the object does.
|
||||
if (d.code === 2542 && ts.flattenDiagnosticMessageText(d, "\n").includes("type 'String'"))
|
||||
return true;
|
||||
// The deprecated Buffer(...) call without `new`, which Node's Buffer
|
||||
// function accepts (containers/bytes.ts lowers it), and fs.ReadStream(...)
|
||||
// and fs.WriteStream(...), which construct their stream
|
||||
|
||||
@@ -114,6 +114,9 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
|
||||
// differs (adopted options, hidden declaration twins), and an unused
|
||||
// suppression changes nothing at runtime
|
||||
2578,
|
||||
// a primitive on the left of instanceof (`"x" instanceof String`): Node
|
||||
// answers false, which the lowering folds
|
||||
2358,
|
||||
// a lib-floor demand (es2024+ members under the es2023 lib): the member
|
||||
// types as the error-any and its use fences per site with the honest
|
||||
// no-lowering story instead of gating the whole program
|
||||
|
||||
@@ -1369,6 +1369,11 @@ export type TypeParamTsResolver = (t: ts.Type) => ts.Type | null;
|
||||
* once per Lowerer and threaded through the recursion — adding a hook means
|
||||
* one new field here, not a new parameter at every call site. */
|
||||
export interface TypeMapperCtx {
|
||||
/** Whether the program creates primitive wrapper objects
|
||||
* (primitive-wrappers.ts): then the boxed interface types (`String`,
|
||||
* `Number`, `Boolean`) map to the checked-dynamic value instead of the
|
||||
* primitive. Absent or false: every mapping is unchanged. */
|
||||
primitiveWrappers?: () => boolean;
|
||||
/** Node islands (scope-B NI-4): whether a type's values are island
|
||||
* handles, which map to the `foreign` type (a dyn value). Absent in
|
||||
* programs without islands. */
|
||||
@@ -2408,6 +2413,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
ts.symbolName(boxSym) === "Boolean") &&
|
||||
boxSym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d)))
|
||||
) {
|
||||
// Once the program creates wrapper objects, a slot of the boxed type
|
||||
// may hold one: the checked-dynamic value holds both.
|
||||
if (ctx.primitiveWrappers?.()) return DYN;
|
||||
return ts.symbolName(boxSym) === "Number"
|
||||
? F64
|
||||
: ts.symbolName(boxSym) === "String"
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-primitive-wrappers.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Primitive wrapper objects: new String/Number/Boolean and Object(primitive).
|
||||
// A wrapper is an object (typeof, identity, own properties, prototype
|
||||
// chain, instanceof, its builtin tag) and converts to its primitive in every
|
||||
// coercion (ToPrimitive through valueOf/toString, ToNumber, property keys,
|
||||
// method receivers). ToBoolean of any wrapper is true.
|
||||
|
||||
const s = new String("lego");
|
||||
const n = new Number(4.5);
|
||||
const b = new Boolean(false);
|
||||
console.log("typeof", typeof s, typeof n, typeof b);
|
||||
console.log("identity", s === s, new String("a") === new String("a"), s == "lego", s === "lego");
|
||||
console.log("valueOf", s.valueOf() === "lego", n.valueOf() + 1, b.valueOf());
|
||||
console.log("coerce", s + "!", `${s}`, String(s), n * 2, +n, n + 1, Number(n));
|
||||
console.log("truthy", b ? "wrapper true" : "wrapper false", !!b, b == false);
|
||||
console.log("string reads", s.length, s[1], s.charAt(0), s.toUpperCase(), s.slice(1, 3));
|
||||
console.log("number methods", n.toFixed(2), n.toString(), new Number(255).toString(16));
|
||||
console.log("instanceof", s instanceof String, n instanceof Number, b instanceof Boolean, s instanceof Object);
|
||||
console.log("not instances", "x" instanceof String, s instanceof Number);
|
||||
console.log("tags", Object.prototype.toString.call(s), Object.prototype.toString.call(n), Object.prototype.toString.call(b));
|
||||
console.log("constructor", s.constructor === String, n.constructor === Number, b.constructor === Boolean);
|
||||
console.log("prototype", Object.getPrototypeOf(s) === Object.getPrototypeOf(new String("z")));
|
||||
|
||||
// Own properties: index properties and length, then expandos.
|
||||
const w = new String("ab");
|
||||
w.extra = 1;
|
||||
w[5] = "q";
|
||||
console.log("keys", Object.keys(w).join(), Object.getOwnPropertyNames(w).join());
|
||||
console.log("in", "0" in w, 2 in w, "length" in w, w.hasOwnProperty("length"));
|
||||
const d = Object.getOwnPropertyDescriptor(w, "0");
|
||||
console.log("descriptor", d.value, d.writable, d.enumerable, d.configurable);
|
||||
const keyed = {};
|
||||
keyed[new String("k")] = 1;
|
||||
keyed[new Number(2)] = 2;
|
||||
console.log("property keys", Object.keys(keyed).join());
|
||||
|
||||
// Overridden coercion hooks are honored.
|
||||
const hooked = new Number(4);
|
||||
hooked.valueOf = function () {
|
||||
return 10;
|
||||
};
|
||||
console.log("hooks", hooked + 1, `${hooked}`, String(hooked));
|
||||
|
||||
// ToObject.
|
||||
const o = Object("x");
|
||||
const p = Object(5);
|
||||
const q = Object(true);
|
||||
const r = {};
|
||||
console.log("Object()", typeof o, o.length, o == "x", o instanceof String, typeof p, p + 1, q.valueOf());
|
||||
console.log("Object() of objects", Object(r) === r, typeof Object(null), typeof Object(undefined));
|
||||
|
||||
// Brand checks on the prototype methods, read through a wrapper.
|
||||
const numberValueOf = new Number(0).valueOf;
|
||||
const stringToString = new String("").toString;
|
||||
console.log("method values", numberValueOf.call(n), stringToString.call(s), numberValueOf === n.valueOf);
|
||||
try {
|
||||
numberValueOf.call(s);
|
||||
} catch (error) {
|
||||
console.log("brand", error.constructor.name, error.message);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 7725, "san": 117318 }
|
||||
@@ -98,7 +98,16 @@ const KNOWN: Record<string, Known> = {
|
||||
"node24-style-text": { run: "SC2020" },
|
||||
"node24-system-errors": { run: "SC1090" },
|
||||
"node24-text-encoding": { run: "SC2020" },
|
||||
"node24-text-utilities": { run: "SC2020" },
|
||||
// The fixture builds a String and a Number wrapper, which used to refuse
|
||||
// with SC2020 and now compile: the case runs, but Node's
|
||||
// ERR_INVALID_ARG_TYPE names a wrapper's class (`an instance of String`)
|
||||
// where the error formatter prints `Object`, and the program's own
|
||||
// `String(functionValue)` prints `function () { [native code] }` for a
|
||||
// named function where Node prints its source.
|
||||
"node24-text-utilities": {
|
||||
divergence:
|
||||
"wrapper objects reach the text utilities now, but ERR_INVALID_ARG_TYPE names them `Object` instead of `String`/`Number`, and function values print without their source",
|
||||
},
|
||||
"node24-timers-unref": { run: "SC2020" },
|
||||
"node24-url-factories": { run: "SC2020" },
|
||||
"node24-url-mutation": { run: "SC1090" },
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// JSON, util.inspect, structuredClone and deep equality cannot read a
|
||||
// wrapper's [[PrimitiveValue]] slot yet, so a wrapper anywhere in their
|
||||
// argument refuses with a catchable error instead of a silently wrong
|
||||
// answer. Values without a wrapper keep working in the same program.
|
||||
test("graph-walking consumers refuse primitive wrapper objects", async () => {
|
||||
const outDir = mkdtempSync(join(tmpdir(), "scriptc-wrapper-refusals-"));
|
||||
const entry = join(outDir, "main.mjs");
|
||||
writeFileSync(
|
||||
entry,
|
||||
`
|
||||
function attempt(label, run) {
|
||||
try { run(); process.stdout.write(label + " ok\\n"); } catch (error) { process.stdout.write(label + " " + error.message + "\\n"); }
|
||||
}
|
||||
attempt("json", () => JSON.stringify({ v: new Number(3) }));
|
||||
attempt("log", () => console.log([new String("a")]));
|
||||
attempt("clone", () => structuredClone([new Boolean(true)]));
|
||||
attempt("plain json", () => JSON.stringify({ v: 3 }));
|
||||
attempt("plain clone", () => structuredClone([true]));
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, {
|
||||
backend: "llvm",
|
||||
sanitize: process.env["SCRIPTC_SAN"] === "1",
|
||||
outDir,
|
||||
outPath: join(outDir, "program"),
|
||||
});
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const { stdout } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
|
||||
const refused = (consumer: string): string =>
|
||||
`${consumer} of a primitive wrapper object (new String/Number/Boolean or Object(primitive)) is not supported yet`;
|
||||
expect(stdout).toBe(
|
||||
[
|
||||
`json ${refused("JSON.stringify")}`,
|
||||
`log ${refused("console.log/util.inspect")}`,
|
||||
`clone ${refused("structuredClone")}`,
|
||||
"plain json ok",
|
||||
"plain clone ok",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user