Haxe 4.3.0 is here! Here's what you need to know.

[ development  haxe  ]
Written on April 6, 2023
Reading time: 25 min

Haxe finally released the long-awaited 4.3.0 update today, bringing with it not just many new bug fixes, but also brand new language features you can use in your own projects. Haxe 4.3.0 sees a lot of improvements for those using abstracts, type parameters, multi-threading, and macros, as well as some new syntax which will be incredibly useful in any project.

I tried to include more detail than the original release notes, to make sure people understand each feature without having to dive into individual pull requests.

Haxe 4.3.0 is currently available on Haxe.org.

New Language Features (All Targets)

Here, I’ve taken each of the new language features and provided a more in-depth explanation of how they work, along with a demo in try.haxe.org (which has 4.3.0 now!). Please check out the demos to get a better idea of what I’m talking about with each entry!

support defaults for type parameters

  • Try Haxe: Demo
  • Haxe Evolution: Read the approved proposal
  • This language feature is related to: Haxe Manual: Type parameters
  • Alongside using <T:Foo> to constrain a type parameter to only allow types extending Foo, you can now specify a default type parameter, used when the type parameter is not specified.
  • If Foo is declared as class Foo<T=String>, then var test:Foo; will be equivalent to var test:Foo<String>;, but not var test:Foo<Int>.
    • Without a default type parameter, var test:Foo; will throw a compilation error.
  • Default types can be used alongside constraints like so: class Bar<T:String=String>
    • Specifying the default parameter first and attempting to constrain it second will not work.

support @:op(a()) on abstracts

  • Try Haxe: Demo
  • Haxe: Read the original issue
  • This language feature is related to: Haxe Manual: Operator overloading for abstracts
  • In the same manner that abstracts could override operations like + and - before, you can now override the function call operator () to essentially make an abstract that acts like a callable function.
  • Since this operation override resolves based on the parameters provided to it, you can have multiple functions annotated with @:op(a()) and the correct one will be chosen. Note the functions still have to be named different on the abstract itself.
  • This acts a lot like function overloading, a language feature used commonly in Java and C#.

support abstract keyword to reference the abstract

  • Try Haxe: Demo
  • Haxe Evolution: Read the approved proposal
  • This language feature is related to: Haxe Manual: Abstracts
  • Normally, within an abstract, the this keyword points to the value of the underlying type. This is what lets you initialize abstracts with this = value in the constructor.
  • However, this leads to the problem of referring to methods of the abstract itself, or attempting to pass the abstract itself as a value. Before 4.3.0, you would have to use (cast this:MyAbstract) to do this.
  • Now, the abstract keyword pulls double-duty; aside from being used to declare a class, it can also be used as an identifier within abstracts to refer to the abstract itself.
  • For example, for an abstract adding the function foo(), this.foo() would fail, but abstract.foo() will call the correct function.

support static var at expression-level

  • Try Haxe: Demo
  • Haxe Evolution: Read the approved proposal
  • This language feature is related to: Haxe Manual: Static access modifier
  • You can now initialize static variables within a function. This essentially acts like a static variable of the class, but bound to the scope of the function.
  • For example, you can put static var x = 0 at the beginning of a function, and this creates a static variable named x which is only accessible from within that function.

Some great example use cases for this include:

  • Caching values. You can do something like this while having the assurance that no other functions in your class will accidentally modify foo and break it.
    public static function getFoo():Void {
      static var x:Foo = null;
      if (x == null) x = new Foo();
      return x;
    }
    
  • If you have a native function reference, you can load it once, lazily, while making it inaccessible without calling the wrapper function.
  • If you have a function which recursively calls itself, you can define a variable to keep track of the depth and return a fallback if you are too deep.

support ?. safe navigation operator

  • Try Haxe: Demo
  • Haxe Evolution: Read the approved proposal
  • This feature adds a brand new operator with new syntax to the language.
  • Typically, calling foo.bar when foo is null throws an error.
    • To resolve this, you would have to either check that foo is null with a conditional statement, enable Null Safety, or use a try/catch block.
    • This problem gets exceptionally more annoying if you are trying to access foo.bar.baz.bat.ball.bin. Checking nullability with conditions is a pain in the ass, rewriting your application to enable null safety may be difficult or even impossible (what if foo is the output of a library you don’t control the code for?), and a try/catch block is still pretty inconvenient.
  • You can now use foo?.bar, which will return null if foo happens to be null.
    • You can now do var result = foo?.bar?.baz?.bat?.ball?.bin; to skip past all those null checks without crashing. If any of foo, bar, baz, bat, ball, or bin is null, result will be null.
  • This is especially powerful in combination with the new null coalescing operator, covered next.

