fix(runtime): narrow the collection-iterator latch to Map/Set receivers - #8998
Conversation
PerryTS#8991 added a plain-collection GetIterator lane behind a latch that flipped on ANY `@@iterator` symbol write anywhere in the process. That made the lane dead code: a class which merely DEFINES `[Symbol.iterator]` as a method — an ordinary iterable class, and most real programs have one — tripped the latch at class-definition time and disabled the lane process-wide. Measured on the ECS archetype-migration row, PerryTS#8991 as merged is +0.07% (5/9): the lane never runs. Flip only when the write's RECEIVER is a registered Map or Set. That is sound because perry models no `Map.prototype` OBJECT: `symbol::get` emulates `Map.prototype[Symbol.iterator]` by binding `entries`, and Set's by binding `values`, inside the resolver itself (PerryTS#2856). There is nothing to patch on a prototype, so an own symbol property on the instance is the only channel that can override a plain collection's default iteration — and that is exactly what this hook observes. A Map/Set subclass instance is an ordinary object rather than a registered collection, so the lane already declines it and the subclass arm decides. The added test is the regression itself: defining `@@iterator` on a non-collection must leave the lane armed, and a plain Map must still be claimed afterwards. Claude-Session: https://claude.ai/code/session_01FUvFrRNZyc5qknBiJbYbby
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe runtime now scopes ChangesIterator protocol latch scope
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🟡 Moderate · up to The change keeps ordinary iterable objects from disabling the optimized Map/Set iteration path, but a descriptor-based Symbol.iterator override may still leave that path enabled and cause incorrect iteration results for affected collections. This bounded correctness risk should be fixed or explicitly accepted before merging. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description explains the problem, the receiver-narrowing fix, why the fix is safe, the related issue, and the test results. It does not use every template heading or checklist item, but it provides the required substantive information.
✨ Finishing Touches🧪 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 |
|
Local merge gate on the Linux build host, head
The parallel run showed one failure, Measurement on the quiet host is queued behind the other candidates and follows here. |
|
Merged. Measuring your own change after it merged and reporting it as a null is the useful half of this — a lane that never runs is worse than no lane, because it looks like coverage. Worth noting I merged #8991 without measuring it and said as much in that comment; this is the correction I could not have made myself. On the narrowing, which is the part that could go wrong. Keying the latch to the receiver means an override the latch no longer sees would leave the lane serving a stale builtin iterator. The obvious candidate is a patched
So an own write on the instance is the only override path, and that still latches. The soundness rests on that modelling choice, so it is worth the comment being right there — if perry ever grows a real The test pair is the right one: an own One fix pushed: this PR touches two Validation — runtime 2787/0 including the 6 |
|
Measured on the quiet Apple silicon host — 9 alternating pairs, control = merge base Row
This confirms the diagnosis: #8991's lane was dead code, and it is worth +25.85% once it actually runs. The row across all three changes
80.22 → 50.87 ms, −36.6% on what the cluster measurement showed was perry's worst ECS row by a factor of five. A note on the microbenchmark
So #8991 should stay, and this is the change that makes it earn its place. |
Follow-up to #8991, which I measured after it merged: as shipped it is +0.07%, 5/9 on the ECS archetype-migration row — a null, because the lane never runs.
Why it never runs
The
ITERATOR_PROTOCOL_TOUCHEDlatch flips on any@@iteratorsymbol write anywhere in the process. A class that merely defines[Symbol.iterator]as a method trips it — an ordinary iterable class, and the ECS corpus has two (utils/multi-map.ts,utils/bit-set.ts). So the latch fires at class-definition time and disables the lane process-wide, in essentially any real program.I described the latch as "deliberately coarse … it can only cost speed, never correctness" in #8991. That was accurate, and it cost all of the speed.
The fix, and why it is sound
Flip only when the write's receiver is a registered
MaporSet.Receiver-narrowing is safe here for a specific reason: perry models no
Map.prototypeobject at all.symbol::getemulatesMap.prototype[Symbol.iterator]by bindingentries, andSet's by bindingvalues, inside the resolver itself (#2856). There is therefore nothing to patch on a prototype, and the only channel that can override a plain collection's default iteration is an own symbol property on that instance — which is exactly what this hook observes.A Map/Set subclass instance is an ordinary object rather than a registered collection, so the lane already declines it and the subclass arm decides; that is unchanged.
Tests
The added assertion is the regression itself: defining
@@iteratoron a non-collection must leave the lane armed, and a plainMapmust still be claimed afterwards. The existing assertions still pin that an own@@iteratorwrite on a Map does disarm it, and that@@asyncIteratordoes not.Local gate on the Linux build host: perry-runtime serial 2766 passed / 0 failed;
iterator32/32,symbol49/49, lane tests 2/2.Paired measurement on the quiet host follows in a comment. If the narrowed lane still does not move the row, the right outcome is to revert #8991 rather than keep inert machinery on a correctness-sensitive path — I will open that revert myself if so.
Claude-Session: https://claude.ai/code/session_01FUvFrRNZyc5qknBiJbYbby
Summary by CodeRabbit
[Symbol.iterator]changes on unrelated classes no longer affect Map and Set iteration.