Fix #191: reachability sweep drops return-only-generic orphan records - #195
Merged
Conversation
A function whose type parameter appears ONLY in its return (`jsonBoxed<T>(): BoxOf<T>`) has that return flagged — a return-only `'a` can't round-trip (rule #4). But `classify` had already registered the record it built for the discarded return, and nothing un-registered it, so an unreferenced `boxOf<'a>` reached the output (and shoved the real `readBox(): BoxOf<string>` type off the clean base name to `boxOfV1hnll`). The per-site snapshot/rollback used for the METHOD path (#189) doesn't work here: the orphan survives through late name/key resolution at emit (#128 + stabilizeNames run after the rollback). So instead, add the post-traversal reachability sweep #178 called for: after every IR tree is final but BEFORE stabilizeNames, walk the emitted roots (component props + baseSpreads, function/const/context signatures, class members) and drop any registered entry unreachable from them. - Key-based (every module-mode ref is keyed via refTo), so a type reached only through a keyed ref whose home is late-bound is never wrongly dropped. - A deep walk (collectAllRefKeys) covers refs inside inline-record fields and record spreads that the shallow collectRefKeys skips. - Runs before naming, so a live sibling reclaims a base name an orphan squatted. - Removing an orphan can also dissolve a spurious SCC merge, giving cleaner homes. Fixture: return-only-generic-orphan. All 116 goldens compile; benchmark baselines updated (real orphan removals in blend + react-rating, metrics equal-or-better). Fixes #191 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Benchmark: ✅ PASS
|
commit: |
…s/statics The class-member root walk seeded only ctor/methods/getters, but the class IR also emits setters (`@set external …Set`) and static members (`@scope(...) external`). A shared type reachable ONLY through a write-only setter or a static-only member had no other path to a root, so the sweep dropped it and the emitted external dangled — uncompilable output for any such class. (Getters+setters usually pair up, so the getter hid this; no bench package has a write-only or static-only case.) Add setters, staticMethods, and staticValues as roots (whole-element, since collectAllRefKeys deep-walks). Fixture: class-setter-static-reachable (a write-only setter + a static-only type), compile-gated. Also note the early-out counts `>=` because `reachable` can hold non-entry `deps` keys (safe-direction: skips the sweep, never drops a live type). All 117 goldens compile; the #191 orphan fix is unchanged.
jagguji
added a commit
that referenced
this pull request
Aug 18, 2026
…ow-up) (#196) Docs-only follow-up to #195 (the #191 reachability sweep), enriching the `sweepUnreachableEntries` section of `docs/TYPE_MAPPING.md`. ## What changed 1. **Precise ReScript v12 vocabulary for the class-member roots** — names exactly what each member emits and therefore why it must be a root: `@new` ctor, `@send` methods, `@get` getters, `@set` write-only setters, `@scope` statics (cross-checked against `emit.mjs` and the v12 interop cheatsheet). Replaces the vague "class members". 2. **The completeness invariant + second fixture link** — states why *all* binding class members must be roots: a type reached only through a `@set` setter or a `@scope` static has no other path, so dropping it dangles the external → ReScript compile error. This is the exact regression the #195 follow-up commit fixed; it's now written into the contract doc and linked to the [`class-setter-static-reachable`](../test/golden/cases/class-setter-static-reachable) fixture (previously only `return-only-generic-orphan` was linked), so a future maintainer won't trim the root walk back to getters. 3. **SCC acyclicity note** — clarifies that ReScript's cross-file module graph must be acyclic (a dependency cycle between two `*Types.res` homes is a compile error), which is why an orphan removal can dissolve a forced merge (#35). ## Notes - **Docs only** — no source/behavior change; both linked fixtures already exist on `main`. - Landed as a fresh branch off `main` because #195 is already merged (it can't be reopened to receive this). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jagguji
added a commit
that referenced
this pull request
Aug 18, 2026
…ivor scan The post-sweep usesJsFn recompute scanned roots + entries.flatMap(entryChildTypes), but entryChildTypes's record branch omits e.indexValue — the `@set_index` value type. A SURVIVING record whose only JsFn.t is a Function-typed index signature (`[k: string]: Function` -> `@set_index …Set: (rec, string, JsFn.t)`) was therefore not scanned, so usesJsFn was wrongly cleared and JsFn.res dropped, leaving the @set_index external dangling — uncompilable. (Same class as the #195 setter/static gap: the survivor surface was narrower than what the emitter produces.) Surgical fix: add each entry's indexValue to the survivor set (kept out of entryChildTypes, which feeds deps/home/SCC and would move the bench). Fixture jsfn-function-index-reachable guards it — JsFn.res stays in the emitted file set and the output compiles. All 119 goldens compile; benchmark unchanged.
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.
Summary
A function whose type parameter appears only in its return (
jsonBoxed<T>(): BoxOf<T>) has that return flagged — a return-only'acan't round-trip (contract rule #4). Butclassifyhad already registered the record it built for the discarded return, and nothing un-registered it, so an unreferencedboxOf<'a>reached the output — and shoved the realreadBox(): BoxOf<string>type off the clean base name toboxOfV1hnll.The per-site snapshot/rollback used for the method path (#189) does not work here: the orphan survives through late name/key resolution at emit (#128 +
stabilizeNamesrun after the rollback). So this adds the post-traversal reachability sweep that #178 explicitly called for.What it does
sweepUnreachableEntries— after every IR tree is final but beforestabilizeNames:baseSpreads, function/const/context signatures, class members) and drops any registered entry unreachable from them;refTo), so a type reached only through a keyed ref whose home is late-bound (Callable-module home placement ignores prop-derived deps (deferred from #103 review) #128) is never wrongly dropped;collectAllRefKeys) so refs inside inline-record fields and record spreads — which the shallowcollectRefKeysskips — are covered;Before / after
jsonBoxed<T>(): BoxOf<T>+readBox(): BoxOf<string>:Validation
npm test— 116 golden cases match; zero existing goldens changed (no orphans there), only the new fixture added.npm run test:compile— all 116 compile on ReScript.npm run bench— real orphan removals surfaced and accepted:@smastrom/react-rating(inputProps<'a>, a callback-return generic) and@juspay/blend-design-system(plus a spuriousHighchartsSharedTypesSCC merge dissolving intoChartsTypes); all metrics equal-or-better, all compile. Baselines updated in this PR.return-only-generic-orphan;docs/TYPE_MAPPING.md+ CHANGELOG updated.The
#189method-path rollback is kept as a cheap early exit; the sweep is now the general backstop (and the mechanism #178 asked for).Fixes #191
🤖 Generated with Claude Code