Skip to content

[js] inline changes observable behavior: Int + Null<String> yields NaN when called, 1 when inlined #13006

Description

@mallyskies

[js] inline changes observable behavior: Int + Null<String> yields NaN when called, 1 when inlined

Repro

class Test {
  static function add(a:Int, ?b:String) {
    return a + b;
  }

  static inline function add_inline(a:Int, ?b:String) {
    return a + b;
  }

  static function main() {
    trace(1 + null);        // 1
    trace(add_inline(1));   // 1
    trace(add(1));          // NaN
  }
}
haxe -main Test -js out.js && node out.js

Actual (js)

Test.hx:11: 1
Test.hx:12: 1
Test.hx:13: NaN

add and add_inline have identical bodies and identical signatures. Adding
inline changes the observable result from NaN to 1.

Expected

$type reports the function as (a : Int, ?b : Null<String>) -> String on
every target, so all three lines should produce the string "1null". At
minimum, add and add_inline should agree with each other.

Cause

The js backend emits the addition verbatim and relies on JavaScript's
polymorphic +:

Test.add = function(a,b) {
	return a + b;
};
...
console.log("Test.hx:11:", 1 + null);       // literal        -> 1
console.log("Test.hx:12:", 1 + null);       // inlined        -> 1
console.log("Test.hx:13:", Test.add(1));    // b is undefined -> NaN

Two distinct problems:

  1. No String coercion. The typer concludes String, but the js backend
    emits a bare a + b. 1 + null is 1 in JavaScript and 1 + undefined
    is NaN — neither is a String.

  2. Omitted optional is undefined, not null. Haxe specifies an omitted
    optional argument as null. In the generated function b is simply a
    missing JS argument, so it is undefined. This is what makes the called
    form (NaN) differ from the inlined form, where the omitted argument has
    been substituted as a literal null at the call site (1).

The python backend gets this right

@staticmethod
def add(a, b = None):
    return (str(a) + ("null" if b is None else b))

Python emits an explicit str() coercion plus a None -> "null"
substitution, and returns "1null" for both the called and inlined forms.
So the typer's String conclusion is achievable and correctly implemented
elsewhere — this looks like a js codegen gap rather than a typing issue.

All targets tested

1 + null add_inline(1) add(1)
js 1 1 NaN
python TypeError (crash) 1null 1null
eval (--interp) throws throws throws

eval raises Uncaught exception Invalid operation: 1 + null — i.e. the
reference interpreter treats this expression as invalid, while js silently
produces a value of the wrong runtime type.

The bare literal 1 + null is separately wrong on both js (1) and python
(TypeError); only the function forms on python match the declared type.

Nothing warns at compile time: default flags, and @:nullSafety(StrictThreaded)
on the class, both compile with exit 0 and no diagnostics.

Version

Haxe 4.3.4 (macOS) and haxe_2026-07-10_development_5f83789.tar.gz.

Possibly related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions