Evil Haxe is a modified version of the Haxe compiler that allows users to use and write parser mods written entirely in Haxe.
This is achieved by exposing the token stream used internally by the Haxe compiler to the Haxe macro runtime. A "mod" is defined in Haxe by providing callback functions to pre-defined hooks added to the parser.
To help you get started, this project provides some built-in mods as examples. You can check the list of them here, and you can view their source code here! Also check out this minimal example below:
import evil.macro.TokenStream;
// This is called using the new compiler argument:
// --conspire Mod.init()
function init() {
Evil.addParserMod("loop_mod", {
onExpr: function(stream: TokenStream): Null<haxe.macro.Expr> {
return switch(stream.peek().token) {
case Const(CIdent("loop")): {
stream.consume();
final expr = stream.nextExpr();
macro while(true) $expr;
}
case _: {
null;
}
}
}
});
}
// Let the Haxe compiler know this module wants to use Evil Haxe mods.
// We specify to only use the "loop_mod".
#evil(loop_mod)
package;
function main() {
// New "loop" feature created by "loop_mod".
var i = 1;
loop {
i *= 2;
if(i > 20) {
break;
}
}
trace(i);
}
The old version of Evil Haxe was an opinionated edit of the Haxe compiler I made about a year ago. It's a superset of Haxe that is created to add as many modern and cool features the language can possibly contain without completely breaking compatibility with vanilla Haxe. It was summoned from an alternative universe using a demonic ritual, and it continues to eat away at my soul from the darkest depths of our world.
Long story short, version 1.0 was just a collection of Haxe mods I made for fun. However with version 2.0, I want to try and make the project more accessible to others. To do this, I will:
- Keep all changes modular and allow features to be enabled/disabled like Babel/TypeScript.
- Create a compile-time Haxe API for making parser mods and use it to implement the majority of the mods.
- Enforce a header syntax to enable Evil Haxe in Haxe source files. This'll help mark code intended to be used with Evil Haxe vs normal Haxe to avoid confusion when reading source code.
- Branch from the latest stable release (post v5.0 release). This should prevent annoying maintenence to keep things compatible with the development branch.
Anyway, just setting this repo up to work on whenever I'm bored. Feel free to make requests in the Issues.
Please visit BUILDING to learn how to compile the Haxe compiler. The process is identical for Evil Haxe.
The main branch is an edit of Haxe's development branch, but if you wish to use Evil Haxe for Haxe 4.3, build using the evil-4.3 branch.
If building Haxe is too high above your skill level, I'm also committing the Windows 64-bit builds I'm testing into the repo. To install:
- Download the latest nightly of Haxe OR the latest version of 4.3-bugfix and install (or use an existing installation).
- Download the
evil_haxe.zip
from the main branch (nightly) or the 4.3 branch. - Extract the contents of evil_haxe.zip and paste into the Haxe installation folder (
std/
folder should be merged). - Run the
evil_haxe
executable like you would with the Haxe compiler normally.
Every Haxe module that wants to use Evil Haxe mods must use #evil
at the top of the file. This should be placed above everything, including package
.
If not provided any arguments, the default mods will be enabled. To configure which mods are the default mods, the new --default-mods
compiler argument can be used:
--default-mods loop,pipe,pow
Otherwise, the mods used by the Haxe file can be specified by the #evil
statement:
#evil(loop, pipe)
To create a parser mod, make a new Haxe module with a static function. Call this function using the new --conspire
argument in the Evil Haxe compiler. This will run the function at an extremely early point in compilation, before initialization macros are even run!
Within this function, mods can be registered using Evil.addParserMod
. The first argument is the name. This should be a unique identifier and is used to enable the mod with the #evil
attribute.
// This mod would be enabled with: #evil(my_mod)
function init() {
var options = { ... };
Evil.addParserMod("my_mod", options);
}
The options
object should be a structure matching the evil.macro.Mod
typedef. All of its fields are optional callbacks that will execute at certain points of source code parsing.
For example, onExpr
will execute whenever the compiler wants to parse an expression. It will first run all the onExpr
functions provided by the enabled mods. The first to return a non-null Expr
will have its return used instead of whatever would've been parsed normally. If all the mod functions return null
, the compiler continues to parse the expression like normal.
typedef Mod = {
?onExpr: (TokenStream, Bool) -> Null<Expr>,
?onAfterExpr: (TokenStream, Expr) -> Null<Expr>,
?onBlockExpr: (TokenStream) -> Void,
?onAfterBlockExpr: (TokenStream, Expr) -> Null<Expr>,
?onTypeDeclaration: (TokenStream, evil.macro.TypeDeclCompletionMode) -> Null<TypeDefinition>,
?onClassField: (TokenStream, Bool) -> Null<Field>,
?tokenTransmuter: (TokenType) -> Null<TokenType>
}
Evil Haxe also contains pre-existing mods. These can be found in the standard library at std/evil/mods
.
This mod adds the **
(exponentiation) operator from JavaScript/Python. For example, with A ** B
it returns the value of A
to the power of B
. Internally, it converts the expression into Math.pow(A, B)
.
trace(2 ** 3); // 8
This mod adds the Kotlin feature for adding a trailing block to an expression to act as a final-argument lambda.
function add(num: Int, mod: (Int) -> Int) {
return mod(num) * 2;
}
add(10) { n -> n * 2; } // 40
This mod enables the pipe operator similar to what's seen in many functional languages:
2 |> Math.pow(5) |> trace; // 25
This mod adds a shorthand for writing function bodies based on Kotlin. A function declaration can be "assigned" its return expression.
function half(num: Float) = num / 2.0;
half(12); // returns 6
This mod enables the defer
keyword based on Go.
Deferred expressions are moved to the end of their block scope.
// Hello world
defer trace("world");
trace("Hello");
This mod adds a shorthand for writing Null<T>
types as T?
.
final num: Int? = 123;
trace(num ?? 321);
This mod adds the fun
keyword that acts as an alias for function
, and it adds the val
keyword as an alias for final
. This mod demonstrates the tokenTransmuter
property for mods.
// Valid Haxe with this mod enabled.
fun main() {
val a = 123;
trace(a); // 123
}