added ?? null coalescing operator

  • Try Haxe: Demo
  • Haxe Evolution: Read the approved proposal
  • This feature adds a brand new operator with new syntax to the language.
  • This condenses a null check conditional statement or ternary into a single operator.
  • The expression value ?? "NULL VALUE" resolves to the string "NULL VALUE" if value is null, and whatever the value of value is otherwise.
  • This complements the safe navigation operator incredibly well.

add -w compiler option to configure warnings

  • This is not available on Try Haxe due to it being a compiler option.
  • Haxe: Read the merged pull request
  • Added the new metadata @:haxe.warning, which takes a String. You can use this to enable or disable compiler warnings.
  • For example, @:haxe.warning('-WVarInit') before a function will suppress all WVarInit warnings within that function.

added new error reporting modes

  • This is not available on Try Haxe due to it being a feature for compilers and IDEs.
  • Haxe: Read the merged pull request
  • This adds a new compile define, which can change how compile errors display to the user.
  • -D message-reporting=classic is the default reporting mode, which uses the current output:
  • -D message-reporting=pretty utilizes new formatting for errors:
  • Use -D message-reporting=pretty -D no-color if your terminal doesn’t support ANSI escape codes for colors.
    [ERROR] Main.hx:8: characters 9-18
     8 |         C.f("hi");
     |         ^^^^^^^^^
     | Could not find a suitable overload, reasons follow
    
        | Overload resolution failed for () -> Void
         8 |         C.f("hi");
           |             ^^^^
           | Too many arguments
    
        | Overload resolution failed for (t : f.T) -> Void
         8 |         C.f("hi");
           |             ^^^^
           | Constraint check failure for f.T
           | String should be Int
           | For function argument 't'
    
  • -D message-reporting=indent provides a similar format to the classic mode, but while using the newly provided indentation. This can be useful for IDEs.
    $ haxe compile-fail.hxml -D message-reporting=indent
    Main.hx:8: characters 3-12 : Could not find a suitable overload, reasons follow
    Main.hx:8: characters 3-12 : Overload resolution failed for () -> Void
      Main.hx:8: characters 7-11 : Too many arguments
    Main.hx:8: characters 3-12 : Overload resolution failed for (t : f.T) -> Void
      Main.hx:8: characters 7-11 : Constraint check failure for f.T
        Main.hx:8: characters 7-11 : String should be Int
        Main.hx:8: characters 7-11 : For function argument 't'
    
  • -D messages-log-file=[path] allows you to define a file to output these messages to.
  • -D messages-log-format=[format] allows you to choose a different log format for the log file than from the displayed output.
    • For example, you can display pretty output to the user and store indent output to a file for the IDE to parse.
  • In the future, VSHaxe will be updated to make use of these new reporting modes.

support custom metadata and defines

  • This is not available on Try Haxe due to it being a feature for compilers and IDEs.
  • Haxe: Read the merged pull request
  • This allows users to define custom metadata and compile defines to the compiler, for the purposes of providing documentation and completiong.
    • Actually implementing these metas and defines into code is still done via macros, in the same manner as previously, but this provides better IDE support for custom defines.
  • The new haxe argument --help-user-metas allows you to print the documentation for all user-defined metadata in the current compilation context.
  • The new haxe argument --help-user-defines allows you to print the documentation for all user-defined defines in the current compilation context.
  • The new function haxe.macro.Compiler.registerCustomMetadata allows you to add documentation for a single metadata.
  • The new function haxe.macro.Compiler.registerCustomDefine allows you to add documentation for a single define.
  • The new function haxe.macro.Compiler.registerMetadataDescriptionFile allows you to pass documentation for multiple metadatas via a file path.
  • The new function haxe.macro.Compiler.registerDefinesDescriptionFile allows you to pass documentation for multiple defines via a file path.

