From 4f7c48b4e311356c6b51181dfd8a09b84a054767 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 28 Oct 2026 14:20:44 -0700 Subject: [PATCH] reflection: Reflect.has, and Object reflection over records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../frontend/lowering/builtins/reflection.ts | 34 +++++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 4 +++ .../8426-reflect-object-reflections.ts.json | 6 ++++ .../corpus/8426-reflect-object-reflections.ts | 28 +++++++++++++++ .../8426-reflect-object-reflections.ts.json | 1 + 5 files changed, 73 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json create mode 100644 tests/corpus/8426-reflect-object-reflections.ts create mode 100644 tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index ef55fa7e5..2988f9feb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 955b57610..fa7afada4 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json new file mode 100644 index 000000000..4081fc5ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8426-reflect-object-reflections.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8426-reflect-object-reflections.ts b/tests/corpus/8426-reflect-object-reflections.ts new file mode 100644 index 000000000..bae308d81 --- /dev/null +++ b/tests/corpus/8426-reflect-object-reflections.ts @@ -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); diff --git a/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json b/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json new file mode 100644 index 000000000..a1a93d6be --- /dev/null +++ b/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json @@ -0,0 +1 @@ +{ "plain": 3121, "san": 5843 }