Using Haxe Macros to Inline Utility Functions at Compile Time
Learn how to write a Haxe macro that replaces a function call with its body during compilation, eliminating runtime overhead and producing cleaner JavaScript output.
26 Feb 2026, 12:32 UTC

Problem: Repeated utility calls bloat the generated JavaScript
When you write small utility functions in Haxe—like a vector addition or a format helper—each call becomes a real function call in the emitted JavaScript. For performance‑critical code or when targeting size‑constrained environments, those extra calls add unnecessary overhead and make the output harder to read.
Thesis: A compile‑time macro can inline those utilities, removing the call while keeping the source code clean and type‑safe.
How Haxe macros work
Haxe macros run during compilation. They receive the abstract syntax tree (AST) of the code they annotate, can inspect and modify it, and then return the transformed AST to the compiler. Because the transformation happens before any target code is emitted, the generated JavaScript (or any other target) contains only the final result—no macro runtime is needed.
Macros are hygienic by default: identifiers they generate are scoped locally and won’t clash with user code. To expose a name outside the macro you must declare it with var or use an #import directive.
Worked example: Inlining a vector‑add helper
Suppose we have a simple utility:
class MathUtil {
public static inline function addV(v1:Vec2, v2:Vec2):Vec2 {
return new Vec2(v1.x + v2.x, v1.y + v2.y);
}
}
Even with the inline keyword, the Haxe compiler may still emit a function call when the function is used across compilation units or when optimisation levels are low. A macro guarantees inlining.
First, define a macro that replaces a call to MathUtil.addV with the body of the function:
import haxe.macro.Context;
import haxe.macro.Expr;
class InlineAddV {
public static macro function addVCall():Expr {
// The macro expects the expression: MathUtil.addV(v1, v2)
var args = Context.getLocalMacroArgs(); // [v1, v2]
if (args.length != 2) {
Context.error('addV macro expects exactly two arguments');
}
// Build the inline Vec2 constructor call
return macro :new Vec2($args[0].x + $args[1].x, $args[0].y + $args[1].y);
}
}
Now annotate the utility with the macro:
class MathUtil {
public static macro function addV(v1:Vec2, v2:Vec2):Expr {
return InlineAddV.addVCall();
}
}
When you write:
var result = MathUtil.addV(a, b);
the macro runs, receives a and b as arguments, and returns the AST for new Vec2(a.x + b.x, a.y + b.y). The compiler then emits JavaScript that looks like:
var result = new Vec2(a.x + b.x, a.y + b.y);
No call to MathUtil.addV
Verification steps
- Save the macro and utility in a file
MathUtil.hxand a main fileMain.hxthat uses the function. - Compile to JavaScript:
haxe -main Main -js output.js(you need Haxe 4.x or 5.x). - Open
output.jsand search foraddV. You should not find a function call; instead you see the inlinenew Vec2(...)expression. - To trace macro execution, add
-D macro-logand recompile withhaxe -D macro-log -main Main -js output.js. The compiler will print macro entry/exit messages, confirming the transformation ran.
Trade‑offs and limitations
- Debugging: Error messages point to the generated code, not the original macro call. Use the verbose macro flag or enable source maps (
-js-output-source-map) to map back. - Code bloat: If the inlined body is large and used many times, the output JavaScript can grow significantly. Measure the size after compilation.
- Purity: Macros should avoid side‑effects (e.g., file I/O) because they run at compile time and may be executed multiple times during incremental builds.
- Compatibility: The macro API is stable across Haxe 4.x and 5.x, but macros written for Haxe 3 may need minor adjustments for new
haxe.macroutilities.
Actionable closing
If you have utility functions that are small, pure, and called frequently, consider wrapping them in a macro as shown. This gives you the readability of a function call with the performance of inline code, without adding any runtime dependency. Start with a single utility, verify the generated output, and then expand the pattern to other helpers where the trade‑off favors compile‑time inlining.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.