Skip to content

Breaking changes in Haxe 4.0.0

Aleksandr Kuzmenko edited this page Nov 4, 2019 · 24 revisions

Important and breaking changes in Haxe 4

Assigning field in a closure in a setter bugfix

function set_some(value) {
  var old = some;
  some = value;
  rollbacks.push(function() some = old); // <-- bypasses setter now as it should

Use -D haxe3compat to get warnings pointing to places that need to be fixed.

final keyword

final is a new keyword and can no longer be used as an identifier.

abstract Null<T>

The Null<T> type is now @:coreType abstract instead of a typedef to T. It still supports implicit conversion from and to T, as well as forwarding any field access to T, so its basic usage shouldn't be affected at all.

However, this change affects macros. Because it's not a typedef now and it won't be matched by the TType constructor. Instead it's now represented as a TAbstract instance, so if a macro is handling Null specifically, it needs to be changed to match TAbstract instead of TType. Also if the macro is handling TAbstract, e.g. by recursing into its underlying type, an additional match for the Null case might be needed, unless it's already handling abstracts with the @:coreType metadata.

The reason for this change is mainly to cleanup the inconsistency and confusion about Null within compiler and macro source. That is, on most static platforms, the nullable type is often different than a non-nullable type, for e.g. basic types (Int,Bool,etc), so having Null<T> represented as a mere type alias (which is what typedef is) leads to bugs and general confusion. Moreover, having an abstract Null<T> type sets the ground for possible future exploration regarding null-safety features, because with abstract types we can control implicit casting and variance easier.

Related PR:

New typed AST node: TEnumIndex

A new constructor were added to the haxe.macro.Type.TypedExprDef enum: TEnumIndex(e1:TypedExpr). It represents access to a enum index value and is generated by the pattern matcher.

Before this node was introduced, the pattern matcher was generating a (potentially inlined) Type.enumIndex call that was hard to distinguish later in the optimization filters and generators.

Related PR:

New Typed AST node: TIdent (TODO: TUnbound?)

Identifiers that are not bound to a known field or variable are now represented by a special haxe.macro.Type.TypedExprDef constructor: TIdent(s:String). These identifiers are most often introduced by the use of untyped (e.g. untyped __js__).

Before this node, such identifiers were represented by a TLocal node pointing a fake local variable with @:unbound metadata. This solution wasn't perfect, because compiler/macro developer would expect TLocal to point an actually declared variable, so having to check whether a given local variable is unbound every time is error-prone.

Related PR:

New macro interpreter

The interpreter used for running scripts and executing macros has been rewritten for better performance and maintainability. There are two things to consider regarding backward compatibility in this change:

First, the target name for the interpreter is now eval, not neko, so if the code contains #if neko conditional compilation implying that it also affects interpreter, that has to be changed to #if eval or #if (neko || eval). The macro define is of course still present when running macros.

Second, because of the new target now has a JIT-compilation step, it's a bit more strict about compiling code that was meant for another target. For example, if a module with your macro function also contains a method with code like untyped __js__, the previous interpreter would not complain about it unless you actually execute that method. The new interpreter will fail at JIT-compilation stage. To fix this, the code that is not meant to run on eval should be skipped, for example by fencing it with #if <target-define> or #if !macro, or by moving it into a separate module that won't be loaded in the interpreter context.

Related PR:

Related blog post:

Unicode-aware lexer

Haxe now expects the source files (.hx) to be UTF-8 encoded and positions reported and expected by the compiler now contain character offsets instead of byte offsets.

This is an important change for IDE support maintainers: all display query offsets and all reported positions must be treated as character-based rather than byte-based.

This makes haxe report proper positions for source files containing multi-byte characters, such as Cyrillic, CJK and others. This also should make it easier for editors to integrate with haxe ide services, since they mostly work with character offsets as well. If the editor has the handling position conversion by e.g. encoding/decoding the file on-request, this should be disabled for Haxe 4.

Related issue:

Related PR:

If your IDE have Haxe 3.x support, you can add -D old-error-format to keep compiler completion working using previous behavior. Other advanced completion services will not work with bytes position.

1-based column numbers in compiler errors and warnings

Haxe now reports both line and column number starting from 1. This is more consistent and is what a lot of editors expect when processing build output for capturing error messages.

If you're parsing the position string (e.g. main.hx:3: characters 17-25) for further processing, please note that characters numbers are now 1-based. This can be disabled with -D old-error-format compiler argument.

Related PR:

New syntax: arrow functions

Haxe 4 now supports short arrow function syntax in the form of (a, b) -> a + b which is equivalent to function(a, b) return a + b.

While it's an obvious feature for language users, IDE maintainers will need to add support for this new syntax. In short:

// no args
() -> expr

// with args
(arg1, arg2) -> expr

// args with type hints
(arg1:Type1, arg2:Type2) -> expr

// return type hint is not supported for arrow functions due to syntax ambiguity,
// one can use type-check syntax in the function expression
(arg1:Type) -> (expr:ReturnType)

// single-arg-no-type-hint convenience special case (no parenthesis required)
arg -> expr

Related proposal:

New function type syntax

In addition to the old Int->Void function type syntax, Haxe 4 now has new syntax for specifying function types with support for argument names.

// no args
() -> Void

// unnamed args
(Int, String) -> Bool

// named args
(name:String) -> Void

// named, unnamed and optional args mixed
(arg1:Type1, ?arg2:Type2, Type3) -> Ret

Related proposal:

haxe.unit removal

The classes in haxe.unit have been removed. They are still available from the hx3compat library. For more elaborate unit testing frameworks, consider using utest or munit.

haxe.remoting removal

The classes in haxe.remoting have been removed. They are still available from the hx3compat library.

List to Array

Some occurrences of List in the standard library have been replaced with Array. This might require some small changes in user code.

haxe.xml.Fast is now an abstract

While usage remains the same, it is no longer possible to use haxe.xml.Fast as a class (e.g. extending it or using reflection on it).

Syntax change for multiple parameter constraints

The syntax T:(A, B) is no longer supported and has been replaced by T : A & B. We decided to break this because there was a syntactic conflict.


Static extension restrictions

  1. Static extensions no longer resolve on an implicit this type. This was a little-known feature which could affect resolution order in some instances (related:
  2. Static extensions no longer apply abstract field-casts. (

Function expressions as "operands"

The function expression no longer checks if it is used as an operand in cases such as function() {} + 1. This change was made to avoid syntactic ambiguities an some other issues, such as function() {} (expression) being treated as a call. If a function is meant to be used this way, it can be wrapped in parentheses: (function() {}) + 1

Signature of EFunction constructor of haxe.macro.ExprDef enum

Signature of EFunction constructor of haxe.macro.ExprDef enum has been changed.

In Haxe 3 it was EFunction(name:Null<String>, f:Function).

New signature is EFunction(kind:Null<FunctionKind>, f:Function), where FunctionKind is the following enum:

enum FunctionKind {
    /** Anonymous function */
    /** Named local function */
    FNamed(name:String, ?inlined:Bool);
    /** Arrow function */

Limitations to macro-generated identifiers

In Haxe 3 using macros it was possible to generate any invalid identifier for a type, field or variable.

Since Haxe 4 macro-generated identifiers are checked by the compiler to be valid identifiers.

For a full definition of valid identifiers refer to

String literals representation in syntax tree

In Haxe 3 string literals were represented in abstract syntax tree as EConst(CString(str:String)).

Since Haxe 4 CString constructor got second argument: CString(str:String, ?kind:StringLiteralKind) where StringLiteralKind allows to distinguish single-quoted and double-quoted strings:

enum StringLiteralKind {
  /** Strings enclosed in double quotes (e.g. "hello world") */
  /** Strings enclosed in single quotes (e.g. 'hello world') */

Abstract types as values

Classes can be passed somewhere as a value of Class<T>. E.g. var stringClass:Class<String> = String.

In Haxe 3.4 it was possible to pass abstract types as values too. E.g. var v:Dynamic = MyAbstractType.

However that was a regression since 3.3.0-rc.1 because abstract types don't exist at runtime. Haxe 3.2.1 did not allow to use abstracts as values. A compile-time error was being emitted in such cases: "Cannot use abstract as value".

In Haxe 4 that error is restored.

Cannot use Void as value

Since Haxe 4 it's impossible to pass the result of a Void function somewhere. E.g. this was possible in Haxe 3:

[1, 2, 3].map(function(i):Void { 

Since Haxe 4 such code causes an error: "Cannot use Void as value".

Use for-loops instead:

for(i in [1, 2, 3]) {

Native properties for Flash target

The Flash property support was reworked in (pull request).

It's now required to specify a proper Haxe property and its accessor functions in the extern class as well as mark it with the metadata:

extern class FlashClass { var enabled(get,set):Bool;
  private function get_enabled():Bool;
  private function set_enabled(v:Bool):Bool;

These accessor functions can be overriden just like normal Haxe functions:

class HaxeClass extends FlashClass {
  override function get_enabled():Bool {
    var result = super.get_enabled();
    return result;

@:setter(propertyName) and @:getter(propertyName) are not needed anymore.

Switching on this in member methods of enum abstracts

Since Haxe 4 this is not implicitly promoted to the enum abstract type for pattern matching inside of member methods of enum abstracts.

enum abstract Volume(String) {
  var Loud = "loud";
  var Quiet = "quiet";
  public function beep()
    switch this {
      case Loud:  // Error: Volume should be String

As a workaround use untyped cast and checktype syntax:

enum abstract Volume(String) {
  var Loud = "loud";
  var Quiet = "quiet";
  public function beep()
    switch (cast this:Volume) {
      case Loud:  // Works

"operator" and "overload" keywords

operator and overload are new keywords reserved for future features and can no longer be used as identifiers.

You can’t perform that action at this time.