Skip to content

A generic class takes its type arguments from its constructor's arguments; undefined in a typed slot is zero - #416

Merged
ASDAlexander77 merged 2 commits into
mainfrom
generic-ctor-array-inference
Sep 30, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
generic-ctor-array-inference

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

new Set([2, 4, 6, 8]) was Set<any>. A generic class written without type arguments never looked at its constructor's arguments:

  • with defaults (Set<V = any>), it took the defaults;
  • without them, it failed with "Generic class 'Box' is missing type arguments", even for new Box(5).

As a result, DefaultLib's Set and Map tests only ever ran the any path. This PR has two commits.

1. A generic class takes its type arguments from its constructor's arguments

inferClassTypeFromConstructorArguments does for new what a generic function call already does:

  1. evaluates each argument's type;
  2. reads the constructor's parameter types, with the class's type parameters standing for themselves;
  3. unifies the two through inferType;
  4. widens the result and instantiates the class.

Details:

  • A type parameter the arguments don't reach keeps its default.
  • It doesn't apply, and the old behaviour stands, when:
    • the class has several constructors;
    • a type parameter has neither an inferred type nor a default;
    • an inferred type is still generic.
  • A function argument's type is not used, because its parameters are what's being worked out.
  • A declared type still wins: const s: Set<number> = new Set([1, 2]) is Set<number>.
  • Literals follow function inference: new Set([2, 4, 6, 8]) is Set<si32>, just as first([2, 4]) is first<si32>. This is the existing integer-literal rule, applied the same way to classes.

This needed one fix in inferType. An unnamed tuple's fields ([K, V]) were matched by field id, and that id is null for all of them, so every field found the first one: K and V both took the first element's type. firstValue([["one", 1]]) inferred <string, string> and didn't compile, and new Map([["a", 1]]) would have gone the same way. Fields are now matched by position.

Test: 00generic_class_constructor_inference.ts. Cases:

  • no default;
  • a default;
  • no arguments;
  • a declared type;
  • a [K, V][] parameter;
  • a parameter left to its default;
  • a callback argument;
  • new inside a generic function;
  • the function-call tuple case.

Without the change it doesn't compile.

2. undefined in a slot that can't hold it is zero, not undef

Typed Sets exposed this straight away. A finished generator's { value: undefined, done: true } puts undefined into a value of the element type. When that type can't hold it (si32, number, boolean), the field was LLVM undef. That happened both in the constant tuple (LLVMCodeHelper::getTupleFromArrayAttr) and in ts.UndefOp's lowering.

Reading it back gave whatever the register held: new Set([1, 2, 3]).values().drop(4).next() printed { value: -971415704, done: true }. Owning types were already zero under rc, for the same reason and a worse one: a retain walks an undef pointer. Now every type in every model gets zero, which is deterministic and matches what a grown array's new slots hold.

Test: 00generator_done_value_zero.ts. Without the fix it fails in every model.

Results

Release ctest 3298/3298, plus the 6 registrations of the generator test (added after that run)
DefaultLib gc (compile + JIT) 158/158
DefaultLib rc (compile + JIT) 3 of 316 fail: the known sleep/weakref_basic link errors

DefaultLib's tests now use the typed path. set_union.ts, for example, instantiates Set<si32>.

The companion DefaultLib PR (ASDAlexander77/TypeScriptCompilerDefaultLib#16) updates iterator_drop.ts. Its last case expected an any Set's { value: undefined, done: true }. A Set<si32> can't hold undefined, so the case now checks done and value == 0.

Found on the way, not fixed

  • let a: any = undefined; a == undefined is false.
  • An any field that is assigned "text" and then compared == "text" is false. In a larger file the same lines crashed instead.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits September 30, 2026 16:02
A finished generator's `{ value: undefined, done: true }` puts undefined in
a `value` of the generator's element type. When that type cannot hold it
(si32, a number, a boolean), the field was LLVM undef, both in the constant
tuple (LLVMCodeHelper::getTupleFromArrayAttr) and in ts.UndefOp's lowering,
and reading it back gave whatever the register held: the last yielded
value, or garbage - a Set<si32>'s `values().drop(4).next()` printed
`{ value: -971415704, done: true }`. Owning types were already zero under
reference counting, for the same reason and a worse one (a retain walks an
undef pointer).

Both now give zero for every type in every model: deterministic, and the
same answer as a grown array's new slots.

Test 00generator_done_value_zero.ts (a finished si32 generator's value,
twice). Without the fix it fails in every model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents

`new Set([2, 4, 6, 8])` was Set<any>: a generic class written without type
arguments never looked at its constructor's arguments. It took its
defaults (`Set<V = any>`), or, with none, was "Generic class 'Box' is
missing type arguments" - even for `new Box(5)`. DefaultLib's own Set and
Map tests therefore only ever ran the `any` path.

inferClassTypeFromConstructorArguments does for `new` what a generic
function's call already does: it evaluates each argument's type, reads the
constructor's parameter types with the class's type parameters standing
for themselves, runs them through inferType, widens the result and
instantiates the class. A type parameter the arguments do not reach keeps
its default. It stays out of the way when there is nothing sure to say:
several constructors, a type parameter with neither an inferred type nor a
default, or an inferred type still generic. A function argument's type is
not used (its parameters are what is being worked out), and a declared type
still wins: `const s: Set<number> = new Set([1, 2])` is Set<number>.
Literals follow function inference: `new Set([2, 4, 6, 8])` is Set<si32>,
as `first([2, 4])` is first<si32>.

It needed a fix in inferType on the way: an unnamed tuple's fields
(`[K, V]`) were matched by their field id, which is null for every one of
them, so each found the first field and K and V both took the first
element's type. `firstValue([["one", 1]])` inferred <string, string> and
did not compile. They are matched by position now.

Test 00generic_class_constructor_inference.ts: no default, a default, no
arguments, a declared type, a [K, V][] parameter, a parameter left to its
default, a callback argument, `new` inside a generic function, and the
function-call tuple case. Without the change it does not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 17ba188 into main Sep 30, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the generic-ctor-array-inference branch September 30, 2026 15:26
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