Skip to content

Compiled: class methods (instance + static) ignore default parameter values #723

@nickna

Description

@nickna

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.

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