Generate code from an expression using macros

I have been looking at certain places on the Internet to try and come up with some way in which to export Haxe code from the things you see on the screen in HaxeLive. I have identified that macros would be a very cool way of doing this, and by using the toString function to get the source of an expression.

I looked at these two examples:


And I thought I’d give using my SceneGen class a go by creating an Exporter class that takes the expression of using SceneGen.generate and returning the string results of this. But I have come across a few errors. It may be worth noting that the SceneGen class is probably not optimized for macros in mind. But here is the error message I am getting on compilation:

C:/Users/Luke/Desktop/Github/HaxeLive/hxlive/openfl/SceneGen.hx:58: characters 36-50 : Class<openfl.Assets> has no field getText
C:/Users/Luke/Desktop/Github/HaxeLive/hxlive/openfl/SceneGen.hx:65: characters 34-48 : Class<openfl.Assets> has no field getText
Invalid_argument("Array.make")

My Exporter class looks like this:

package hxlive.openfl; 
import haxe.macro.Expr;
using haxe.macro.Tools;

#if sys
import sys.io.File;
#end

class Exporter
{
    
    public static function export(data:Dynamic, file:String)
    {
        File.saveContent(file, get(data).toString());
    }
    
    macro public static function get(data:Dynamic):Expr
    {
        return macro $v { SceneGen.generate(data) };
    }

}

I’ve only just got started with macros, so if you’re one of those people who laugh at this code, I can probably understand that. How would I go about solving this issue, do you think?

EDIT; Since encapsulating Asset class usage to non-system targets, I’ve removed the getText error messages, but the last error message remains, and obviously I don’t know what Invalid_argument("Array.make") means.

macro $v{} doesn’t do what you want it to do. I’m not entirely sure what you want it to do, but I know it doesn’t do it. ($v{} processes only a single value, and the value has to be a basic type, such as a Bool, Int, or Float. I think Array is allowed too, but I’m not certain.)

Could you give an example input, and the output string you hope to get based on that input?

Ideally, I would want to generate a class that extends a Sprite, initialising and drawing the user interface you see on the screen at the time of the export. In a JSON file you may have something like this:

{
    "instanceType": "MainMenu",
    "name": "MyMainMenu",
    "contents": [
        {
            "type": "Text",
            "name": "_myText",
            "fontFile": "font/OpenSans-Regular.ttf",
            "fontSize": 11,
            "fontColor": "0xFF0000",
            "selectable": false,
            "text": "This is some text."
        }
    ]
}

Which would roughly translate to:

class MainMenu extends Sprite
{
    private var _myText:TextField;

    public function new()
    {
        _myText = new TextField();
        _myText.defaultTextFormat = new TextFormat(Assets.getFont("font/OpenSans-Regular.ttf").fontName, 11, 0xFF0000);
        _myText.embedFonts = true;
        _myText.selectable = false;
        _myText.text = "This is some text.";

        addChild(_myText);
    }
}

The way that scenes are generated in the current state of the repository is based on parsing a JSON file and then recreating the entire scene. The idea behind generating the code would effectively allow for adding event listeners to object’s you make without needing to use another API for it.

I’m not an expert on macros, so I’m not entirely sure where I would start if I may be honest. The project is open source.

I’m currently working on the editor and a Scene API that would make it easier to switch themes and styles on display object’s.

Since you’re doing this at runtime rather than compile time, maybe you should skip macros entirely. Just concatenate strings until you have the class definition you need.

If you want to use a built-in feature for this, try the template system. Your template might look something like this:

class ::instanceType:: extends Sprite {
    ::foreach contents::private var ::name:: : ::type::;
    ::end::
    public function new() {
        super();
        
        ::foreach contents::::initCode::
        
        addChild(::name::);
        ::end::
    }
}

Before you can use this template, you’ll need to modify your source data. Instead of "type": "Text", you’ll want "type": "TextField". And instead of all the text-specific entries like fontFile and selectable, you’ll want a single initCode value containing the Haxe code needed to set it up.

