Skip to content

Experiment: direct-base assignability for generic interface wrappers - #1

Draft
wrhall wants to merge 1 commit into
mainfrom
codex/interface-base-fastpath
Draft

wrhall wants to merge 1 commit into
mainfrom
codex/interface-base-fastpath

Conversation

@wrhall

@wrhall wrhall commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Discussion experiment

Try a direct-base assignability shortcut for a generic interface that adds members to another generic interface:

interface Owned<T> extends Base<T> { owner: string; }
declare const owned: Owned<RecordType>;
const base: Base<RecordType> = owned;

This is a draft in my fork for review before proposing anything upstream. There is no associated upstream Backlog issue yet, and this is not ready for upstream submission.

The checker already normalizes single-base interfaces with no own members through getSingleBaseForNonAugmentingSubtype. Adding even one member excludes that normalization. This experiment adds a one-way shortcut for assignability, without replacing the source type or changing generic inference.

Scope

33 implementation lines across two existing checker files. Only different interface-reference targets in the assignable relation are eligible. The source must have generic arguments and exactly one direct base whose generic target matches the destination. Exclude variance marker types, explicit source this arguments, declarations containing this, bases not proven this-free by the existing helper, own member overrides, and added call/construct/index signatures.

Instantiate the matching base with the source arguments and use the existing relation machinery. Only TernaryTrue returns early; all other outcomes retain the structural fallback and its diagnostics. This is intentionally conservative, with no new cache in the first prototype.

The motivating Mongoose hierarchy is not covered by the current this-free restriction. This draft does not claim to recover the previously observed application-level benefit of changing an interface wrapper to an intersection. It isolates a possible assignability mechanism for discussion.

Measurements

Unmodified upstream 432307174150be61eb931ef1f754277f7197c117 versus this patch, using separately built native binaries. Three runs per version, alternating order. All checks exit 0. The compiler fixture uses a distinct --tsBuildInfoFile for every run; no traces. Counts reproduced exactly in these runs.

Workload Main instantiations Prototype instantiations
Synthetic large generic wrapper, 300 assignments / 30 generic methods 342,035 35,435
Synthetic unrelated generic target 35,101 35,101
Synthetic small wrapper 35,101 34,505
Repository compiler fixture 250,042 250,090

The last row is a small regression (+48, approximately 0.02%). This is not evidence of a general compiler speedup. The large-wrapper median check time was 0.311s → 0.181s; compiler-fixture median was 0.683s → 0.690s. These short timings on a shared machine need stronger measurement before drawing conclusions.

An initial recursively expanding unrelated-target stress case was interrupted while running the unmodified compiler; it is not represented by the completed unrelated-target control above. Initial compiler-fixture runs reused incremental state and were discarded in favor of the fresh-build-info runs reported here.

Reproduce the synthetic cases

Build one binary at the base commit and one with the patch:

go -C tsc build -o /tmp/ts-before ./cmd/tsc
# Apply the patch, then:
go -C tsc build -o /tmp/ts-after ./cmd/tsc

Generate the standalone cases:

from pathlib import Path

methods = "\n".join(
    f"operation{i}<U>(f: (value: T) => U): Base<{{ item: U; previous: T }}>;"
    for i in range(30)
)
assignments = "\n".join(
    f'declare const source{i}: Owned<{{ tag: "{i}" }}>;\n'
    f'const target{i}: Base<{{ tag: "{i}" }}> = source{i};'
    for i in range(300)
)
wrapper = "interface Owned<T> extends Base<T> { owner: string; }\n"
Path('/tmp/large.ts').write_text(
    "interface Base<T> { value: T;\n" + methods + "\n}\n" + wrapper + assignments
)
small = "interface Base<T> { value: T; }\n" + wrapper
Path('/tmp/small.ts').write_text(small + assignments)
Path('/tmp/unrelated.ts').write_text(
    small + "interface Other<T> { value: T; }\n"
    + assignments.replace(": Base<", ": Other<")
)

Run each binary with --noEmit --strict --extendedDiagnostics /tmp/large.ts (and the other two files). For the repository fixture:

/tmp/ts-before --noEmit --extendedDiagnostics \
  --tsBuildInfoFile /tmp/unique-before.tsbuildinfo -p tsc/testdata/fixtures/compiler
/tmp/ts-after --noEmit --extendedDiagnostics \
  --tsBuildInfoFile /tmp/unique-after.tsbuildinfo -p tsc/testdata/fixtures/compiler

Correctness checks

Generated the new fixture's diagnostics/types/symbols baselines with the unmodified compiler, then verified the prototype matches them. It covers matching and incompatible arguments, argument substitution, missing derived members, narrowed overrides, multiple bases, polymorphic this, overloads, recursion, and generic inference.

go -C tsc test -run='TestLocal/interfaceDirectBaseAssignability' ./internal/testrunner
ok .../internal/testrunner 0.540s

go -C tsc test ./internal/checker ./internal/testrunner
ok .../internal/checker 0.052s
ok .../internal/testrunner 41.778s

No existing baselines changed. The upstream dprint check passes for both changed Go files, and git diff --check passes. The initial Go/formatter sandbox failures were rerun successfully with access to the installed toolchain/cache. The full CONTRIBUTING.md release/API/language-service/lint validation matrix has not been run.

Questions for review

  • Is this an appropriate place for a success-only base comparison, including recursion/variance bookkeeping?
  • Is the helper's eligibility work cheap enough, or should declaration-level positive/negative results be cached?
  • Is extending this to polymorphic this worthwhile, and how should the inherited this argument be represented?
  • Which broader performance workloads should determine whether this should proceed?

AI assistance

Drafted and tested with GPT-6 via Codex at my request, following a specific investigation into generic interface-wrapper comparisons. This fork draft is for my review before any upstream submission.

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