Repository navigation
feat(ts): fields typed by an associated type - #644
Conversation
Quest-Claim: 8744fe16662b1e24
…meter A projection a generic type's fields name, such as smallvec's <A as Array>::Item, directly, through a generic type of the crate's they hold, or as a std iterator's item, is a TypeScript type parameter of its own after the type's, named for its associated type, which each instance gives what the projection normalizes to there. A field's Option of one is an rs.OptionOf, checked per instance as a parameter's is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nothing A projection from a type parameter may drop at an instance, as the parameter may, so a generic type holding one keeps its glue, which drops it through its [Symbol.dispose], and an instance shares that glue only where what each of its projections is can be disposed of. Projections only a PhantomData or a std iterator's own arguments hold name no type parameter, as typing them names nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a generic struct's glue drops through is its type arguments and what each projection its fields name is at them, for whether the instance's values need disposing as for whether its glue can dispose of them. An Option or Poll over a projection its definition types nothing of, as a PhantomData's, is not checked for its erasure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: full rethink, picked by the maintainer on 2026-11-07.
Rejected: targeted fixes with a shared ignored-parts rule; one walk with self-reference refused. (Written by Claude Opus 5.5 with high effort) |
…s and Options A generic type's projection parameters and the Option/Poll parts its instances are checked against are recorded by ty() while its fields are typed, instead of found by separate walks over the Rust types, so what types nothing (a PhantomData) is never checked or made a parameter. Types typed with each other complete together: an instance of a definition still being typed gets its projection arguments filled in once every definition in the cycle is, and its drop glue and checks run then. A projection whose value at an instance holds that instance again is refused with its span instead of overflowing the translator's stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing an instance's own type arguments, or a definition or trait object they reach, ends at a named type, so only re-entry while typing projection values or the erasure check is endless. The census at the moq pin had 9 false refusals (kio::Shared, crossbeam's Atomic<Node<T>>, Shared<dyn ErasedSession>). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pending instance at other arguments than its definition's parameters is refused when filling it in names another instance still being typed, not only a new projection, and the instances it named are never filled. Drop classification stops at an instance it is already asking about or past the crate's recursion limit. Instance types are memoized within one type expression, so typing a deep chain is linear, and the decided-place analysis's `lends` walk stops past the recursion limit. Each ends in a spanned refusal where rustc accepts the input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A type whose projections name ever deeper instances, twice at each level, now refuses at the recursion limit without building the doubling type it typed, the drop walk refuses at the outermost type's definition instead of answering inert, and the walk over what a type holds answers that it may hold anything past the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An instance without type parameters that a projection argument holds is named by an exported alias of its own, typed once, so a type whose two projections each name the next instance grows with the instances rather than the paths through them, and one whose projection holds it again (`Source::Item = Held<Source>`) names its alias instead of being refused. An enum's alias spells out its union, which TypeScript needs for an alias that names itself. Drop-glue classification keeps each answer once it no longer rests on an instance still being asked about. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: nested and self-referencing instances are now named by TypeScript type aliases, one exported alias per instance, as the maintainer decided on 2026-11-07.
The next review starts a new session with (Written by Claude Opus 5.5 with high effort) |
…crete chains An alias is named by all its type arguments, so one at a defaulted parameter's default is no longer named as its type is. An instance without type parameters met while its definition is still being typed no longer counts as endless growth when its projections name another: each has an alias, so a finite chain ends, and one nested past the recursion limit is refused at the instance the source wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stances Filling in an instance's projection arguments may name more projections or instances; they are now filled until none is new, refused only past the crate's recursion limit, by how deep instances nest or how many projections the definition names, rather than at the first growth. An instance of a type with only const arguments gets an alias too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A definition may name more projections than the recursion limit, each independent; what grows without end is a projection of projections nesting ever deeper, so that is what is refused past the limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he recursion limit Each round of the fill loop fills what the round before named, so a finite layout settles in as many rounds as its chain is long, and endless growth of any kind never settles. More rounds than the crate's recursion limit are refused at the definition. This replaces the instance-depth and nested-projection-count guards, which missed growth inside ordinary type arguments and refused 130 sibling projections two deep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Design change: the fill loop's rounds are capped at the crate's recursion limit, decided on 2026-11-07 by the coordinator while the maintainer was away (option 1 of round 3's checkpoint).
The next review starts a new session with (Written by Claude Opus 5.5 with high effort) |
…s, and check held instances' Options Filling an open definition's instances can name an instance of a definition further out that is still being typed; the inner one now completes with it instead of alone with too few projection arguments. A definition also takes in the `Option` and `Poll` parts each generic instance over its parameters decided, so `Outside<()>` holding `Projected<()>` is checked as `Projected<()>` is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trolFlow and future parts A definition's checks keep the definition that typed each part and its arguments, so an instance of a type holding `Slot<MaybeUninit<S>>` is checked as that slot's instance is. A `ControlFlow` breaking with what an instance decides, and a future of it, are checked at instances too. Only instances outside projection arguments are held, which an instance types at its own arguments, and a refused endless definition checks nothing, so checks no longer multiply with the instances. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ill what they keep A trait object's methods are typed over the trait's own parameters, so typing them while a definition is open no longer records the trait's `Option<T>` against that definition. Completing a cycle that typed instances of open definitions now fills their projection arguments wherever a type typed meanwhile is kept, including the methods an object holds and items made before the cycle, leaving those of a cycle further out for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 14 minutes. View limit details
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. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head 7c9b53ee41ef0010257301af6e48d307c6e4d246 against base 532edb9081fe4a78a92f0bc7948b33598587d2dc.
Found one P2 issue: a projection that normalizes to a generic instance can lose that instance's deferred representation checks. Details and a minimal regression case are inline.
The ty()-driven record and concrete-instance aliases are a sound direction. Retain check propagation for generic instances introduced by projection normalization and add a reject regression for the indirect Slot<MaybeUninit<u32>> case before merging.
Scope: projection recording and erasure checks, deferred cycle completion, aliases, trait-object recording/signature filling, drop-glue classification, recursion guards, and the added fixtures. Verification was static, through GitHub only. No builds, compiler probes, or tests were run. This is a COMMENT review, not an approval.
(Written by review (OpenAI))
| let (Some((adt, _)), Some(_)) = (self.defining, self.defining_state.field) else { | ||
| return; | ||
| }; | ||
| if !instance.has_param() || self.defining_state.projecting > 0 { |
There was a problem hiding this comment.
[P2] Retain checks for generic instances produced by projection normalization
Skipping every instance while projecting > 0 loses checks when a projection normalizes to a generic ADT in the enclosing definition. For example:
pub trait P { type Item; }
pub struct Slot<T> { pub value: Option<T> }
pub struct Inner<T: P> { pub item: T::Item }
pub struct Src<S>(std::marker::PhantomData<S>);
impl<S> P for Src<S> { type Item = Slot<S>; }
pub struct Outer<S> { pub inner: Inner<Src<S>> }
pub fn expose(x: &Outer<std::mem::MaybeUninit<u32>>)
-> &Outer<std::mem::MaybeUninit<u32>> { x }While typing Outer<S>, Inner<Src<S>>'s projection becomes Slot<S>. This guard omits that slot from Outer's held instances, and its immediate erasure check defers because Option<S> still has a parameter. Inner has no checks to propagate, and Outer records no projection of its own after normalization. Typing the concrete Outer<MaybeUninit<u32>> therefore never checks Slot<MaybeUninit<u32>>: its field becomes rs.OptionOf<rs.u32>, whose None is undefined, while the Rust instance requires rs.None. This is the incompatibility already rejected by the direct projected_uninit_slot fixture, reached through a normalized projection instead. Preserve these parametric instances' checks in the enclosing record. Static trace only; the example has not been executed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Problem
rust2-ts refused a generic type whose field is typed by an associated type of its parameter, such as smallvec's
Drain<'a, T: Array>holdingslice::Iter<'a, T::Item>, withunsupported type <T as smallvec::Array>::Item. A type's definition is one TypeScript type over its type parameters, and a projection had no TypeScript name. Quest: quest/m3/ts-assoc-type-fields.md.Decisions
ty()named: ✅ full rethink,ty()as the one source of truth, including self-referencing projections (rejected: targeted fixes with a shared ignored-parts rule; one walk with self-reference refused).Held<Src>whereSrc::Item = Held<Src>: ✅ name nested instances with TypeScript type aliases, one exported alias per instance, so output grows with the number of instances, not the paths through them, and a self-referencing projection names its alias (rejected: refusing past rustc'stype_length_limit, which refuses programs rustc accepts and is still slow to reach; only stopping typing after the self-reference refusal, which still runs out of memory on the deep case). The recursion-limit guards stay where they still matter.Node<S>holdingNode<Wrap<S>>) is bounded while its instances' projection arguments are filled in, decided on 2026-11-07 by the coordinator while the maintainer was away, after review round 3 of the alias session found the endlessS::Many<Wrap<Wrap<…>>>growth missed and 130 sibling projections two deep refused: ✅ cap the fill loop's rounds at the crate's recursion limit, one guard replacing both the instance-depth guard and the nested-projection count, refused at the definition, as rustc bounds its own recursion (rejected: capping each new projection's nesting depth, which keeps two guards and refuses a finite projection written more than 128 types deep; refusing the first growth at arguments over type parameters, which refuses finite layouts native Rust accepts, such asLayer<Peel<S>>). Accepted cost: a finite chain of more than 128 links through an open definition is refused.Approach
ty()in the definition) is the one record of what it names (ts/src/lower/defined.rs): each projection over its parameters it types becomes a TypeScript type parameter after its own, and eachOption,Poll, orControlFlowit types over a parameter is noted for the check of its instances. That covers a projection named directly, one held through another generic type of the crate (Batch<S>holdingDrained<S>getsBatch<S, Item>), and a std iterator's item (Peekable<I>'s<I as Iterator>::Item), each becausety()typed it. What types nothing, such as aPhantomData, is neither a parameter nor checked. A parameter is named for its associated type and numbered past any name it would shadow (Ahead<_Item, _I, Item2, Item3>).Drained<[u8; 4]>isDrained<Uint8Array, rs.u8>.field_value's casts use the same arguments (Translator::instance_params).Translator::instance_alias):export type Carried$generics_Looped = Carried<Looped.Looped, Carried$generics_Looped>. The alias is named before its body is typed, so an instance its own projections reach again, directly or through another (Ping<Rally>/Pong<Rally>), names it. An enum's alias spells out its union at the instance (type Chain$generics_Linked = { tag: "End" } | { tag: "Link"; _0: rs.u8; _1: Chain$generics_Linked }), sincetscrefuses an alias that names itself through a generic alias (TS2456); a struct's names the interface or class. An instance the source writes, outside a projection argument, keeps its inline type arguments.Defined::checks,defined::Decides): anOptionorPollover a parameter or projection, aControlFlowbreaking with one (typed as anrs.Result, which anOption<!>orResult<!, _>break makes otherwise), and a future of one (whose output an instance may makers.Pending). It also records each instance of the crate's generic types over its parameters its fields type outside projection arguments (Outside<S>holdingProjected<S>), and each round takes in that instance's checks at its arguments, still in the frame of the definition that typed them, so an instance of the outer type checks exactly what the held instance would be checked for. Instances in projection arguments are left out, as each instance types them at its own arguments, which checks them there. A definition the round cap refuses checks nothing more.generic_erasurereads only the recorded checks: anOptionorPollof a projection is anrs.OptionOf/rs.PollOflike one of a parameter, so an instance whose erasure differs is still refused (Option<MaybeUninit<u32>>,Option<Option<()>>). What it types of a projection's value goes through the aliases too.Pile<S>holdingBox<Pile<S>>) or through others (aTree<S>ofBranch<S>es), complete together. An instance of a definition still being typed holds aType::Laterfor its projection arguments. When the outermost definition of the cycle finishes, those are filled in, round by round, until no record grows, then spliced in, aliases included, and the instances' checks and the definitions' drop glue run. Each round fills what the round before named, so a finite layout settles in as many rounds as its chain is long; more rounds than the crate's recursion limit are refused at the outermost definition (Defining::complete). Filling may name an instance of a definition further out that is still being typed (Twig<Sprout>namingGrove<Tail>whileGroveis typed); the inner definition then completes with that one rather than alone. Completing fills those projection arguments wherever a type typed meanwhile is kept: the cycle's items, items made before it such as a trait object's type, and the methods an object holds (Holderholding aBox<dyn Get<u8>>whoseholderreturnsOption<Holder>). A trait object's methods are typed outside the definition that names the object, over the trait's own parameters. A type holding itself with its parameters swapped (Swap<A, B>holdingSwap<B, A>) gets its own projections in the other order:Swap<B, A, Item2, Item>.glue_held). Each such answer is kept once it rests on no instance still being asked about, and answers that rest on one are kept once it holds, so a graph of instances is classified once per instance, cycles included (glue_all).Evidence:
lang/shapes:generics::projectionsruns a struct, an enum that holds itself, a nested generic type, a stdPeekable, and a projection from a projection, at arrays ofu8,u16,(), andOption<u8>, and at a store that is no array, against native Rust, with drop order for items that drop (Kept<Ticks>), items lent by reference behind anRc(Kept<Lent>), and projections only aPhantomDataholds (Marked).generics::projected_cyclesruns mutually recursive types (Tree/Branch, the enumEvenwith the structOdd), a type holding itself with swapped parameters (Swap), and a projection beside aPhantomData<Option<_>>of it at[Option<()>; 2](Ghost).generics::projected_selvesruns a self-referencing struct that drops (Carried<Looped>), a self-referencing enum (Chain<Linked>), a struct and an enum holding each other (Ping<Rally>/Pong<Rally>), a defaulted parameter (Defaulted), a finite chain through an open definition (Node<Tail>), finite projection growth (Layer<Peel<S>>), a const-only instance (Counted<0>), 130 associated types (Wide<u8>), a projection of 130 sibling projections only two deep (Sibling<u8>), an enum whose variant's type names the enum's instance before the enum names its item, in both variant orders (Grove<Tail>,Copse<Tail>), a type holding a trait object whose methods return it (Holder), andForked<Depth<0>>, whose two projections each name the next of 25 instances, the last holding the first again, with items that drop.lang::shapestranslates in about 15 seconds. Beforeglue_allkept its answers, the same fixture took over 8 minutes in drop-glue classification, and a 25-instance standalone crate took 2 minutes; it now takes about 4 seconds, and about 2 with the cycle back to the first instance.glue_heldpassing only type arguments),lang::shapesreports 145 differences from native Rust in the drop log.values::projected_uninit,values::projected_nested,values::projected_chain(a type holding itself at its parameter'sNext, whose projections would be new at each link),values::projected_spread(a type holding itself at a wrapped source, refused at its definition and its use),values::projected_sprawl(an endless chain of instances without type parameters, typed while its definition is),values::projected_outside(anOption<Option<()>>reached only through another generic type's field),values::projected_uninit_slot(Slot<MaybeUninit<u32>>held byUninit<u32>, whileUninit<u8>translates ingenerics::projected_selves),values::projected_flow(aControlFlowbreaking with a projection that is anOption<Infallible>),values::projected_future(a future of a projection that is aPoll),values::projected_grown(endless growth inside an ordinary type argument,S::Many<Gather<Gather<…>>>, one projection deep, refused at the definition),values::projected_fork(an endless chain of ever deeper instances, refused at the recursion limit),values::projected_deep(dropping a borrowed chain nested past the recursion limit, refused rather than counted inert), andvalues::projected_over(a self-referencing instance over a generic type's own parameter, see below).values::projected_inertandvalues::projected_disposabledrop a self-referencing instance behind anRcand translate.Impact
<Type>$<arguments>, in its type's module. No type that translated before changes shape. Only the new fixture types and their callers appear in snapshots.Choices made without asking
Item), thenItem2,Item3… past any it would shadow. The alternative,S_Item-style names that always spell the parameter out, reads worse for the common single-projection case. Trait objects already name their associated types by bare name (Source<Item>).Forked$generics_Depth_1). Only instances inside a projection argument get one; an instance the source writes keepsDrained<Uint8Array, rs.u8>, which reads better and is never the exponential or endless part.Lent<Relent<T>>inOver<T>) is still typed inline, and one whose projection holds it again is still refused (values::projected_over). An alias there would take the definition's parameters, including projections recorded only once its cycle completes, and would hide forms such asrs.ArrayOf<T>from the castsfield_valuederives, so it is a larger change for a case no census reaches. The same inline typing can still grow with the paths through a chain of generic impls below the recursion limit. See Follow-ups.unsupported type, as before this PR.ControlFlow: one typed as anOption(ControlFlow<Option<Infallible>, T>) is now checked like anOption, sincety()types it asrs.OptionOf<T>too. No fixture changed.Pile,Swap,Layer<Peel<S>>). It is refused at its definition when filling its instances takes more rounds than the recursion limit (Spread,Sprawl,Nested,Grown), as decided above.Alternatives
StoreItem<S>resolving byS): this would need one TypeScript type per trait, mapping every impl's self type. It cannot express impls on types that TypeScript erases alike ([u8; 4]and aUint8Arrayview), and it breaks for blanket impls.Options (this PR's first design): each walk drifted from whatty()typed (rounds 1 and 3), which the record removes.type_length_limit, or stopping after the self-reference refusal: rejected by the maintainer, see Decisions.Follow-ups
Exponential width under the round cap: a definition whose instances name two new instances each round (
Bin<S>with<Le<S> as Two>::A = Bin<Le<Le<S>>>andB = Bin<Ri<Le<S>>>) names 2^n instances by round n, so translation runs for minutes before the cap. The guards this PR replaced hung on it the same way (both measured past a 5-minute timeout on the reject fixture). Recommend deciding with the next design checkpoint whether to bound the instances filled rather than the rounds; no census reaches it.Aliases for instances over a generic type's own parameters (
Over<T>holdingLent<Relent<T>>): recommend planning it only once a census reaches one. No moq census does today, and it needs alias type parameters that track the definition's projections.Validation
just ciat the head (after mergingorigin/mainat532edb908): lint passes and all 91 fixture tests pass.generic_uninit_byte_opengains a refusal at its use ofByteAnd<u8, u32>, the instance itsHoldsBytedefinition was already refused for, now checked through whatHoldsByte<u32>holds.Holderpanicked before its fix (Option<T/#1> has parameters, but no args were provided).Grove<Tail>panicked inexpand_enum_aliases, andprojected_outsidetranslated with no refusal.quest check --fix: 35 documents ok.just ts censusat the moq pin, at this head: model (ts/moq/model.toml) 50 refusals, kio 75, moq-net 335, the same before and after every review round's fixes and the latest merge (341 at the earlier base55bf7409a; main has moved). No refusal names the fill-round guard or the recursion limit. smallvec'sunsupported type <T as smallvec::Array>::Itemstays gone.values::projected_grown(endless growth throughS::Many<Gather<…>>) is refused atGrown's definition, andgenerics::projected_selves'Sibling<u8>(130 sibling projections two deep) translates and matches native Rust.f1851e946's guards.lang::shapesdiffers from native Rust in 145 cases (the drop log).Defaulted$generics_Back), finite concrete chains refused while their definition is open (Node<Tail>), finite projection growth refused at its first step (Layer<Peel<S>>), const-only instances without an alias (Counted$0), and a projection-count bound refusing many independent associated types (Wide<u8>, 130 of them), each with a differential case ingenerics::projected_selves.lendsinmir/decided.rs,reachesinmir/mod.rs); each is bounded by the recursion limit and answers conservatively.Models
Size: L (the quest's
[M], grown by the rethink)Implementer: Claude Code, claude-opus-5-5, high effort
(Written by Claude Opus 5.5 with high effort)
🤖 Generated with Claude Code
Review
Rewritten by
quest reviewafter each round.01a1b875-2d09-71d0-89af-342ad6e7dd97, round 2Both reported defects are fixed, compiler probes and 504 differential cases pass, and no further defects were found in the pinned diff; the full suite was not rerun.ts/src/lower/defined.rs:262:[P1] Suspend ADT recording while typing trait-object definitions;Trait-object definitions and methods now use outside_definition at ts/src/lower/dynamic.rs:164 and :356, and the original panic reproducer completes with zero refusals.ts/src/lower/defined.rs:549:[P2] Resolve deferred arguments in retained type copies;Completion fills retained item types at ts/src/lower/defined.rs:569 and cached object signatures through fill_objects at :579, and the original recursive-return call completes with zero refusals.