Skip to content

Compiled: base-typed call to a derived override that adds trailing optional params dispatches to the base method (override ignored) #790

@nickna

Description

@nickna

Summary

In compiled mode, when a derived class overrides a base method by adding one or more trailing optional / default parameters (a valid TypeScript override — the signature stays assignable), a call made through a base-typed reference dispatches to the base method instead of the derived override. The derived method's added default therefore never fires, and the override is effectively invisible to base-typed call sites. The interpreter is correct.

This is not a default-parameter-firing bug: it reproduces with a reference-type default too (so the default mechanism itself is fine), and a direct call on the derived static type works. The root cause is virtual dispatch to a wider-arity override.

Repro

// value-type added default
class Base { m(x: number): number { return x; } }
class Derived extends Base { m(x: number, y: number = 100): number { return x + y; } }

const d = new Derived();
console.log(d.m(3));          // A  interp: 103   compiled: 103   (direct call — OK)

const b: Base = new Derived();
console.log(b.m(3));          // B  interp: 103   compiled: 3     (BUG — dispatches to Base.m)

// reference-type added default — shows it is NOT default-firing-specific
class B2 { g(x: number): string { return "x" + x; } }
class D2 extends B2 { g(x: number, s: string = "Z"): string { return "x" + x + s; } }
const b2: B2 = new D2();
console.log(b2.g(3));         // C  interp: x3Z   compiled: x3    (BUG — dispatches to B2.g)
case scenario interp compiled
A direct Derived.m(3) (static type Derived) 103 103 ✓
B base-typed b.m(3), value-type added default 103 3
C base-typed b2.g(3), reference-type added default x3Z x3

Compiled case B returns 3 and case C returns "x3" — exactly the results of invoking Base.m/B2.g, confirming the base-typed call binds to the base method and never reaches the override.

Root cause

Derived.m(x, y = 100) is a valid TS override of Base.m(x) because (x: number, y?: number) => number is assignable to (x: number) => number. But its CLR arity differs from the base (2 params vs 1), so the IL emitter defines it as a new (newslot) method rather than overriding the base's vtable slot. A statically-Base-typed call resolves to Base.m(double) and dispatches there.

The same-arity case is handled — ParameterTypeResolver's hierarchy-consistent widening (#705/#723/#787) unifies an override group that shares a signature (e.g. B.m(x) / D.m(x = 5), both arity 1), guarded by Override_DerivedAddsDefault_StillOverrides. It does not cover the different-arity case, where the base does not declare the added parameter at all.

Fix sketch

Emit an override shim / bridge for the base arity: Derived.m(double) marked override of Base.m(double), forwarding to Derived.m(double, double) with the omitted params filled by the default prologue. This is the "bridge methods" alternative noted in #737's fix sketch (the hierarchy-consistent-widening route shipped in #787 cannot unify signatures of different arity). Applies to instance methods generally (value-type and reference-type added defaults, and added no-default optional params — y?: number — exhibit the same dispatch gap).

Scope / discovery

Pre-existing and independent of the build-break fix it was found under (the generator-stub ParameterInfo[]/Type[] semantic merge conflict). Distinct from #723/#737/#738/#739: those are now fixed for direct calls and same-arity override groups (verified: cases A/D pass). This is the remaining different-arity override-dispatch slice. Discovered while verifying the #723/#737/#738/#739 cluster after #787.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions