Files
scriptc/packages/compiler/src/frontend/lowering/builtins/reflection.ts
T
Chris Tate e36a442468 Throw for primitive Reflect.has targets, record isRaw, and match StringDecoder chunks
Reflect.has over a target held as any, unknown or a union answered a
presence result for a primitive. It now checks the target at run time
after both arguments are evaluated and throws Node's TypeError
("Reflect.has called on non-object"). The stdlib-fence snapshot no
longer lists Reflect.has, which lowers.

process.stdin.isRaw read a flag that only the dynamic setRawMode path
set, so a direct process.stdin.setRawMode(true) on a TTY left it false.
The flag now lives with scr_process_stdin_set_raw_mode, which every
path calls, readline's raw mode included.

StringDecoder.end(chunk) with a dynamic chunk that is undefined threw
ERR_INVALID_ARG_TYPE. It now only flushes, as Node's end skips the
write for undefined. A dynamic ArrayBuffer or SharedArrayBuffer chunk
decoded its contents; write and end now throw Node's
ERR_INVALID_ARG_TYPE for it, as it is not an ArrayBufferView.
2026-10-29 23:59:44 -05:00

266 lines
9.9 KiB
TypeScript

import { varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { builtinModuleFnOf } from "../surfaces.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import {
BOOL,
DYN,
type IrExpr,
STRING,
type SrcLoc,
VOID,
arrayOf,
isUnitType,
} from "../../../ir/ir.js";
/** The function value of the Reflect member `member` (ownKeys, on native
* computed-key reflection), or null for members without one. */
export function reflectMemberValue(lowerer: Lowerer, member: string, loc: SrcLoc): IrExpr | null {
if (member !== "ownKeys") return null;
const value = lowerer.lowerNativeCallableValue(
{
fn: "dyn.ownKeys",
params: [DYN],
result: DYN,
valueParams: [{ mode: "required", type: DYN }],
},
"Reflect.ownKeys",
loc,
)!;
return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc };
}
/** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target,
* thisArg, argsList) where the TARGET is a builtin
* rest-parameter table fn (path.join / path.resolve — test/common
* fixtures.js's fixturesPath forwards its rest args exactly this way):
* the packed libCall over a validated string[] extraction of argsList.
* The builtins ignore the receiver, so thisArg must be an effect-free
* spelling (`this`, an identifier, a unit literal) whose dropped
* evaluation is unobservable; every other Reflect.apply keeps the
* fence. Null when this isn't a supported Reflect call. */
export function lowerReflectCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (
!call.questionDotToken &&
!access.questionDotToken &&
lowerer.stdlibGlobalMember(access, "Reflect") === "ownKeys" &&
call.arguments.nodes.length === 1 &&
!ts.isSpreadElement(call.arguments.nodes[0]!)
) {
return {
kind: "libCall",
fn: "dyn.ownKeys",
args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)],
type: DYN,
loc: locOf(call),
};
}
if (call.questionDotToken) return null;
const reflectMember = lowerer.stdlibGlobalMember(access, "Reflect");
// Reflect.has(target, key) — the `in` operator's presence query over
// the target's checked-dynamic view (own and inherited names, proxy
// `has` traps included). A primitive target is Node's TypeError
// ("Reflect.has called on non-object"): a statically primitive target
// is refused, and a target held as any, unknown or a union is checked
// at run time (dyn.reflectHasTarget).
if (
reflectMember === "has" &&
call.arguments.nodes.length === 2 &&
!call.arguments.nodes.some(ts.isSpreadElement)
) {
const loc = locOf(call);
const target = lowerer.lowerExpr(call.arguments.nodes[0]!);
const primitive =
isUnitType(target.type) ||
["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind);
if (primitive)
lowerer.noLowering(
"Reflect.has on a primitive target",
call.arguments.nodes[0]!,
"Node throws a TypeError for a primitive target; narrow to an object first",
);
if (!lowerer.dynConvertible(target.type))
lowerer.noLowering("Reflect.has on this target", call.arguments.nodes[0]!);
const key = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN);
if (target.type.kind !== "dyn" && target.type.kind !== "union") {
return {
kind: "libCall",
fn: "dyn.hasKeyComputed",
args: [lowerer.coerceToExpected(target, DYN), key],
type: BOOL,
loc,
};
}
// A target held as any, unknown or a union may be a primitive at run
// time: both arguments are evaluated, then the target is checked.
const targetLocal = lowerer.declareHiddenLocal("%reflectHasTarget", DYN);
const keyLocal = lowerer.declareHiddenLocal("%reflectHasKey", DYN);
const targetRef = varRef(targetLocal.id, DYN, loc);
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: targetLocal.id,
init: lowerer.coerceToExpected(target, DYN),
loc,
},
{ kind: "varDecl", localId: keyLocal.id, init: key, loc },
{
kind: "exprStmt",
expr: { kind: "libCall", fn: "dyn.reflectHasTarget", args: [targetRef], type: VOID, loc },
loc,
},
],
result: {
kind: "libCall",
fn: "dyn.hasKeyComputed",
args: [targetRef, varRef(keyLocal.id, DYN, loc)],
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
// Reflect.getOwnPropertyDescriptor and Reflect.defineProperty over
// checked objects (execa's Promise mixin): the Object operations, with
// defineProperty answering true. A non-object target throws the Object
// operation's TypeError.
if (
(reflectMember === "getOwnPropertyDescriptor" && call.arguments.nodes.length === 2) ||
(reflectMember === "defineProperty" && call.arguments.nodes.length === 3)
) {
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const loc = locOf(call);
const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
if (reflectMember === "getOwnPropertyDescriptor")
return { kind: "libCall", fn: "dyn.getOwnPropertyDescriptor", args, type: DYN, loc };
return {
kind: "seqExpr",
stmts: [
{
kind: "exprStmt",
expr: { kind: "libCall", fn: "dyn.defineProperty", args, type: DYN, loc },
loc,
},
],
result: { kind: "boolLit", value: true, type: BOOL, loc },
type: BOOL,
loc,
};
}
// Reflect.deleteProperty over a value with a live property table: a
// checked-dynamic value, or a JavaScript class whose statics are runtime
// properties of its constructor
// (`Reflect.deleteProperty(Response, "getResponseState")`).
if (
reflectMember === "deleteProperty" &&
call.arguments.nodes.length === 2 &&
!call.arguments.nodes.some(ts.isSpreadElement)
) {
const loc = locOf(call);
let target = lowerer.lowerExpr(call.arguments.nodes[0]!);
const info =
target.type.kind === "classval" ? lowerer.classes.get(target.type.className) : undefined;
// A primitive target throws the TypeError, so it boxes as is.
const primitive =
isUnitType(target.type) ||
["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind);
if (
primitive ||
(info !== undefined && hasRuntimeStatics(info) && lowerer.dynConvertible(target.type))
)
target = lowerer.coerceToExpected(target, DYN);
if (target.type.kind !== "dyn") return null;
return {
kind: "libCall",
fn: "dyn.reflectDeleteProperty",
args: [target, lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN)],
type: BOOL,
loc,
};
}
if (reflectMember === "get" || reflectMember === "set") {
const required = reflectMember === "get" ? 2 : 3;
if (
call.arguments.nodes.length < required ||
call.arguments.nodes.length > required + 1 ||
call.arguments.nodes.some(ts.isSpreadElement)
) {
lowerer.noLowering(
`Reflect.${reflectMember} with this argument shape`,
call,
`use Reflect.${reflectMember}(target, key${reflectMember === "set" ? ", value" : ""}, receiver?)`,
);
}
const loc = locOf(call);
const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
const target =
call.arguments.nodes.length === required
? lowerer.declareHiddenLocal("%reflectTarget", DYN)
: null;
const operation: IrExpr = {
kind: "libCall",
fn: reflectMember === "get" ? "dyn.reflectGet" : "dyn.reflectSet",
args: target
? [varRef(target.id, DYN, loc), ...args.slice(1), varRef(target.id, DYN, loc)]
: args,
type: reflectMember === "get" ? DYN : BOOL,
loc,
};
return target
? {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: target.id, init: args[0]!, loc }],
result: operation,
type: operation.type,
loc,
}
: operation;
}
if (lowerer.stdlibGlobalMember(access, "Reflect") !== "apply") return null;
const loc = locOf(call);
const fenceHint =
"the lowered form is Reflect.apply(path.join | path.resolve, <effect-free this>, args) — call other functions directly (f(...args))";
if (call.arguments.nodes.length !== 3 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering("Reflect.apply with this argument shape", call, fenceHint);
}
const targetNode = call.arguments.nodes[0]!;
const bi = ts.isPropertyAccessExpression(targetNode) ? lowerer.builtinMemberOf(targetNode) : null;
const fn = bi ? builtinModuleFnOf(lowerer, bi.module, bi.member) : null;
if (!fn || fn.variadicPack !== true) {
const target = lowerer.lowerExprExpecting(targetNode, DYN);
const receiver = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN);
const argumentsList = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN);
return {
kind: "libCall",
fn: "dyn.reflectApply",
args: [target, receiver, argumentsList],
type: DYN,
loc,
};
}
const thisNode = call.arguments.nodes[1]!;
const effectFree =
thisNode.kind === ts.SyntaxKind.ThisKeyword ||
ts.isIdentifier(thisNode) ||
thisNode.kind === ts.SyntaxKind.NullKeyword ||
(ts.isIdentifier(thisNode) && thisNode.text === "undefined");
if (!effectFree) {
lowerer.noLowering(
"Reflect.apply with a computed thisArg",
thisNode,
"the target ignores its receiver, so only effect-free spellings drop honestly (`this`, a binding, null, undefined)",
);
}
const packed = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, arrayOf(STRING));
return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc };
}