Haxe Macros: Code completion for everything

Haxe

This post is about Haxe macros, providing us smarter code-completion. In this post we create a class that gives completion automagically based on files on disk, using Haxe macro scripting. This gives us development power, less error-prone code and smaller JavaScript output.

Most developers will recognize this type of class:

class FileNames {
  public static inline var BACKGROUND:String = "background.jpg";
  public static inline var BALL:String = "ball.jpg";
  public static inline var CAR:String = "car.jpg";
}

Why are we using this?

It’s a common class with static references. There are a lot of variations on this class, think of FileNames, XMLNames, AssetNames, URLNames, LibraryNames, FontNames etc.. You name them. All these type of classes have mostly thing in common; they point to static things that refers mostly to info on disk or inside a XML- or JSON-file or something.

You use this type of class to have a reference to a thing on disk, but have it stored in one class to have code-completion on it, make refactoring/searching easier. It’s also inlined which basically means the values are pasted-in-place in the final output.

Why are we writing this manually?

Good question! To be honest, I dislike doing things manually. We are using a computer, so it should help us. I’d like to get warnings when I code something wrong or when I point to things that are removed. Could it possible to fill this class automatically with properties and get warnings when a file or node in XML/JSON-file does not exists anymore?
When you think about it, most information is already available, the content is mostly inside files.

Haxe Macros

The Haxe macro system allows powerful compile-time code-generation without modifying the Haxe syntax.

😮 What does this mean? In simple words, macros allows you to manipulate you classes when the actual build of your code happens (compile-time). Macros are really just Haxe programs which run while compiling. Haxe is very open. Macros can be used to add properties, to generate pieces of code or even to build complete new classes. Does this sounds helpful or confusing? Coming from an Actionscript background, this concept for me is new and somehow vague.

But.. Let’s try to learn something. Haxe compiler can access your harddisk. That means while the code is compiling, you can run your own code (macros), have ultimate control over classes and also access things like the file-system. Another fun fact, the code completion (in VSCode and HaxeDevelop) under the hood is also provided by Haxe, it is calling a Haxe build command. This is because of the high number of Haxe features and things like type inference; code editors cannot easily handle completion by just parsing the Haxe files.

Getting smart auto-completion in Haxe

So, the Haxe compiler can access our file-system and the auto-completion is using the compiler. With all this info, it should be possible that we write some rules to get smarter auto-completion, right? Imaging that we want the FileNames class (first code example) to be automatically filled with properties, which reflect the files from certain folder on disk. But without writing it manually 😮

Well, to start, the class should be empty 🙂 The computer is wise enough to provide you that info. Let’s clear the class of the first example!

@:build(FileNamesBuilder.build("assets/"))
class FileNames {
  // Ha! Nothing in here!
}

Okay, the class empty.

Second thing we need to do: something needs access to this class and provide the properties. You probably noticed the @:build() over there. That is a magic key to access macro scripting for this class, it’s a meta-tag. This meta-tag expects a reference static function build() from a class called FileNamesBuilder. I also provided ‘assets/’ as directory to get the information from.

Let’s see how this builder class could look like. I placed it in a separate file (FileNamesBuilder.hx), to avoid confusion between normal functions and macro functions. This differences is important to make, since they are for different times (compile-/runtime).

import haxe.macro.Context;
import haxe.macro.Expr;

class FileNamesBuilder {
  public static function build() {
    var fields = Context.getBuildFields();
    return fields;
  }
}

What happens over here? The haxe.macro.Context class knows things of your class. With ‘your class’ I mean ‘FileNames’ class, since that is the current context where it is doing things upon. At the moment it get the current fields (properties, variables) from the class, and returns them (This function has a given return type). In our case you and me know there are no fields, but lets keep it clear; if there were any or if we add them manually, they will be remained.

What is a field in Haxe?

The docs are not very clear on what a Field is, but once you know how to use it, it’s ok I guess. Basically a field is what any variable is of a class. You can create them using this class. Not to go into deep programming philosophy, but in Haxe everything in the end is an expression, and a Field is a specific expression to define properties.

In my FileNamesBuilder class, I used this to create a Field:

fields.push({
  name: fileRef.name,
  doc: fileRef.documentation,
  access: [Access.APublic, Access.AStatic, Access.AInline],
  kind: FieldType.FVar(macro:String, macro $v{fileRef.value}),
  pos: Context.currentPos()
});
  • The name property is the variable name. Note, this may not contain all type of chars, so in my builder I replaced some chars like a dot or minus sign to underscores.
  • The doc property (optional) is to provide documentation. IDE’s like FlashDevelop use this.
  • The access property (optional) is to set the access of the new field. Options are (Public,APrivate,AStatic,AOverride,ADynamic,AInline or AMacro) as found in found in haxe.macro.Expr.Access. I wanted to have a public static inline variable, so should be pretty clear what is in the list.
  • The pos property actually defines the position, which is displayed in error messages. Context.currentPos() mostly does the trick.
  • The kind property is to set the field type but also to set the actual value, since both go hand in hand. I set this to String, with as value the name of the file as String. The syntax is kinda weird on this, it’s called reification escaping. More on this topic can be found over here. I am also not completely familiar with the syntax, but try to understand what’s happening. FVar (is an Haxe enumerator) defines a variable field, whereas FFun defines a method and FProp defines a property. With real syntax you can define fields which are variables, properties or methods, so naturally the kind can be one of the three.