Just a reminder, ALL of the above features work in EVERY Haxe target. You can use these features when targeting C++, JavaScript, Java, Python, Lua, or whatever target language the Haxe compiler supports.

Standard Library Improvements

There have been many improvements to classes in the standard Haxe libraries:

  • haxe.ds.Vector has a new function fill(T) which sets every element in the Vector to the provided value.
  • haxe.ds.Vector can now be instantiated using either new Vector(length) or new Vector(length, defaultValue) to automatically call fill() after instantiationn.
  • New class sys.thread.Condition for threaded targets.
    • A condition includes functionality which in other languages may be known as a Lock. It has functions to acquire, release, signal, or broadcast.
    • This thread syncronization tool allows only a single thread to access a block of code, preventing other threads from accessing until it is safe to do so.
    • For example, you can wait until an object is written to a map on one thread before reading it on another thread.
    • An analogy for this is like a bathroom key in a school.
    • This is useful for creating thread-safe, multi-platform code.
  • New class sys.thread.Semaphore for threaded targets.
    • A semaphore is a lock which allows a specific number of threads to enter a block before waiting, rather than just one.
    • This thread syncronization tool allows only a specific number of threads to access a block of code, preventing other threads from accessing until it is safe to do so.
    • For example, you can create a pool of connections of limited size, and essentially create a queue which waits for connections to be freed before
    • An analogy for this is like a bouncer at a club, keeping occupancy within the limit despite multiple other guests attempting to enter via other threads.
    • This is useful for creating thread-safe, multi-platform code.
  • New function sys.Http.getResponseHeaderValues allows for retrieving header values when a response contains multiple headers of the same name.
  • Sys.environment() now returns a copy of the array, rather than an array which can be modified in place.
    • This makes the function’s behavior consistent across platforms.
  • Sys.putEnv(name, value) now consistently deletes an environment variable when value is null.
    • This behavior was previously inconsistent across platforms.

Macro Improvements

