HaxeFoundation / HaxeFoundation/haxe

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

Open
#13,006 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Haxe
Stars
6.9k
Forks
715
Avg merge
2d 2h
Merged PRs (30d)
11

Description

# [js] `inline` changes observable behavior: `Int + Null` yields `NaN` when called, `1` when inlined

## Repro

```haxe
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` 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 `+`:

```js
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

```python
@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

- #4555 — `"Result = " + o` with `o:Null` fails at runtime (open)
- #8627 — abstract + String concat generates incorrect code (open)
- #12975 — `[interp] Std.string((null:UInt))` throws `Invalid operation: null + 0` (open)

Contributor guide

Open the contributing guide

Research direction

Start with the Test.hx reproducer and run `haxe -main Test -js out.js && node out.js` to compare the literal, inline, and called forms. Trace the JavaScript backend code generation for optional arguments and addition; done means the called and inlined forms agree and produce the declared String result, with a regression test covering the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.