ReviewOS

home-lang/home

fix(ts): close recursive generic defaults at constraint fixed points

#639
Closed chris opened this 8 days ago ·
8 days ago

Parent

  • #548
  • Correctness tracker: #416
  • Discovered while validating #633
  • Related symbolic-default work: #634

Problem

A recursive, fully defaulted generic interface can retain a declaration-owned type parameter at the fixed-point edge of its lowered object graph. containsFreeTypeParameter then classifies the entire target constraint as unresolved and defers otherwise decidable invalid type arguments.

Reduced example:

interface BaseState {
  output: unknown;
  tag: string;
  constr: new (def: any) => Type;
  parent?: Type;
}
interface State<O = unknown> extends BaseState { output: O }
interface Type<O = unknown, S extends State<O> = State<O>> { _zod: S }
type SomeType = { _zod: BaseState };
type FunctionOut = Type;
interface Tuple<Rest extends SomeType | null> { rest: Rest }
interface Good<Rest extends FunctionOut = FunctionOut> { value: Tuple<Rest> }
interface Bad { value: Tuple<{ nope: number }> }
interface WrongShape { _zod: { nope: number } }
interface BadNested<R extends WrongShape> { value: Tuple<R> }

Current Home ReleaseFast emits no diagnostics. TypeScript 6.0.3 accepts Good and emits TS2344 at both Bad and BadNested. TypeScript 7.0.2 accepts Good, emits TS2741 at Bad, and emits TS2344 at BadNested.

This is distinct from #633: declaration-order substitution works when the stored default remains symbolic. It is also distinct from #634's indexed-access preservation. The remaining problem is representing or classifying the closed recursive fixed point without treating its declaration-owned parameter as a genuinely free use-site argument.

Acceptance criteria

  • Preserve acceptance of the valid Good declaration.
  • Reject both invalid locations above, matching the current TypeScript 6/7 diagnostic families and locations.
  • Model the recursive default as closed, or otherwise distinguish declaration-owned fixed-point parameters from genuinely free outer parameters.
  • Do not globally disable free-parameter deferral or re-lower declarations during diagnostic proof.
  • Add focused checker and Program coverage with valid, concrete-invalid, and constrained-invalid controls.
  • Run Debug, ReleaseSafe, and ReleaseFast checker suites.
  • Re-run the unchanged pinned Zod 4.5.2 graph and require zero added normalized diagnostics.
  • Document accepted results and link commits/issues.
8 days ago

Related symbolic-default work #634 has landed on main in f6a014d4b / 773af2405. It deliberately preserves indexed-access TypeId graphs only during default lowering and does not alter free-parameter classification or recursively re-lower declarations. The recursive fixed-point repro and acceptance criteria in this issue therefore remain current and isolated.

8 days ago

Progress update for the recursive generic-default fixed-point work:

  • Root cause: recursive interface self-rechecks preserved provisional type-parameter payloads, so a later default such as S extends State<O> = State<O> could remain unknown. The fix refreshes the original parameter identities from the completed self-recheck with bounded, cycle-pruned substitution and preserves declaration/interface provenance through substitution.
  • Constraint closure is ownership-aware: only type parameters proven to belong to another HIR declaration or an imported Program declaration may close from stored defaults. Parameters owned by the current declaration, infer/mapped parameters, parameters without defaults, and unknown provenance still defer.
  • Free-parameter discovery now traverses generic-instantiation arguments; imported zero-argument declaration types retain display-only Program names. No HIR syntax is re-lowered and there is no global free-parameter exception.
  • A Zod regression found during the audit exposed readonly metadata loss in the bounded object substitution path. That path now preserves readonly/array/tuple origin metadata, with same-file and imported readonly controls.

Focused oracle status:

  • valid recursive default: accepted
  • concrete invalid Tuple<{ nope: number }>: TS2344
  • nested invalid Tuple<R> where R extends WrongShape: TS2344
  • genuine owner-local free parameter: still deferred
  • readonly recursive-default controls: accepted
  • same results across the two-file Program boundary

Pinned Zod 4.5.2, unchanged strict/no-emit 106-file graph, exact post-#634 parent:

  • parent: 604 normalized diagnostics
  • candidate: 600
  • removed: 4 × TS2345 at src/v4/core/schemas.ts:4395/4397/4401/4403
  • added: 0
  • parent identity hash: b1507875d4327b4df2dce0fed41123d3d409d8f85aca7b05fcaf6d3777c29210
  • candidate identity hash: f093515438ca8b68df2a1fea133f3e247d21574900579902513adac4636ecfe2
  • final candidate wall time: 25.05s (single-run transparency only; not a speed claim)
  • final ReleaseFast binary hash: a201e6d7c5c20f046a9b591ef43d8c47f9679653981d61027d6340fd5ebaab80

The pre-readonly-fix audit was rejected because it added three TS2344 identities; the final audit has zero additions. Full final-source Debug/ReleaseSafe/ReleaseFast checker and Program reruns are next.

8 days ago

Landed directly on main in two small, authored conventional commits (no coauthors):

  • 7d7cf23f8 — fix(ts): close recursive generic default fixed points (#639)
  • f7fa8f9c5 — docs(ts): record recursive default fixed-point audit (#639)

The implementation refreshes stable recursive declaration parameters after self-recheck, closes only foreign declaration-owned parameters with stored defaults, propagates explicit Program provenance, and preserves interface/display identity plus readonly/array/tuple metadata through bounded substitution. Current-declaration and unknown-provenance parameters still defer; there is no global deferral bypass or HIR re-lowering workaround.

Fresh harness-pinned oracle:

  • TypeScript 6.0.3: TS2344 for both invalid controls
  • native TypeScript 7.0.2: TS2741 for the concrete missing-property case and TS2344 for the constrained case
  • Home: TS2344 for both; the valid default and readonly controls pass

Accepted unchanged Zod 4.5.2 graph audit:

  • exact parent 773af2405: 604 normalized identities
  • candidate: 600 normalized identities
  • removed: the four target TS2345 identities at src/v4/core/schemas.ts:4395:77, :4397:53, :4401:77, and :4403:53
  • added: 0
  • candidate identity SHA-256: f093515438ca8b68df2a1fea133f3e247d21574900579902513adac4636ecfe2
  • ReleaseFast binary SHA-256: a201e6d7c5c20f046a9b591ef43d8c47f9679653981d61027d6340fd5ebaab80

After rebasing onto the then-current origin/main, the artifact hash stayed identical, both focused checker/Program reductions passed, and a fresh Zod run reproduced the same 600 identities byte-for-byte. The earlier rejected candidate's three readonly regressions were eliminated before acceptance.

Full pre-rebase semantic-source matrix passed for ts_checker and ts_program in Debug, ReleaseSafe, and ReleaseFast; the rebase touched only unrelated upstream runtime/codegen files. zig fmt, git diff --check, and scoped pickier pass. The required repository-wide bunx --bun pickier . still reports the existing 20,869 diagnostics across the broader tree; this change adds none in its documentation files.

The README summary links the detailed methodology and results in TS_PERFORMANCE.md. During final CLI oracle validation I also isolated the pre-existing --skipLibCheck / --ignoreConfig exit-139 bug in #645 and linked it to #486; both the exact parent and candidate reproduce it, so it is not a #639 regression.

Sign in to comment on this issue.