Summary
In compiled mode, class methods — both instance and static — ignore default parameter values entirely. A missing argument is left at its CLR zero value (0 for number, null for reference types) instead of the parameter's default. The interpreter handles all of these correctly. Top-level function declarations and arrow / function expressions apply defaults correctly in compiled mode (see #646); only class methods are affected.
Repro
class C {
m(a: number, b: number = 2) { return a + b; }
s2(a: string, b: string = "z") { return a + b; }
static st(a: number, b: number = 2) { return a + b; }
}
console.log(new C().m(5)); // interpreted: 7 compiled: 5 (b dropped → 0)
console.log(new C().s2("x")); // interpreted: xz compiled: xnull (b dropped → null)
console.log(C.st(5)); // interpreted: 7 compiled: 5
When every argument is supplied the methods behave correctly (e.g. new C().m(5, 10) ⇒ 15 in both modes); only the omitted-argument (default) path is broken.
Root cause
EmitMethod (Compilation/ILCompiler.Classes.Methods.cs) defines the method's parameters but never applies defaults: unlike the arrow path it calls neither ParameterTypeResolver.WidenDefaultedParamsToObject nor ILEmitter.EmitDefaultParameters, and unlike the top-level-function path it generates no OverloadGenerator lower-arity overloads. So a call supplying fewer arguments than parameters pads the missing slots with the CLR default and the body observes 0/null.
Why this is filed separately from #698
#698 (a default that references an earlier parameter) is now fixed for arrows / function-expressions / function-declarations in both modes, and for methods in the interpreter. The compiled-method earlier-param case (m(a, b = a * 2) ⇒ 5 instead of 15) is a strict subset of this broader gap — compiled methods drop all defaults, not just earlier-param ones — so it cannot be closed without fixing this first.
Fixing it needs care around virtual dispatch / override-signature matching. The arrow-style fix (widen defaulted params to object) changes a method's CLR signature and would break override / interface-implementation matching against a base or subclass whose same-named method has no defaults (e.g. class Base { m(a, b = 2) {} } vs class Derived extends Base { m(a, b) {} } — both must stay signature-compatible). The safer route is likely method-level arity overloads analogous to OverloadGenerator for free functions, with method dispatch selecting the right arity.
(Class expression methods in Compilation/ILCompiler.Classes.ClassExpressions.cs DO call EmitDefaultParameters and so may behave differently — not verified here. Note that path applies defaults via the null/$Undefined check, which cannot observe a value-type double/bool slot, cf. #705.)
Discovered
While implementing #698. Interpreter parity confirmed correct for all the above.
Summary
In compiled mode, class methods — both instance and static — ignore default parameter values entirely. A missing argument is left at its CLR zero value (
0fornumber,nullfor reference types) instead of the parameter's default. The interpreter handles all of these correctly. Top-level function declarations and arrow / function expressions apply defaults correctly in compiled mode (see #646); only class methods are affected.Repro
When every argument is supplied the methods behave correctly (e.g.
new C().m(5, 10)⇒15in both modes); only the omitted-argument (default) path is broken.Root cause
EmitMethod(Compilation/ILCompiler.Classes.Methods.cs) defines the method's parameters but never applies defaults: unlike the arrow path it calls neitherParameterTypeResolver.WidenDefaultedParamsToObjectnorILEmitter.EmitDefaultParameters, and unlike the top-level-function path it generates noOverloadGeneratorlower-arity overloads. So a call supplying fewer arguments than parameters pads the missing slots with the CLR default and the body observes0/null.Why this is filed separately from #698
#698 (a default that references an earlier parameter) is now fixed for arrows / function-expressions / function-declarations in both modes, and for methods in the interpreter. The compiled-method earlier-param case (
m(a, b = a * 2)⇒5instead of15) is a strict subset of this broader gap — compiled methods drop all defaults, not just earlier-param ones — so it cannot be closed without fixing this first.Fixing it needs care around virtual dispatch / override-signature matching. The arrow-style fix (widen defaulted params to
object) changes a method's CLR signature and would break override / interface-implementation matching against a base or subclass whose same-named method has no defaults (e.g.class Base { m(a, b = 2) {} }vsclass Derived extends Base { m(a, b) {} }— both must stay signature-compatible). The safer route is likely method-level arity overloads analogous toOverloadGeneratorfor free functions, with method dispatch selecting the right arity.(Class expression methods in
Compilation/ILCompiler.Classes.ClassExpressions.csDO callEmitDefaultParametersand so may behave differently — not verified here. Note that path applies defaults via the null/$Undefinedcheck, which cannot observe a value-typedouble/boolslot, cf. #705.)Discovered
While implementing #698. Interpreter parity confirmed correct for all the above.