Merge train 217: implicit-this exception safety, template/map/regex/GC perf, defineProperty attrs, instanceof and Symbol accessor fixes (v0.5.1595) - #10631
Merged
Conversation
Several runtime guards displace IMPLICIT_THIS/new.target around a call they don't own (the Temporal/Intl subclass super() bridges, the handle-method prototype-walk accessor dispatch, the stdlib listener/ getter dispatchers) with a bare save/call/restore statement pair, not an RAII guard. When the bracketed call throws, the restore statement textually follows it, so neither longjmp nor a system unwind ever reaches it -- both cells stay pinned at whatever the failed call set them to for every later read. Add implicit_this and new_target as two more members of the catch_savepoints! family exception.rs already maintains for exactly this shape (shadow stack, runtime handles, call-method depth, ...): captured at every try-push, replayed by js_throw before it transports the exception, uniformly for both the setjmp and the unwind handler kind (both already funnel into one try_push_with_kind -> CatchSavepoint::capture(), and js_throw calls .restore() unconditionally before branching on transport). The captured bits are a second root for whatever heap value they hold while a try is open, invisible to the live cell's own scanner, so scan_exception_roots_mut now also walks the live prefix of the per-thread savepoints slab and rewrites both fields across a moving collection. Proven with a unit test that reproduces the bare save/call/restore shape using only pre-existing public entry points (so the same test body fails on the unfixed tree and passes here): an inner js_throw crossing the bare site leaves IMPLICIT_THIS/new.target stuck at the inner value instead of the enclosing try's baseline. NOT verified: that any of the four named sites is reachable in the way the finding assumes -- see the PR comment.
…ests The perex GC-isolation test harness (register_host_roots) clears the production scanner registry and manually restores only what its own tests need. implicit_this/new_target in exception.rs's catch_savepoints! are a second root for whatever these tests displace IMPLICIT_THIS to across a throw (previously nothing in scan_exception_roots_mut needed scanning for these specific tests, since their thrown values are always plain numbers) -- register it alongside the live-cell scanner it already restores, or a forced-evacuation minor between the try-push and the throw leaves the savepoint copy stale and the post-catch receiver wrong. Fixes the 3 new failures the previous commit introduced under `cargo test --release -p perry-runtime --tests` (perex_dispatch, perex_replace, perex_split's "restores this" tests); full suite is back to 3975 passed, 1 pre-existing failure (a_free_or_move_outside_every_ scope_is_caught_in_debug_builds, a debug_assert! funnel that cannot fire under --release, unrelated to this change and already documented in #10564's own PR body), 4 ignored.
A template literal opening on a substitution (`${x}...`, no literal text
before the first `${`) unconditionally seeded its desugared concat chain
with a literal `Expr::String("")`: every interior quasi already skipped
itself when empty, but the leading one never got the same guard, so it
survived to `js_string_concat_chain` as an always-empty classification
slot, and a single-substitution template missed the chain fold's 3-part
minimum entirely.
Separately, a `number`-typed parameter's substitution kept its redundant
`StringCoerce` wrapper unless codegen had a dataflow *proof* it was
numeric; a plain, unspecialized function parameter only ever has the
declared annotation. `js_string_concat_chain`'s own classify loop already
tag-dispatches every part and falls back to the exact `js_jsvalue_to_string`
/ `js_string_materialize_to_heap` calls `js_string_coerce` itself forwards
to for every non-numeric shape, so trusting the declared type here (the
same trust `is_declared_string_expr` already extends to strings) cannot
change the output, only which code path produces it.
Measured on `${s}:${n}` (s a short string, n a non-integer double),
differencing two probes to cancel fixed per-process cost: 1173 -> 955
instructions per evaluation (-18.6%), loop16/loop80 control ~0 in both
arms. An integer-interpolation variant isolates the leading-quasi fix
alone at -5.4%, confirming the larger win comes from the StringCoerce
elision.
…ements Array.prototype.map's plain-result fill used the once-resolved-header fast path (fill_resolved_array_slot) only for a source of at most 64 elements; longer sources fell back to note_array_slot, which re-classifies the result's ownership/forwarding through clean_arr_ptr on every element (array_numeric_layout) and unconditionally pays layout_note_slot. The result pointer is re-derived from its own GC root immediately before either helper runs, with no intervening allocation or safepoint, so fill_resolved_array_slot's contract holds regardless of length -- the 64-element split was scope, not a correctness boundary. Also drops a redundant raw ptr::write of the mapped value that ran right before both branches; both helpers perform their own (possibly canonicalized) store of the same slot, so it was always immediately overwritten. a.map(x => x + v) over a 16-vs-80-element number[]: 450.8 -> 206.1 instructions per element (-54.3%), measured as the marginal per-call cost difference of two probes differing only in element count (control (loop80-loop16)/64 ~0 in both arms).
…lass accessors
A ClassBody get/set accessor lives in the class vtable, not the
address-keyed descriptor tables defineProperty writes. A generic
descriptor (no get/set/value/writable, e.g. { enumerable: true })
against an existing class accessor fell through to the ordinary
define path, which could not see the class key: it appended a new
data-property keys-array entry with writable: false that shadowed
the class accessor on writes, breaking the setter and leaking a
stale enumerable/configurable reading.
Add a per-(class_id, is_static, name) attrs side table
(class_registry/accessor_attrs.rs) that a generic descriptor against
a declared accessor updates instead of materializing a shadowing
data property, and route getOwnPropertyDescriptor, enumeration,
has-own and delete through it.
Fixes #10480
CLASS_ACCESSOR_ATTRS (added for #10480) stores only scalars/String, never a heap pointer -- verdict not_a_gc_pointer in scripts/gc_runtime_root_holders.json.
…rable
Object.defineProperty(C, 'x', { enumerable: true }) on a declared
static accessor updated getOwnPropertyDescriptor/Object.keys/for-in
via the #10480 side table, but js_object_property_is_enumerable's
ClassRef branch only ever checked static FIELDS, so
C.propertyIsEnumerable('x') stayed false. Check the declared static
accessor's tracked enumerable attribute alongside the static-field
check.
Found by CodeRabbit review on #10582; verified against Node before
fixing (propertyIsEnumerable: false vs Node's true, while the
descriptor/for-in/keys already agreed with Node).
#10362) `visit_gc_layout_slot_descriptors` called `HeapChildSlotIterator::next` once per payload slot. For the common case — a `Masked` selection whose mask is `LayoutSlotMask::Inline` — that call re-dispatched the selection, re-decoded the mask's niche and rebuilt the limit and cursor masks FOR EVERY SLOT, for about eight instructions of work. The mask's set bits ARE the slot indices, in order, so the arm now takes the word once (`take_inline_mask_word`) and walks it with `trailing_zeros` and `word &= word - 1`. Every other selection — including a `Heap` mask, i.e. more than 64 payload slots — keeps the iterator unchanged. `take_inline_mask_word` carries the iterator's two side conditions with it: the one-shot raw-numeric accounting `next`'s first call performs, and the cursor, which it leaves at the end so a later `next` yields nothing. The prefix and meta edges are the caller's, taken before the payload; the helper debug-asserts they are already gone, because losing one of them is the only way this can go wrong without disagreeing with `next` on any payload index. Base: 01063a8, the head of #10552 — a correctness fix to this same file, which lands first. MEASURED ON A DISTINCT-CHILD CONTROL. The `rec*_ptr` controls this campaign has been using aim every pointer field at ONE shared object, so `mark_addr`'s one-entry address memo answers 83.3% of its classifications (exact: 360,094 `classify_arena` calls under 2,160,037 `mark_addr` calls). On the real fixtures it answers 0.0%. `dist*_ptr` points field j of record n at `pool[(n*K+j) % 4096]` instead: memo hit rate 0.19%, same slot count, same object population. Per pointer-slot visit, exact self Ir under callgrind, dist16_ptr (base arm): 97.3 CopyingPointerSet::classify_arena 82.4 CopyingNurseryCollector::visit_slot_with_weak_fact 75.8 HeapChildSlotIterator::next 67.6 CopyingNurseryCollector::scan_object_fields::{closure#0} 44.5 CopyingNurseryCollector::mark_addr 31.1 visit_gc_layout_slot_descriptors 18.5 the rewrite trampoline ----- 417.1 slot path = 42.3% of the program THE RANKING MOVED: `next` is third on this control, not first. What the blind control hid is `classify_arena` (20.5 -> 97.3 Ir/slot) and `mark_addr` (28.4 -> 44.5); `next` itself is unchanged by the control, 75.9 -> 75.8. It is the largest REMOVABLE item, not the largest item, and those are different claims. The walk removes 69.7 Ir per pointer-slot visit (exact, dist16_ptr: -164,025,897 over 2,352,324 visits), of which -161,432,000 is `next` and -2,599,000 is the descriptor loop itself. WHAT IT COVERS, on gc3: 6,181,927 traced-object visits, half of which yield no payload slot at all and never entered the iterator; 12,784,094 pointer-slot visits, of which 9,693,123 (75.8%) come through the masked arm and 3,090,971 (24.2%) are the shared shape-record `keys` prefix edge, one per masked object, which this does not touch. Of the iterator calls the collector actually made, the walk removes 12,364,032 of 12,784,116 — 96.7%. On oldyoung, whose masked slots are mostly behind one `Heap` mask, it removes 66.7%. ALL THREE CONSUMERS of the descriptor walk, inclusive Ir on gc3, exact: copying minor (run_copied_minor_attempt) 4,665,326,035 -> 4,294,459,662 -7.95% full mark (drain_trace_worklist_step) 1,913,478,665 -> 1,595,408,917 -16.62% remembered rebuild (rebuild_evacuated_...) 738,658,748 -> 608,717,911 -17.59% dirty scan (scan_dirty_object_slots) 218,745,912 -> 218,703,379 -0.02% instructions:u, min of 5, whole program: gc3 11,771,100,698 -> 10,976,853,287 -6.75% w20000 4,735,487,965 -> 4,457,784,668 -5.86% w5000 1,888,564,009 -> 1,796,034,248 -4.90% w1000 1,046,189,863 -> 1,021,502,063 -2.36% oldyoung 1,455,160,271 -> 1,439,702,970 -1.06% alloc 320,187,977 -> 320,187,679 -0.00% Witness: `gc::tests::layout_inline_mask`. The equivalence is a property, so it is tested as one: the walk's index sequence must equal the iterator's for every mask word (empty, one bit at each end, full width, both alternations, 64 pseudo-random words) crossed with every live slot count (0, 1, ..., 63, 64, 65, 96, 128), and every index it yields must be live and in the mask. Two sabotaged twins are armed in-tree and must fail; two more were applied to the real source and the witnesses went red — an off-by-one in the word's limit fails the property, and a visit loop that stops one bit early fails only the collection witness, which is how we know the two cover different code.
…n move them #10532 removed the argument-count ceilings on dynamic calls, but two flows in that path held GC values in plain Rust locals across an allocation: - Reflect.apply rebinds a concise-method callee's captured `this`, which CLONES the closure -- the one shape rebind_explicit_this allocates for. The callee, receiver and every argument sat in plain locals across that clone. - CreateListFromArrayLike's array-like path allocates an index-key string per element (and can run a getter), with the source object and previously-read elements sitting in plain locals across it. - A (...fixed, ...rest) body that also synthesizes `arguments` builds TWO arrays; the second allocation could move the first, which the bundler then handed the callee by its pre-move address. Root only where a collection is actually possible: a new rebind_explicit_this_allocates predicate (kept in sync with rebind_explicit_this's clone shape by a same-file test) lets the common non-cloning Reflect.apply path skip rooting entirely, and only the cloning shape takes the rooted slow path. value_can_move() skips rooting immediate values (numbers, undefined/null, etc.) in the array-like path. A first cut of this rewrite rooted unconditionally and cost Reflect.apply +38.8% instructions; this version measures near zero (see perf table in the PR). Adds gc/collection_points.rs (named, test-only collection points so a rooting regression test can arm a copying minor at a specific allocation inside one call) and gc/tests/runtime_roots/call_argument_lists.rs, which reproduces all three flows and asserts the callee observes post-collection addresses. Recovered from a mirror after the original build host was destroyed mid-validation; re-verified (apply, build, new tests, cargo fmt, lint gates) from scratch on a fresh clone.
…lists CodeRabbit review of #10587: create_list_from_array_like's value_can_move only recognized NaN-boxed (POINTER_TAG/STRING_TAG/BIGINT_TAG) values as movable. Some closures (the Promise executor's resolve/reject, from js_promise_new_with_executor) and some TypedArray/Buffer pointers on certain platforms are handed through as a raw pointer bitcast to f64 -- no NaN-box tag, top16 == 0 -- rather than POINTER_TAG-boxed (see the existing comments in object/native_call_method/handle_methods.rs and value/dynamic_object.rs for the established precedent). Such a value read out of an array-like object's indexed property and copied bare into the local out Vec was invisible to value_can_move, so a collection triggered by a later index's key-string allocation could move it while it sat unrooted. value_move_kind now also recognizes a raw heap-pointer-shaped bit pattern via the existing addr_class::is_plausible_heap_addr predicate, and roots it through RuntimeHandleScope::root_heap_word_u64 (the raw-aware HeapWord slot -- root_nanbox_f64's Nanbox slot only rewrites tagged bit patterns and would silently do nothing for a raw one). Also extends the test-only collection_points harness with arm_collection_point_after(site, skip), so a test can put the forced collection on a LATER loop iteration than the first -- needed here because the one-shot arm firing on the very first iteration meant every element was always read fresh post-collection, never exercising the "already read, then a later allocation moves it" window the fix protects. New test: array_like_argument_lists_root_raw_untagged_heap_pointer_elements, proven to fail against the previous commit's create_list_from_array_like and pass with this fix.
A one-shot mock timer (node:test's mock.timers) left the queue via state.callbacks.remove(idx) and was destructured into (id, callback, args, context), dropping the popped entry's ScheduledTimerId pin right there -- before call_timer_callback had run, let alone finished. A callback that then churned more than TIMER_REF_STATES_CAP timers evicted its own handle mid-dispatch, exactly as #10447 evicted long- lived timers before the pin existed. Carry the ScheduledTimerId in the dispatch action tuple instead, and let it drop only after call_timer_callback returns. Interval mock timers are unaffected: their entry stays in the queue across a tick, so their pin was never at risk.
scripts/gc_runtime_root_holders.py flags any new static/thread_local whose type could hold a GC heap pointer that no registered scanner in its own file reaches. crates/perry-runtime/src/timer/tests_inline.rs's new SELF_ID: AtomicI64 (added by the mock-timer dispatch-pin regression test) stores a scheduled timer id, never a heap pointer, and only exists under #[cfg(test)]. Classify it test_only.
A call whose method NAME matched a Date/Number/Array builtin (`getTime`, `toFixed`, `toISOString`, `toSorted`, `endsWith`, ...) lowered straight to the builtin's runtime entry point regardless of the receiver, because a method name alone was treated as proof of receiver kind. Any class, function-constructor prototype, or object literal defining a same-named method (dayjs's `toISOString`, decimal.js/bignumber.js's `toFixed`, a `Clock.getTime`, ...) had its own method silently skipped in favor of the builtin, producing NaN, "[object Object]", Invalid Date or an uncaught RangeError instead of calling the user's code (#10476). A 0-arg call of a user method sharing a name with a required-arg String builtin (`endsWith`/`includes`/`startsWith`) didn't even compile. Add crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs: for a receiver the compiler has proven to be a Date/number/array, keep the direct builtin call; for any other receiver, evaluate it once (rooted), then branch at runtime on its actual kind to either the builtin or the universal method dispatcher, which resolves an own/inherited user method and still reaches the builtin via the prototype chain for a real Date/number/array. Known-class receivers are left to the existing class dispatch tower. Companion changes in lower_array_method.rs, lower_string_method.rs, number_string.rs and the HIR-side expr_call lowering route the affected method names through this guard instead of the unconditional builtin path. Adds test-files/test_gap_10476_builtin_named_guarded_receivers.ts and test_gap_10476_builtin_named_user_methods.ts (both fail to compile or mismatch Node on main; both pass byte-for-byte against Node after this fix), plus unit coverage in builtin_kind_guard_tests.rs and builtin_named_user_methods_tests.rs. Known cost: for a receiver whose kind is NOT statically provable (an `any`-typed or otherwise unproven object calling a builtin-named method), the new runtime kind check adds real overhead. On a synthetic microbench exercising 200k iterations of such calls, `dayjs_shape` and `money_shape` land well outside the usual 20%-of-Node floor (see PR body for numbers); the prior "fast" baseline numbers for those two shapes were never valid, since the baseline computed the wrong answer (money_shape) or crashed (dayjs_shape) instead of doing the work Node does.
`class Sub extends EventEmitter {}` compiled `new Sub() instanceof
EventEmitter` through js_instanceof_dynamic, which never registered or
consulted the class-chain parent edge that Array/Map/Set/Error subclassing
uses. A genuine subclass instance is a real ObjectHeader carrying Sub's own
class id, not a handle and not prototype-linked to EventEmitter.prototype,
so it was invisible to the handle/prototype probes there and always
answered false.
Register EventEmitter's reserved class id as a valid `extends` parent
(builtin_parent_reserved_class_id in instance_misc1.rs), and have the
dynamic-dispatch EventEmitter branch in instanceof.rs delegate to
js_instanceof(value, CLASS_ID_EVENT_EMITTER) first, so it picks up the
class-chain walk; the general prototype walk stays as a fallback for
util.inherits-style shapes.
Extends test_gap_10556_instanceof_native_emitter.ts to cover a direct
subclass, a two-level subclass, the default-import form, and a
util.inherits function-constructor subclass, alongside the existing direct
EventEmitter coverage.
…ation js_instanceof_dynamic's INT32-tag classification (and value_is_callable's matching guard) treated any value tagged 0x7FFE as a codegen-emitted class reference and dispatched its low 32 bits straight into js_instanceof as a class id. That tag band is also a legal IEEE-754 NaN payload a JS program can construct directly (e.g. via DataView), so a crafted number sharing the tag was misread as a class id instead of reaching the unresolved-RHS TypeError / correctly answering non-callable. Both call sites now go through class_ref_id, which additionally requires is_class_id_registered -- the same helper every other class-ref check in this crate already uses.
x instanceof F was always false when F was an imported non-class constructor function. Lowering only attached a runtime value to an identifier RHS for a local, a module function or a native module, so an imported binding reached codegen as a bare name, resolved to no class id and folded to js_instanceof(v, 0) - false for every instance. Every import form was affected (named, default, CJS module.exports/exports.F), while ns.F, a local alias and the check inside the defining module all worked. Imported bindings now lower to their value and take the prototype-chain path. Codegen keeps the static class-id check for an imported class, and for a binding that is not a compiled source-module import (its value form is a placeholder), so the class fast path and the reserved builtin ids are unchanged.
find_near and find_near_lent each built a Search and then advanced it: two view acquisitions per JS regex call, and a Search moved between them, for a search whose first quantum decides it. Search::run acquires the views once and runs that quantum in the same borrow, so a decided search never builds a Search at all. Run::Paused falls into the existing advance loop unchanged, and a pause from a Frames/Undo shortage re-raises on the next advance (perex keeps it in state.blocked) straight into the scratch-growth branch, so both paths behave exactly as Search::new followed by advance did. Instructions per call, control subtracted, one Perry commit and one perex commit differing only by this patch, release build, min of 5 interleaved rounds on a 16-core Linux host: .test() anchored 4,382.2 -> 4,063.7 -7.3% .test() inline literal 5,698.2 -> 5,379.7 -5.6% exec, two groups 7,566.9 -> 7,237.9 -4.3% .test() unanchored miss 2,438.7 -> 2,133.7 -12.5% .test() unanchored hit 5,813.1 -> 5,481.1 -5.7% 200,000-char subject 1,439,713 -> 1,439,003 -0.0% 305 to 332 instructions whatever the pattern, which is the fixed cost of the call; it vanishes against a subject long enough for matching to dominate. Every probe returns an identical answer on both arms and on Node 26.5.1. Wall-clock figures are deliberately omitted: the measuring host carried other sessions' builds throughout, and round-to-round spread reached 68-244% against effects of 0.5-25%, enough to invert the sign of a known-good result. Only instruction counts are quoted. perex 0.1.9 supplies Search::run and Run. Its src/ is byte-identical to the commit the figures above were measured against, and re-measuring the released crate reproduced every row to 0.1 instructions.
…receiver An inherited Symbol-keyed accessor (Object.defineProperty(Fn.prototype, sym, ...), an object-literal get [sym]() reached through Object.create, or a declared class prototype) ran its getter/setter with no receiver at all, so it observed whatever this happened to be ambient. fastify 5.10.0's Reply.prototype[kRouteContext] getter crashed every HTTP request with TypeError: Cannot read properties of undefined (reading 'request'). own_symbol_property now resolves to an OwnSymbolSlot (Accessor or Data) before deciding how to read it, so every prototype-chain walk (resolve_proto_chain_symbol, explicit_prototype_symbol_slot, declared_prototype_chain_symbol) can thread the read/write's actual receiver through to the accessor invocation. Reflect.get/set for a Symbol key now reach the receiver-aware entry points directly. An inherited SETTER is now consulted too - obj[sym] = v used to silently shadow it with a new own data property instead of running it - gated by a symbol-id-keyed accessor filter (symbol_may_have_accessor) so the common no-accessor write path stays cheap.
…aths Two CodeRabbit-flagged gaps in the #10481 receiver fix: 1. get.rs: the parent-closure fallback (a class extending a function value, e.g. class Svc extends Context.Tag(id)<...>() {}) called the no-receiver js_object_get_symbol_property entry point, so Reflect.get(Svc, sym, other) saw the parent closure as this instead of other. 2. properties.rs: an inherited symbol setter was never reached on a non-extensible receiver - the OBJ_FLAG_NO_EXTEND check returned early before the inherited-accessor walk ran. [[Set]] through an inherited accessor never creates a new own property, so non-extensibility must not block it; moved the inherited-setter check ahead of that gate.
…le-band predicate
…#10574) The retained-source pool already shared nested function bodies, but it disabled intern when unique-string lengths summed past 8 MiB. That is the tsc case: ~24 MB of overlapping slices of a ~6 MB module, so __cstring kept one copy per function. Over-budget modules now still share into the longest parent (the CJS factory / module wrapper). --function-source=header stores `function name(params) { /* source elided */ }` instead of the body for the remaining unique-source win; full interned source stays the default so fn.toString() is byte-identical.
…urce=header
Header mode resolved a function's name and parameters through
`function_by_id`, which searches only `hir.functions` and class members.
Arrow functions, function expressions and nested declarations lower to an
`Expr::Closure` nested in an expression tree, so every one of them missed
and fell through to `synthesize_function_header("", &[])` — emitting
`function () { /* source elided */ }` with no name and no parameters.
On typescript@5.9.3 that was ~9,600 of 9,644 functions, all interned onto
one shared anonymous string: the linked binary held 48 distinct headers,
of which 27 carried parameters. That breaks the documented contract for
this mode (Angular/Vue-style parameter-name DI, name extraction) and is
worse than Static Hermes, which at least emits `function f(a0, a1)`.
`ClosureHeaders` maps FuncId -> (params, is_arrow) from the `closures`
slice `emit_module_artifacts` already holds, so this is a map build rather
than a new traversal. Arrows now render as `(a, b) => { ... }`: calling an
arrow `function` misreports the function kind on top of eliding the body.
tsc, header mode, after: 4,007 distinct headers, 3,963 of them carrying
parameters (was 27), one empty header left as the genuine unknown-id
fallback. Binary 67,782,520 -> 68,112,752 bytes (+330 KB, +0.5%) — real
headers intern less than one shared anonymous string did. `--noEmit`
output and exit code stay byte-identical to node.
Nested function declarations keep their parameters but not yet their name,
because lowering records `closure_display_names` for function expressions
and object methods but not for nested declarations. `fn.name` is
unaffected and still matches node (`"inner"`), so `Function.prototype.name`
consumers — including tsc's own `Debug.getFunctionName`, which checks the
name property before parsing `toString()` — do not see this.
Also in this commit, from review of the same PR:
- CLI beats env for `--function-source`, matching the precedence documented
for `--cache-dir`/`PERRY_CACHE_DIR`. The flag is now `Option<String>` so
an explicit `--function-source=full` is distinguishable from an omitted
flag; previously an exported `PERRY_FUNCTION_SOURCE` silently won.
- An unrecognised `PERRY_FUNCTION_SOURCE` is rejected instead of quietly
selecting full source, so a typo no longer retains every function body
with no diagnostic.
- Reverted the version bump in Cargo.toml/CLAUDE.md per CONTRIBUTING.md
("maintainer handles these at merge").
Tests: closures_keep_their_names_and_parameters and
arrow_closures_keep_arrow_syntax both fail on the unfixed code, verified by
removing the fallback and re-running — left `function () { ... }` against
the expected `function (epsilon) { ... }` and `(g, d) => { ... }`.
…rs for the file cap
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (117)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 18, 2026
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This train lands 13 PRs as v0.5.1595. Each source commit is verified to preserve its patch-id and authorship.
thisin every guard that restores it (#10490) #10564 (#10490) — completes the displaced implicit-thiswork: the rooting half landed in train 216, and this adds the exception-safety half.Array.prototype.map's result header past 64 elements.#10574) — interns nestedFunction.toStringsource past the 8 MiB cap.#10480) — applies genericObject.definePropertyattrs to declared class accessors.#10476) — guards builtin-named methods on the receiver's own shape.#10478,#10479,#10556) — resolvesinstanceofandObject.createconstructors.#10477) — gives an imported constructor'sinstanceofits value.#10481) — runs inherited Symbol-keyed accessors with the original receiver.Three things this train decided, not just carried
#10564 completes a fix that shipped half-done. Train 216 landed its rooting half and deliberately withheld the
Fixeskeyword, because the PR gained three exception-safety commits after that train was assembled. Those commits close a hole where an innerjs_throwcrossing a bare save/call/restore site leavesIMPLICIT_THISandnew.targetstuck at the inner value instead of the enclosingtry's baseline — proven by a test built only from pre-existing public entry points, which fails on the pre-fix tree. It costs +7.83% instructions on a 5M-iteration try/catch loop that never throws (895.28M → 965.34M, ~14 pertry-push), and the author records that none of the four named bare sites is proven reachable. That trade was put to the owner with those numbers and approved.#10490closes here.#10580 adopts
perex 0.1.9from inside the supply-chain soak window. ItsSearch::runsingle-entry decision needsperex::executor::Run, which lands in 0.1.9 — a release ~11 hours old against a 7-dayglobal-min-publish-age, so the resolver skipped it andperry-runtimewould not compile. The window is not weakened:SOAK_DAYSandglobal-min-publish-ageare untouched, and the version is pinned inCargo.lockinstead, which is the escape the mechanism itself defines (too-new releases are skipped unless already locked). Verified to build with no override environment set, andnpm run soakstill reports all surfaces matchingSOAK_DAYS=7with no drift. Owner-approved and recorded in a dated fragment, because stepping outside a soak window should be visible rather than silent.Two train repairs, both for defects the gates caught rather than style. #10597's new guard used the bare
is_valid_obj_ptr, which is only a floor check (addr >= 0x1000) and, per its own documentation, deliberately does not reject the fetch/zlib/proxy handle bands; since handles are pointer-tagged with small addresses, a handle receiver would have reached the deref and segfaulted on Linux while macOS hid it. It now usesis_above_handle_band, the sanctioned predicate. And #10579 tookcodegen/artifacts.rsto 2006 lines, so the retained-source collection moved toartifact_source_text::collect_user_fn_source(1949 + 216 lines).Validation
Validated head
a8f74b0dff. Five-package release build pinned and hash-verified, and re-verified after the gap run (artifacts_match_pin_after_gap=True).lintis 82/83, the one red being the public-benchmark freshness step known-red onmain.--filternow intersects instead of last-wins). Each line records what the run actually selected against whattest_gap_*matching predicts, so a filter that silently fails to narrow is a red:unexplained_regressions={}.Issues closed by this train
A merge train closes its source PRs rather than merging them, so the
Fixes #Nkeywords in those PR bodies never evaluate. They are carried here, on the PR that actually merges, so they fire:Fixes #10490
Fixes #10574
Fixes #10480
Fixes #10476
Fixes #10478
Fixes #10479
Fixes #10556
Fixes #10477
Fixes #10481
Summary by CodeRabbit
Bug Fixes
instanceofbehavior for imported constructors, native objects, subclasses, and varied value types.Object.create()constructor resolution.mapbehavior and zero-argument string search methods.Performance
New Features
--function-sourceorPERRY_FUNCTION_SOURCE.Chores