This is a good time for a TextField-specific template:

        ::name:: = new TextField();
        ::name::.defaultTextFormat = new TextFormat(Assets.getFont(::fontFile::).fontName, ::fontSize::,
            ::if fontColor::::fontColor::::else::0x000000::end::);
        ::name::.embedFonts = true;
        ::name::.selectable = ::selectable::;
        ::name::.text = ::text::;

Oh, yeah. I didn’t think of that. Although I may most likely keep the way the current structure of the JSON file is because it would be easier to make adjustments in its current form.

I’m thinking that each object type should have it’s own template, for example with the TextField-specific example you provided. And then combine them altogether into a single file.

So the initCode in this instance should be replaced with the generated code from a template for that specific object. Sounds like a plan. Thanks for the help.

Yeah, don’t change that. The point is to take that format as input, use the second template to parse it, and store the second template’s result in initCode. This makes it available for the first template to use.

public static function convert(data:Dynamic):String
{
    for(object in data.contents) {
        if(object.type == "Text") {
            object.type = "TextField";
            object.initCode = textFieldTemplate.execute(object);
        } else {
            //Similar code for other types.
        }
    }
    
    return spriteTemplate.execute(data);
}

I still dont think i quite get the work flow, i mean:

  1. Create JSON with UI in it
  2. Run app that turns JSON into sprite
  3. Export built sprite to haxe code
  4. Import haxe code into app again?

I dont quite understand why there is any generated code at all, i mean, why not:

  1. Create JSON with UI in it
  2. Macro turns JSON into nice object at compile to (type errors compile errors, etc)
  3. Ability to re-export built object back into json if needed

eg:

@build(Macros.buildSprite("myFile.json"))
class MySpriteClass extends Exporter {
    public function new() {
        doSomething();
    }

    @exportable myAddedField:String; // this is added by the user
    @exportable myAddedTextField:TextField; // this is added by the user
    
    private function doSomething():Void {
    }
}

All the other fields, like, _myText would be added by the buildSprite macro at compile time (as typed objects), and could also link up any events, or anything you want really. The Exporter base class (probably a bad name) could also contain util functions to export json with the current state, linke up events, anything you want.

Just seems strange to me (and has the potential for bugs) to be building haxe source from json, but as strings, not macros, which you are then going to use in an app - one too many steps imo

Ian

I think the situation is, HaxeLive allows you to arrange your UI by clicking and dragging, and once you’ve got everything in place, it exports JSON. However, tienery also wants the app to be able to export Haxe code.

All of this happens while HaxeLive is running, which is why I’m not recommending macros.

Fair enough, but then wouldnt it make more sense to export json rather than templated haxe source?

If you’re going to convert to Haxe anyway, exporting Haxe code is arguably the way to go. That way, you don’t have to run the macros every time you recompile.

The main point of HaxeLive is to provide live previewing to help build user interfaces at runtime, so everytime you make changes to a JSON file, it immediately takes affect. This is easier to modify versus actually modifying Haxe code, and since there is no solid solution (currently) that supports running a Haxe application dynamically inside of another Haxe application to provide this live previewing capability, I feel using JSON is one alternative for the time being.

Until a solid solution for live previewing haxe-written applications is found, this is what I am doing.

Ultimately, the idea of also exporting Haxe code comes with the fact that there is a lot of boilerplate code that needs to be written for UI to be displayed. By reducing the amount of typing to just modifying JSON files until you get an interface you like, it reduces the amount of time it takes to create a UI as well as giving you an Exporter that you can effectively go back to if you want a hard-coded solution as well, so it works both ways.

This is optional. You don’t have to export to Haxe code, but I would personally recommend it for larger projects with lots of JSON to parse, otherwise. HaxeLive has a SceneGen class which does all the generation from a JSON file. The Live class is the class that deals with automatically detecting changes in JSON files every time they are saved, to effectively re-parse everything in the JSON file again.

So once the Exporter is complete you will have two choices: You can stick with the JSON files and parse them using the SceneGen class, or you can CTRL+E inside the application to Export what you see to Haxe code to use later in your projects.

JSON is easier to redistribute and smaller in size, making them a better option for small projects.