Skip to content

feat(ts): fields typed by an associated type - #644

Merged
kixelated merged 27 commits into
mainfrom
quest/m3/ts-assoc-type-fields
Nov 8, 2026
Merged

kixelated merged 27 commits into
mainfrom
quest/m3/ts-assoc-type-fields

Conversation

@kixelated

@kixelated kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

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> holding slice::Iter<'a, T::Item>, with unsupported 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

  • How projections and the checks over them are found, picked by the maintainer on 2026-11-07 after review round 3 kept finding drift between separate walks over the Rust types and what 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).
  • How nested and self-referencing instances are named, picked by the maintainer on 2026-11-07 after review round 3 (second session) found a type whose two projections each name the next instance, 24 deep, reaching about 2^24 inline nodes, and since no finite list of type arguments names Held<Src> where Src::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's type_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.
  • How a definition that holds its own instances at other arguments (Node<S> holding Node<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 endless S::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 as Layer<Peel<S>>). Accepted cost: a finite chain of more than 128 links through an open definition is refused.

Approach

  • Typing a generic type's fields (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 each Option, Poll, or ControlFlow it 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> holding Drained<S> gets Batch<S, Item>), and a std iterator's item (Peekable<I>'s <I as Iterator>::Item), each because ty() typed it. What types nothing, such as a PhantomData, 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>).
  • Each instance passes what each projection normalizes to there: Drained<[u8; 4]> is Drained<Uint8Array, rs.u8>. field_value's casts use the same arguments (Translator::instance_params).
  • An instance without type parameters that a projection argument holds is named by an exported alias in its type's module, typed once (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 }), since tsc refuses 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.
  • A definition records each part its fields type whose representation its instances decide (Defined::checks, defined::Decides): an Option or Poll over a parameter or projection, a ControlFlow breaking with one (typed as an rs.Result, which an Option<!> or Result<!, _> break makes otherwise), and a future of one (whose output an instance may make rs.Pending). It also records each instance of the crate's generic types over its parameters its fields type outside projection arguments (Outside<S> holding Projected<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_erasure reads only the recorded checks: an Option or Poll of a projection is an rs.OptionOf / rs.PollOf like 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.
  • Types typed with each other, directly (Pile<S> holding Box<Pile<S>>) or through others (a Tree<S> of Branch<S>es), complete together. An instance of a definition still being typed holds a Type::Later for 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> naming Grove<Tail> while Grove is 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 (Holder holding a Box<dyn Get<u8>> whose holder returns Option<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> holding Swap<B, A>) gets its own projections in the other order: Swap<B, A, Item2, Item>.
  • Drop glue: a projection over type parameters may drop, as a parameter may. A generic class's glue types its fields normalized, in the type's defining context. An instance shares that glue, or counts it inert, by its type arguments and what each recorded projection is at them (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:

  • Differential cases in lang/shapes: generics::projections runs a struct, an enum that holds itself, a nested generic type, a std Peekable, and a projection from a projection, at arrays of u8, u16, (), and Option<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 an Rc (Kept<Lent>), and projections only a PhantomData holds (Marked). generics::projected_cycles runs mutually recursive types (Tree/Branch, the enum Even with the struct Odd), a type holding itself with swapped parameters (Swap), and a projection beside a PhantomData<Option<_>> of it at [Option<()>; 2] (Ghost). generics::projected_selves runs 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), and Forked<Depth<0>>, whose two projections each name the next of 25 instances, the last holding the first again, with items that drop.
  • The 25-deep fork typed inline was round 3's out-of-memory case. With aliases, lang::shapes translates in about 15 seconds. Before glue_all kept 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.
  • Without the projection drop fix (glue_held passing only type arguments), lang::shapes reports 145 differences from native Rust in the drop log.
  • Reject cases values::projected_uninit, values::projected_nested, values::projected_chain (a type holding itself at its parameter's Next, 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 (an Option<Option<()>> reached only through another generic type's field), values::projected_uninit_slot (Slot<MaybeUninit<u32>> held by Uninit<u32>, while Uninit<u8> translates in generics::projected_selves), values::projected_flow (a ControlFlow breaking with a projection that is an Option<Infallible>), values::projected_future (a future of a projection that is a Poll), 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), and values::projected_over (a self-referencing instance over a generic type's own parameter, see below). values::projected_inert and values::projected_disposable drop a self-referencing instance behind an Rc and translate.

Impact

  • Translator output: a generic type whose fields name a projection gains one type parameter per projection, after its own. An instance a projection argument holds is named by a new exported alias, <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.
  • No runtime, config, or CLI change.

Choices made without asking

  • Naming: the associated type's own name (Item), then Item2, 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>).
  • Alias names and scope: an alias is named like a function instance's suffix, by its type arguments (Forked$generics_Depth_1). Only instances inside a projection argument get one; an instance the source writes keeps Drained<Uint8Array, rs.u8>, which reads better and is never the exponential or endless part.
  • Instances over type parameters get no alias: in a generic type's definition, an instance over its parameters (Lent<Relent<T>> in Over<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 as rs.ArrayOf<T> from the casts field_value derives, 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.
  • Unions: a union's slots are typed outside its definition's record, so a projection in one stays unsupported type, as before this PR.
  • ControlFlow: one typed as an Option (ControlFlow<Option<Infallible>, T>) is now checked like an Option, since ty() types it as rs.OptionOf<T> too. No fixture changed.
  • A type holding itself at other arguments than its own parameters translates while filling its instances' projection arguments settles (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.
  • Dated census notes: the counts in quest/m2/ts-model/README.md and quest/m3/ts-unsafe/README.md are left as they are, since they are dated records of earlier censuses.

Alternatives

  • Per-trait conditional types (StoreItem<S> resolving by S): 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 a Uint8Array view), and it breaks for blanket impls.
  • A separate walk over the Rust types to find projections and Options (this PR's first design): each walk drifted from what ty() typed (rounds 1 and 3), which the record removes.
  • Refusing past 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>>> and B = 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> holding Lent<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 ci at the head (after merging origin/main at 532edb908): lint passes and all 91 fixture tests pass.
  • generic_uninit_byte_open gains a refusal at its use of ByteAnd<u8, u32>, the instance its HoldsByte definition was already refused for, now checked through what HoldsByte<u32> holds.
  • Holder panicked before its fix (Option<T/#1> has parameters, but no args were provided).
  • Round 1 of the new session's reproducers fail before their fixes: Grove<Tail> panicked in expand_enum_aliases, and projected_outside translated with no refusal.
  • quest check --fix: 35 documents ok.
  • just ts census at 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 base 55bf7409a; main has moved). No refusal names the fill-round guard or the recursion limit. smallvec's unsupported type <T as smallvec::Array>::Item stays gone.
  • Round 3 of the alias session's two reproducers: values::projected_grown (endless growth through S::Many<Gather<…>>) is refused at Grown's definition, and generics::projected_selves' Sibling<u8> (130 sibling projections two deep) translates and matches native Rust.
  • The branching case under Follow-ups ran past a 5-minute timeout on the reject fixture both at this head and with f1851e946's guards.
  • Without the projection drop fix, lang::shapes differs from native Rust in 145 cases (the drop log).
  • Alias session rounds 1 and 2 fixed: alias names dropping default arguments (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 in generics::projected_selves.
  • Review rounds 1 and 2 of the first session also surfaced three walks that never ended on ever deeper instances, two of them before this PR (lends in mir/decided.rs, reaches in mir/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 review after each round.

  • Reviewer: Codex, gpt-6.1-sol, xhigh effort
  • Session: 01a1b875-2d09-71d0-89af-342ad6e7dd97, round 2
  • Head: 7c9b53e
  • Base: origin/main at 532edb9
  • Score: 10/10, Both 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.
  • Findings: none
  • The last round's findings, at 97c2967:
    • Fixed: high, 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.
    • Fixed: medium, 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.

@pullfrog

pullfrog Bot commented Nov 7, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

…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>
@kixelated kixelated changed the title Fields typed by an associated type feat(ts): fields typed by an associated type Nov 7, 2026
kixelated and others added 2 commits November 7, 2026 04:28
…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>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Design change: full rethink, picked by the maintainer on 2026-11-07.

  • The type translation is the one source of truth. Typing a generic type's fields records each projection it names and each Option/Poll it types over a parameter; the drop glue and the Option/Poll erasure check read only that record, with no separate walk over the Rust types.
  • A type that holds itself, directly or through others, has its instances' projection arguments filled in once its definition (and every definition it is typed with) finishes, instead of from a partial list.
  • Self-referencing projections (Source::Item = Held<Source>) are to translate rather than be refused.

Rejected: targeted fixes with a shared ignored-parts rule; one walk with self-reference refused.

(Written by Claude Opus 5.5 with high effort)

kixelated and others added 6 commits November 7, 2026 07:05
…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>
kixelated and others added 2 commits November 7, 2026 12:14
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>
@kixelated

Copy link
Copy Markdown
Collaborator Author

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.

  • An instance without type parameters that a projection argument holds gets an alias in its type's module, typed once and named before its body is typed: export type Carried$generics_Looped = Carried<Looped.Looped, Carried$generics_Looped>. Output grows with the number of instances, not the paths through them, so round 3's two-projection fork no longer blows up. The fixture uses 25 instances, the last pointing back to the first, and lang::shapes translates in about 15 s.
  • Self-referencing projections (Source::Item = Held<Source>, and the mutual Ping<Rally>/Pong<Rally>) name their alias instead of being refused. An enum's alias spells out its union at the instance, since tsc rejects an alias that names itself through a generic alias (TS2456).
  • Drop-glue classification (glue_all) was also exponential on the deep fork (over 8 minutes on the fixture). It now keeps each answer once nothing still under way decides it. Answers inside a cycle are kept once the cycle's root holds.
  • Rejected: refusing past type_length_limit, and only stopping typing after the self-reference refusal.
  • Still refused: a self-referencing instance over a generic type's own parameters (values::projected_over), which no alias names yet. The recursion-limit guards stay.

The next review starts a new session with quest review --new.

(Written by Claude Opus 5.5 with high effort)

kixelated and others added 4 commits November 7, 2026 12:45
…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>
kixelated and others added 4 commits November 7, 2026 13:37
…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>
@kixelated

Copy link
Copy Markdown
Collaborator Author

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).

  • Each round of Defining::complete's 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 (projections, instances, or ordinary type arguments) never settles. More rounds than the recursion limit are refused at the outermost definition of the cycle, as rustc bounds its own recursion.
  • This one guard replaces both Pending::depth (instances named through more instances than the limit) and the nested-projection count. Spread, Sprawl, and Nested are now refused at their definitions.
  • Round 3's reproducers are fixtures: values::projected_grown (S::Many<Gather<Gather<…>>>) is refused at Grown's definition, and Sibling<u8>, a projection of 130 sibling projections two deep, translates and matches native Rust in generics::projected_selves.
  • Accepted cost: a finite chain of more than 128 links through an open definition is refused.
  • Rejected: capping each new projection's nesting depth (keeps two guards, and refuses a finite projection written more than 128 types deep); refusing the first growth at arguments over type parameters (refuses finite layouts native Rust accepts, such as Layer<Peel<S>>).

The next review starts a new session with quest review --new.

(Written by Claude Opus 5.5 with high effort)

kixelated and others added 6 commits November 7, 2026 14:13
…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>
@kixelated
kixelated marked this pull request as ready for review November 8, 2026 00:33
@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4dc7ff13-f00c-4fa9-aa8e-a457f2ebc93d
Commits

Reviewing files that changed from the base of the PR and between 532edb9 and 7c9b53e.

Files selected for processing: 73

  • quest/m3/README.md
  • quest/m3/ts-assoc-type-fields.md
  • ts/README.md
  • ts/fixtures/lang/shapes/expected/core/bool.ts
  • ts/fixtures/lang/shapes/expected/core/option.ts
  • ts/fixtures/lang/shapes/expected/generics.Ahead.ts
  • ts/fixtures/lang/shapes/expected/generics.Back.ts
  • ts/fixtures/lang/shapes/expected/generics.Batch.ts
  • ts/fixtures/lang/shapes/expected/generics.Branch.ts
  • ts/fixtures/lang/shapes/expected/generics.Carried.ts
  • ts/fixtures/lang/shapes/expected/generics.Chain.ts
  • ts/fixtures/lang/shapes/expected/generics.Copse.ts
  • ts/fixtures/lang/shapes/expected/generics.Counted.ts
  • ts/fixtures/lang/shapes/expected/generics.Deep.ts
  • ts/fixtures/lang/shapes/expected/generics.Defaulted.ts
  • ts/fixtures/lang/shapes/expected/generics.Depth.ts
  • ts/fixtures/lang/shapes/expected/generics.Drained.ts
  • ts/fixtures/lang/shapes/expected/generics.Even.ts
  • ts/fixtures/lang/shapes/expected/generics.Flags.ts
  • ts/fixtures/lang/shapes/expected/generics.Forked.ts
  • ts/fixtures/lang/shapes/expected/generics.Get.ts
  • ts/fixtures/lang/shapes/expected/generics.Getter.ts
  • ts/fixtures/lang/shapes/expected/generics.Ghost.ts
  • ts/fixtures/lang/shapes/expected/generics.Grove.ts
  • ts/fixtures/lang/shapes/expected/generics.Head.ts
  • ts/fixtures/lang/shapes/expected/generics.Holder.ts
  • ts/fixtures/lang/shapes/expected/generics.Kept.ts
  • ts/fixtures/lang/shapes/expected/generics.Layer.ts
  • ts/fixtures/lang/shapes/expected/generics.Lent.ts
  • ts/fixtures/lang/shapes/expected/generics.Linked.ts
  • ts/fixtures/lang/shapes/expected/generics.Looped.ts
  • ts/fixtures/lang/shapes/expected/generics.Marked.ts
  • ts/fixtures/lang/shapes/expected/generics.Node.ts
  • ts/fixtures/lang/shapes/expected/generics.Odd.ts
  • ts/fixtures/lang/shapes/expected/generics.Paths.ts
  • ts/fixtures/lang/shapes/expected/generics.Peel.ts
  • ts/fixtures/lang/shapes/expected/generics.Pile.ts
  • ts/fixtures/lang/shapes/expected/generics.Ping.ts
  • ts/fixtures/lang/shapes/expected/generics.Pong.ts
  • ts/fixtures/lang/shapes/expected/generics.Rally.ts
  • ts/fixtures/lang/shapes/expected/generics.Remade.ts
  • ts/fixtures/lang/shapes/expected/generics.Seed.ts
  • ts/fixtures/lang/shapes/expected/generics.Shoot.ts
  • ts/fixtures/lang/shapes/expected/generics.Sibling.ts
  • ts/fixtures/lang/shapes/expected/generics.Slot.ts
  • ts/fixtures/lang/shapes/expected/generics.Sprout.ts
  • ts/fixtures/lang/shapes/expected/generics.Swap.ts
  • ts/fixtures/lang/shapes/expected/generics.Tail.ts
  • ts/fixtures/lang/shapes/expected/generics.Ticks.ts
  • ts/fixtures/lang/shapes/expected/generics.Top.ts
  • ts/fixtures/lang/shapes/expected/generics.Tree.ts
  • ts/fixtures/lang/shapes/expected/generics.Twig.ts
  • ts/fixtures/lang/shapes/expected/generics.Uninit.ts
  • ts/fixtures/lang/shapes/expected/generics.Wide.ts
  • ts/fixtures/lang/shapes/expected/generics.ts
  • ts/fixtures/lang/shapes/expected/lib.ts
  • ts/fixtures/lang/shapes/expected/shapes.ts
  • ts/fixtures/lang/shapes/src/generics.rs
  • ts/fixtures/lang/shapes/src/lib.rs
  • ts/fixtures/reject/census.txt
  • ts/fixtures/reject/expected.txt
  • ts/fixtures/reject/src/values.rs
  • ts/fixtures/reject/ts.toml
  • ts/src/lower/defined.rs
  • ts/src/lower/drop.rs
  • ts/src/lower/dynamic.rs
  • ts/src/lower/iter.rs
  • ts/src/lower/mir/decided.rs
  • ts/src/lower/mir/mod.rs
  • ts/src/lower/mod.rs
  • ts/src/lower/names.rs
  • ts/src/print.rs
  • ts/src/ts.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@kixelated
kixelated added this pull request to the merge queue Nov 8, 2026

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Comment thread ts/src/lower/defined.rs
let (Some((adt, _)), Some(_)) = (self.defining, self.defining_state.field) else {
return;
};
if !instance.has_param() || self.defining_state.projecting > 0 {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Merged via the queue into main with commit 2a0a7d4 Nov 8, 2026
10 checks passed
@kixelated
kixelated deleted the quest/m3/ts-assoc-type-fields branch November 8, 2026 00:45
kixelated added a commit that referenced this pull request Nov 8, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant