reflection: Reflect.has, and Object reflection over records

Reflect.has had no lowering. It is [[HasProperty]] — own and inherited
names (Array.prototype's, Object.prototype's, a class's compiled methods),
proxy `has` traps, symbol and property-key conversion — which is exactly
the `in` operator's checked-dynamic probe, so it lowers to dyn.hasKeyComputed
over the target's dyn view (and the target is fenced when it has no such
view). A primitive target is Node's TypeError, which the probe answers
false for instead: fenced by name, never silently divergent.

Object.getOwnPropertyNames/Symbols/Descriptors only accepted values that
already were checked-dynamic; a record, array or class instance now boxes
into its own dyn view (own fields and elements, in property order) like
the function and error targets already did.

Corpus 8426 pins both against Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-29 05:38:28 -07:00
co-authored by Claude Opus 5.5
parent 1a3caa88eb
commit 4f7c48b4e3
5 changed files with 73 additions and 0 deletions
@@ -62,6 +62,40 @@ export function lowerReflectCall(
}
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"), which the presence query
// answers false for instead: fenced, never silently divergent.
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]!);
return {
kind: "libCall",
fn: "dyn.hasKeyComputed",
args: [
lowerer.coerceToExpected(target, DYN),
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN),
],
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
@@ -7885,6 +7885,10 @@ function lowerObjectStaticCall(
isUnitType(object.type)
)
object = { kind: "dynFrom", value: object, type: DYN, loc: locOf(call) };
// A record, array or class instance reflects through its own
// checked-dynamic view (own fields and elements, in property order).
if (object.type.kind !== "dyn" && lowerer.dynConvertible(object.type))
object = lowerer.coerceToExpected(object, DYN);
if (object.type.kind === "dyn")
return {
kind: "libCall",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8426-reflect-object-reflections.ts"
],
"diags": []
}
@@ -0,0 +1,28 @@
// Reflect.has over the checked-dynamic view, and the Object reflection
// operations over records, arrays and class instances (own fields and
// elements in property order). Reflect.has is [[HasProperty]], so inherited
// names (Array.prototype's, Object.prototype's, a class's methods) answer
// true; a primitive target is Node's TypeError and stays a lowerer fence
// (SC2020) rather than a silent `false`.
class Point {
x = 1;
y = 2;
sum(): number {
return this.x + this.y;
}
}
const record = { a: 1, b: "two" };
const list = [10, 20];
const point = new Point();
console.log(Reflect.has(record, "a"), Reflect.has(record, "z"));
console.log(Reflect.has(list, "0"), Reflect.has(list, "length"), Reflect.has(list, "9"));
console.log(Reflect.has(point, "x"), Reflect.has(point, "sum"));
const key = "b";
console.log(Reflect.has(record, key), Reflect.has(record, key.toUpperCase()));
console.log(Reflect.has(record, "toString"), Reflect.has(record, "constructor"));
console.log(Object.getOwnPropertyNames(record).join(","));
console.log(Object.getOwnPropertyNames(point).join(","));
console.log(Object.getOwnPropertyNames(list).length);
const descriptors = Object.getOwnPropertyDescriptors(record);
console.log(Object.keys(descriptors).join(","));
console.log(Object.getOwnPropertySymbols(record).length);
@@ -0,0 +1 @@
{ "plain": 3121, "san": 5843 }