Summary
Follow-up to #705. In compiled mode, a value-type default (x: number = N, b: boolean = ...) on a sync instance or static method does not fire — an omitted argument leaves the CLR zero value and an explicit/padded undefined throws/NaNs. #705 fixed this for the non-virtual members (free functions, constructors, private methods) by widening the defaulted param to an object slot so the entry prologue can detect undefined. The remaining members were deliberately left out because widening them is not override-safe.
Repro (compiled)
class C { add(a: number, b: number = 10): number { return a + b; } }
console.log(new C().add(5)); // interp: 15 compiled: 5 (default not applied)
console.log(new C().add(5, undefined)); // interp: 15 compiled: throws InvalidCastException
class S { static add(a: number, b: number = 10): number { return a + b; } }
console.log(S.add(5)); // interp: 15 compiled: 5
Reference-type defaults (name: string = "x") do fire — they need no slot change. Argument-present calls are correct. Generator methods (*m(b = 5)) also don't fire value-type or reference-type defaults (their state-machine emitter, GeneratorMoveNextEmitter, never runs the default prologue at all).
Why deferred from #705
Instance methods are virtual. Widening a defaulted param's slot from double/bool to object changes the CLR signature, so a derived override that adds a default to a value-type param the base declares as required silently lands in a new vtable slot — a base-typed call then dispatches to the base method (a real polymorphism regression). See the regression guard Override_DerivedAddsDefault_StillOverrides in SharpTS.Tests/CompilerTests/ClassMethodDefaultParameterTests.cs. ParameterTypeResolver.ResolveMethodParameters therefore does not widen (see its remarks).
Fix sketch
Two override-safe options:
- Hierarchy-consistent widening — decide each param's slot from the topmost base declaration of the method (so the whole override chain agrees), not per-method defaults.
- Bridge methods — keep the value-type signature for override matching and emit a separate widened impl that the bridge forwards to (boxing).
Separately, the generator/async-generator method emitters need the default-parameter prologue added (the async non-generator emitter already has it).
Discovered
While implementing #705.
Summary
Follow-up to #705. In compiled mode, a value-type default (
x: number = N,b: boolean = ...) on a sync instance or static method does not fire — an omitted argument leaves the CLR zero value and an explicit/paddedundefinedthrows/NaNs. #705 fixed this for the non-virtual members (free functions, constructors, private methods) by widening the defaulted param to anobjectslot so the entry prologue can detectundefined. The remaining members were deliberately left out because widening them is not override-safe.Repro (compiled)
Reference-type defaults (
name: string = "x") do fire — they need no slot change. Argument-present calls are correct. Generator methods (*m(b = 5)) also don't fire value-type or reference-type defaults (their state-machine emitter,GeneratorMoveNextEmitter, never runs the default prologue at all).Why deferred from #705
Instance methods are virtual. Widening a defaulted param's slot from
double/booltoobjectchanges the CLR signature, so a derived override that adds a default to a value-type param the base declares as required silently lands in a new vtable slot — a base-typed call then dispatches to the base method (a real polymorphism regression). See the regression guardOverride_DerivedAddsDefault_StillOverridesinSharpTS.Tests/CompilerTests/ClassMethodDefaultParameterTests.cs.ParameterTypeResolver.ResolveMethodParameterstherefore does not widen (see its remarks).Fix sketch
Two override-safe options:
Separately, the generator/async-generator method emitters need the default-parameter prologue added (the async non-generator emitter already has it).
Discovered
While implementing #705.