-
-
Notifications
You must be signed in to change notification settings - Fork 163
fix(hir): a capturing class constructed with omitted args gets undefined, not its captures, in those params (#11229) #11235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b091353
e306ddc
e3a9c2c
9f869bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| - **A capturing class constructed with fewer arguments than its constructor declares now sees `undefined`, and so its defaults, in the omitted parameters (#11229).** A class that reads an enclosing binding receives the captured values through synthesized trailing `__perry_cap_*` constructor params. That covers every class in a CommonJS module that reads a module-scope binding. The `new` site appends the captured values after the user arguments and records their count in `Expr::New::cap_args_appended`. The monomorph default-fill pass (`monomorph/defaults.rs`) then padded the call out to the constructor's full param count, which included the cap params, by appending `undefined` **after** those captures. So the captures shifted into the omitted user parameters, and the padding landed in the capture slots. mongodb 7.5.0's `new OnDemandDocument(this.bson, offset)` bound the module-scope `BSONElementOffset` object to `isArray`, and every cursor operation (`findOne`, `find().toArray()`, `countDocuments`, …) threw `TypeError: Cannot convert undefined or null to object`. The fill boundary now excludes the cap params, and the padding is spliced in between the user arguments and the appended captures. A constructor that reads `arguments` still skips padding (#10484), so `arguments.length` stays exact. | ||
| - `Reflect.construct(C, args)` now accepts a per-evaluation class object, meaning any capturing class or class returned from a factory. `is_constructor_function` only recognized closures and ClassRefs, so it threw `[object Function] is not a constructor` while `new C(...args)` worked. With a distinct `newTarget`, the result now takes `newTarget.prototype` (`construct_class_object_with_new_target`, the same approach as the Date arm). Before, it fell through to the plain-function tail. | ||
| - Files: `crates/perry-hir/src/monomorph/defaults.rs`, `crates/perry-runtime/src/proxy/apply_construct.rs`, `crates/perry-runtime/src/object/class_registry/construct.rs` + `construct/class_object.rs`. Unit test `monomorph::tests::fill_defaults_pads_before_appended_class_captures`. Gap test `test-files/test_gap_11229_capture_ctor_omitted_args.ts` (+ `fixtures/issue_11229_capture_ctor_arity/`) covers the mongodb shape, `new` from inside and outside the class, `new this.constructor`, defaults reading earlier params, rest params, spread arguments, explicit and implicit `super()` chains, `arguments.length`, `Reflect.construct` with and without `newTarget`, and a TypeScript factory class. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,3 +65,35 @@ unsafe fn construct_object_with_new_target(new_target: f64) -> f64 { | |
| } | ||
| instance.with_mut_ptr::<ObjectHeader, _>(|i| crate::value::js_nanbox_pointer(i as i64)) | ||
| } | ||
|
|
||
| /// #11229: `Reflect.construct(C, args, newTarget)` where `C` is a | ||
| /// per-evaluation class object (a capturing class) and `newTarget` is a | ||
| /// different constructor. Construct `C` the normal way -- that replays its | ||
| /// constructor with its own captures -- then honor | ||
| /// `GetPrototypeFromConstructor(newTarget)`, as the Date arm does. Falling | ||
| /// through to the generic tail instead ran the class as a plain function | ||
| /// against a bare object, so the result was not `instanceof newTarget`. | ||
| unsafe fn construct_class_object_with_new_target( | ||
| func_value: f64, | ||
| args_ptr: *const f64, | ||
| args_len: usize, | ||
| new_target: f64, | ||
| ) -> f64 { | ||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||
| let nt = scope.root_nanbox_f64(new_target); | ||
| let func = scope.root_nanbox_f64(func_value); | ||
| let proto = new_target_custom_object_prototype(nt.get_nanbox_f64()) | ||
| .map(|bits| scope.root_heap_word_u64(bits)); | ||
| let result = js_new_function_construct(func.get_nanbox_f64(), args_ptr, args_len); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Construct the class with the supplied
🤖 Prompt for AI Agents |
||
| if let Some(proto) = proto { | ||
| let jv = crate::value::JSValue::from_bits(result.to_bits()); | ||
| if jv.is_pointer() { | ||
| let addr = (jv.bits() & crate::value::POINTER_MASK) as usize; | ||
| super::super::prototype_chain::object_set_static_prototype( | ||
| addr, | ||
| proto.get_heap_word_u64(), | ||
| ); | ||
| } | ||
| } | ||
| result | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| "use strict"; | ||
| // Mirrors mongodb 7.5.0's lib/cmap/wire_protocol/on_demand/document.js: the | ||
| // class reads module-scope bindings, so it is a capturing class, and it | ||
| // constructs itself with FEWER arguments than its constructor declares. | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.OnDemandDocument = void 0; | ||
| const BSONElementOffset = { type: 0, nameOffset: 1, nameLength: 2, offset: 3, length: 4 }; | ||
| function parseToElementsToArray(bson, offset) { return [bson.length, offset]; } | ||
| class OnDemandDocument { | ||
| constructor(bson, offset = 0, isArray = false, elements) { | ||
| this.cache = Object.create(null); | ||
| this.bson = bson; | ||
| this.offset = offset; | ||
| this.isArray = isArray; | ||
| this.elements = elements ?? parseToElementsToArray(this.bson, offset); | ||
| } | ||
| child(offset) { return new OnDemandDocument(this.bson, offset + BSONElementOffset.offset); } | ||
| childArray(offset) { return new OnDemandDocument(this.bson, offset, true); } | ||
| } | ||
| exports.OnDemandDocument = OnDemandDocument; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| "use strict"; | ||
| const K = { tag: "K" }; | ||
| function helper(x) { return "h" + x; } | ||
| function norm(v) { | ||
| if (v === undefined) return "<undef>"; | ||
| if (Array.isArray(v)) return v.map(norm); | ||
| if (v !== null && typeof v === "object") { const out = {}; for (const k of Object.keys(v)) out[k] = norm(v[k]); return out; } | ||
| return v; | ||
| } | ||
| function show(o) { return JSON.stringify(norm(o)); } | ||
| class Doc { | ||
| constructor(bson, offset = 0, isArray = false, elements) { | ||
| this.n = arguments.length; | ||
| this.bson = bson; this.offset = offset; this.isArray = isArray; | ||
| this.elements = elements ?? [helper(bson), K.tag]; | ||
| } | ||
| child(o) { return new Doc(this.bson, o); } | ||
| childArr(o) { return new Doc(this.bson, o, true); } | ||
| viaThisCtor(o) { return new this.constructor(this.bson, o); } | ||
| } | ||
| class Dep { | ||
| constructor(a, b = a + 1, c = b * 2) { this.v = [a, b, c, arguments.length, K.tag]; } | ||
| static make1(a) { return new Dep(a); } | ||
| static make2(a, b) { return new Dep(a, b); } | ||
| } | ||
| class Rest { | ||
| constructor(first, ...more) { this.v = [first, more, arguments.length, helper(1)]; } | ||
| static none() { return new Rest(); } | ||
| static one() { return new Rest(1); } | ||
| static three() { return new Rest(1, 2, 3); } | ||
| static spread(xs) { return new Rest(...xs); } | ||
| } | ||
| class Spread { | ||
| constructor(a, b = "B", c) { this.v = [a, b, c, arguments.length, K.tag]; } | ||
| static s(xs) { return new Spread(...xs); } | ||
| } | ||
| class Base2 { | ||
| constructor(a, b = "b-default", c) { this.base = [a, b, c, arguments.length, K.tag]; } | ||
| } | ||
| class Mid extends Base2 { | ||
| constructor(a) { super(a); this.mid = helper(a); } | ||
| } | ||
| class Leaf extends Mid {} | ||
| class Implicit extends Base2 {} | ||
| class Many { | ||
| constructor(a, b, c, d, e) { this.v = [a, b, c, d, e, arguments.length, K.tag, helper(0)]; } | ||
| static zero() { return new Many(); } | ||
| } | ||
| // The same shapes WITHOUT `arguments` in the constructor: those are the ones | ||
| // the call-site padding pass rewrites (a ctor reading `arguments` is skipped). | ||
| class DepNA { | ||
| constructor(a, b = a + 1, c = b * 2) { this.v = [a, b, c, K.tag]; } | ||
| static make1(a) { return new DepNA(a); } | ||
| static make0() { return new DepNA(); } | ||
| } | ||
| class BaseNA { | ||
| constructor(a, b = "b-default", c = helper(a)) { this.base = [a, b, c, K.tag]; } | ||
| } | ||
| class MidNA extends BaseNA { | ||
| constructor(a) { super(a); this.mid = helper(a); } | ||
| static again(a) { return new MidNA(a); } | ||
| } | ||
| class LeafNA extends MidNA {} | ||
| class RestNA { | ||
| constructor(first, second = "S", ...more) { this.v = [first, second, more, helper(2)]; } | ||
| static one() { return new RestNA(1); } | ||
| } | ||
| module.exports = { Doc, Dep, Rest, Spread, Base2, Mid, Leaf, Implicit, Many, DepNA, BaseNA, MidNA, LeafNA, RestNA, show, K }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 41391
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 42351
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 42083
Track synthesized capture parameters separately from user parameters.
Lowering preserves a user parameter named
__perry_cap_user, while synthesized capture parameters use the same prefix. The filter removes both.With user parameters
__perry_cap_user, value = 0and one appended capture,new C()can insert padding before the capture as if onlyvaluewere present. The capture can bind tovalue, and the synthesized capture parameter can remain unfilled.Use explicit capture metadata instead of filtering every name that starts with
CAP_FIELD_PREFIX.🤖 Prompt for AI Agents