There have been a lot of improvements and new functions for macros too:

  • Certain functions of haxe.macro.Context will now throw a warning if they are used within an initialization macro when they shouldn’t be, and when they are used outside an init macro when they shouldn’t be.
    • For example, haxe.macro.Context.typeof(expr) will throw a warning if from an initialization macro.
  • The new function haxe.macro.Context.onAfterInitMacros(Void->Void) allows you to define a callback function, invoked after initialization macros are done and as typing begins.
    • This is used to delay typing-dependant code from your initialization macros.
  • Fun fact if you’re reading this, haxe.macro.Compiler.signature(v:Dynamic) gives you an MD5 signature for a Haxe object. It’s existed for a while, but isn’t documented for whatever reason. Not related to the update just thought that was interesting.
  • The new function haxe.macro.Context.getMacroStack() returns an Array<haxe.macro.Expr.Position> containing the full call stack for the current macro. This can be useful for the purposes of producing improved error messages.
  • The new function haxe.macro.Context.initMacrosDone() returns true if configuration macros are done and parsing/typing can start. This is used to separate configuration macros from typing macros better.
  • haxe.macro.Context.makeExpr(value:Dynamic) now allows taking Maps as arguments.
  • haxe.macro.Context.getConfiguration() gives you access to a CompilerConfiguration, which is a standard means of accessing values such as:
    • The current version of the Haxe compiler.
    • A list of all the arguments passed to the compiler, either via the command line or via hxml file.
    • Whether --debug is set.
    • Whether --verbose is set.
    • The current target platform.
    • The compilation configuration for the target platform (see below)
    • The path for the current main class.
    • A list of access rules for certain packages (for example, packageRules.get("java") returns Forbidden on the Python target).
  • Added PlatformConfig, accessible via getConfiguration(), providing many details about the current compilation target’s configuration.
    • staticTypeSystem is true if Int, Float, Bool are not-nullable basic types on this target platform.
    • sys is true if this platform has access to the sys package.
    • overloadFunctions is true if this platform supports overloaded functions natively (for example, the Java and C# targets use this to allow overloads in externs).
    • reservedTypePaths specifies type paths reserved by the target platform.
    • supportsFunctionEquality is true if the platform supports being able to function == function.
    • supportsThreads is true if the target platform supports threads
    • supportsRestArgs is true if the target platform supports ...rest in function arguments.
    • There are many other useful values available in this, check the docs.
  • The new function haxe.macro.TypeTools.toModuleType lets you convert a haxe.macro.Type to a haxe.macro.Type.ModuleType
  • The new function haxe.macro.TypeTools.fromModuleType lets you convert a haxe.macro.Type.ModuleType to a a haxe.macro.Type
  • The new function haxe.macro.Context.getMainExpr() returns an Expr which contains a call the application’s Main function, if it exists.
    • This function will only return a non-null result from haxe.macro.Context.onGenerate or haxe.macro.Context.onAfterGenerate
  • The new function haxe.macro.Context.getAllModuleTypes() returns an array of haxe.macro.Type.ModuleTypes to be generated in the output.
    • Be aware that modifying this array does nothing, and may change over time; it is only considered complete during the generation phase.
  • The new function haxe.macro.Context.withImports<X>(imports:Array<String>, usings:Array<String>, code:()->X).
    • This allows you to execute the given function, while temporarily adding the specified import and using statements to that code’s context.
    • These imports/usings do not affect any code run afterwards, even if code throws an exception.
    • The function returns the return value of the function. If you want to pass arguments into code, use .bind() to create a new function with no parameters.
  • The new function haxe.macro.Context.withOptions<X>(options, code:()->X)
    • This allows you to execute the given function, while temporarily modifying some compiler options.
    • The compiler is restored to default behavior afterwards, even if code throws an exception.
    • The options available include:
      • allowInlining to enable or disable inlining during typing with typeExpr
      • allowTransform to disable some abstract type transformationns. The typed code will be almost exactly the same as the input code.
  • The new function haxe.macro.Context.makeMonomorph creates a new TMono type, which can be used with Context.unify to make the compiler bind the monomorph to an actual type.
  • The new compiler option -D eval-print-depth=### allows you to set the maximum depth when attempting to print an eval.
    • This is useful if you are debugging, and want to look deep inside a block of a macro statement.
  • The new compiler option -D eval-pretty-print causes eval values to be indented when printing.
    • This massively improves the experience of lookinng at deep eval blocks, especially when eval-print-depth is increased.

Target-specific Improvements

  • [JVM] The --jvm option now supports taking a directory as an argument, rather than a JAR file path.
    • This will export compiled Java .class files into the folder, rather than zipping them up.
  • [CPP] Externs now support type parameters.
  • [LUA] Now includes an implementation of SSL.
  • [JAVA] Now automatically applies @:java.default to methods with default implementations while loading JARs.
  • [JAVA] Experimental support for functional interfaces. Notably, the functional interface does not need to be an extern to a Java functional interface.
// NOTE: This code only works on the Java target.
// A similar result can be achieved on other targets by using abstracts.

interface MathOperation {
	function perform(a:Int, b:Int):Int;
}

class Ops {
	static public final add:MathOperation = (a, b) -> a + b;
	static public final subtract:MathOperation = (a, b) -> a - b;

	static public function performMathOperation(operation:MathOperation) {
		return operation.perform(8, 4);
	}
}

class Main {
	static function main() {
		var result = Ops.performMathOperation(Ops.add);
		trace('Add: ${result}');

		result = Ops.performMathOperation(Ops.subtract);
		trace('Subtract: ${result}');

		result = Ops.performMathOperation(multiply);
		trace('Multiply: ${result}');

		result = Ops.performMathOperation(function(a, b):Int {
			return Std.int(a / b);
		});
		trace('Divide: ${result}');
	}

	static function multiply(a, b):Int {
		return a * b;
	}
}

Bug Fixes and Improvements (All Targets)

I can’t cover all of the bug fixes and optimizations, as some of them are too boring or too obtuse for me, but I’ll try to cover some of them.

  • There have been a bunch of optimizations that should result in cleaner, faster code for each compilation target.
  • There have been a bunch of bug fixes across different targets.
  • Haxe will now guess the return type of a getter function based on the type of the property. Try Haxe
  • Haxe now assumes Null<T> for nullable values. This is useful when type safety is enabled. Try Haxe
  • Trying to add a dynamic function to an abstract now fails at compile time instead of runtime, and gives a more helpful error message. Try Haxe