To sum it all up: I came up with this FileNamesBuilder class:

/**
 * @author Mark Knol [blog.stroep.nl]
 */
import haxe.macro.Context;
import haxe.macro.Expr;
import sys.FileSystem;

class FileNamesBuilder {
  public static function build(directory:String):Array {
    var fileReferences:Array = [];
    var fileNames = FileSystem.readDirectory(directory);
    for (fileName in fileNames) {
      if (!FileSystem.isDirectory(directory + fileName)) {
        // push filenames in list.
        fileReferences.push(new FileRef(fileName));
      }
    }

    var fields:Array = Context.getBuildFields();
    for (fileRef in fileReferences) {
      // create new fields based on file references!
      fields.push({
        name: fileRef.name,
        doc: fileRef.documentation,
        access: [Access.APublic, Access.AStatic, Access.AInline],
        kind: FieldType.FVar(macro:String, macro $v{fileRef.value}),
        pos: Context.currentPos()
      });
    }

    return fields;
  }
}

// internal class
class FileRef {
  public var name:String;
  public var value:String;
  public var documentation:String;

  public function new(value:String) {
    this.value = value;

    // replace forbidden characters to underscores, since variables cannot use these symbols.
    this.name = value.split("-").join("_").split(".").join("__");

    // generate documentation
    this.documentation = "Reference to file on disk \"" + value + "\". (auto generated)";
  }
}
haxe3 macro filenames autocompletion

Thanks to inlining, the Javascript output is very small, the FileNames class does not even exists as actual class in the final output 🙂

(function () { "use strict";
  var Main = function() { }
  Main.main = function() {
    console.log("background.jpg");
  }
  Main.main();
})();

Note; I use -D js-flatten -dce full in my compiler arguments.
In HaxeDevelop goto ‘Project’ > ‘additional compiler arguments’ to have the inlining and removal of dead code enabled. This gives very small JavaScript output.

Conclusion

Haxe is a very powerful language since it is open and you can access everything, and even do things while building.

  • Macro scripts are a bit hard to understand at first sight, but when you slowly understand, it gives a lot of extra development power.
  • With smart use of macro scripting, you can get auto-completion on almost anything. In our example we only read a directory, but we can even parse XML/JSON-files and grab information out of that.
  • Having completion on it also means that when a thing is removed from disk, you get a compile error.
  • Of course, this is only a usage for code completion, but you can use the same trick to add fields/properties to any class or type.
  • Using inlining, you have no static instances in a separate class in the final output, which is extreme useful for mobile development.

I hope you enjoyed this post!

7 responses to “Haxe Macros: Code completion for everything”

  1. existen says:

    Awesome! FlashDevelop + macroses = fun)

  2. Cambiata says:

    Thanks a lot! Great read!

  3. Michal says:

    Hi,
    I’ve done somthing like that to “emulate” haxe.xml.Proxy class (in order to set dynamically the file ‘s path…). It works fine setting “static” fields but if you try to set instance’s variables, it doesn’t show the completion.

  4. Michal says:

    Again,
    I’ve just updated to latest git haxe version, it works fine now (I suppose 3.1.0 works fine too) 🙂

  5. Kent Larsson says:

    Interesting read! It was my first wow in the Haxe macro world. 🙂

  6. Iskren Stanislavov says:

    I want to create a code-completion macro for JSON-like object comming from JS.
    They are already parsed as json.parse(jsonData).
    What I miss is how to create the Object(Object(Object)) and Array<Array> cases.
    Would you please give me more information on how to create the kind for those two cases
    fields.push({
    “name”: childName,
    “pos” : _pos,
    //”kind”: FVar(macro:Dynamic) – this is not gi
    //”kind”: FVar(TAnonymous(generateSubFields2(child))),
    //”kind”: TAnonymous(generateSubFields2(child)),
    //”kind”: childType.toString(),
    //”kind”: FVar(macro {childType.toString(); }),
    //”kind”: FVar(macro Array)
    “kind”: FVar(macro: Array)
    });

  7. jred_kai says:

    Amazing post!
    p.s: Never gonna let you down!

Say something interesting

Please link to code from an external resource, like gist.github.com.