[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:
-
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.
-
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
[js]
inlinechanges observable behavior:Int + Null<String>yieldsNaNwhen called,1when inlinedRepro
Actual (js)
addandadd_inlinehave identical bodies and identical signatures. Addinginlinechanges the observable result fromNaNto1.Expected
$typereports the function as(a : Int, ?b : Null<String>) -> Stringonevery target, so all three lines should produce the string
"1null". Atminimum,
addandadd_inlineshould agree with each other.Cause
The js backend emits the addition verbatim and relies on JavaScript's
polymorphic
+:Two distinct problems:
No String coercion. The typer concludes
String, but the js backendemits a bare
a + b.1 + nullis1in JavaScript and1 + undefinedis
NaN— neither is a String.Omitted optional is
undefined, notnull. Haxe specifies an omittedoptional argument as
null. In the generated functionbis simply amissing JS argument, so it is
undefined. This is what makes the calledform (
NaN) differ from the inlined form, where the omitted argument hasbeen substituted as a literal
nullat the call site (1).The python backend gets this right
Python emits an explicit
str()coercion plus aNone->"null"substitution, and returns
"1null"for both the called and inlined forms.So the typer's
Stringconclusion is achievable and correctly implementedelsewhere — this looks like a js codegen gap rather than a typing issue.
All targets tested
1 + nulladd_inline(1)add(1)11NaNTypeError(crash)1null1null--interp)eval raises
Uncaught exception Invalid operation: 1 + null— i.e. thereference interpreter treats this expression as invalid, while js silently
produces a value of the wrong runtime type.
The bare literal
1 + nullis 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
"Result = " + owitho:Null<UInt>fails at runtime (open)[interp] Std.string((null:UInt))throwsInvalid operation: null + 0(open)