[Solved] Field index not found on prototype, due to macro?

When I compile my project, I get this very strange compiler error:

[ERROR] (unknown position)

   | Field index for SetNextWindowSize not found on prototype helios.imgui.ImGui

This only happens if I compile a new file I added which contains a macro. I wanted to get rid of boilerplate so I could just say “edit this field on this static variable”, and it will automatically also add a tooltip based on that field’s documentation comment. If I compile this new file individually, it’s fine (though useless), but when compiling the entire project I get the above error.

The function it’s referring to is defined in the extern class in question, and I’ve used it before too. If I delete that particular function, it starts referring to another one in the same class instead.

Does anyone know what the problem might be? It looks like a compiler bug to me… I tried upgrading from 4.3.7 to 5.0-preview1, but that only added the extra “[ERROR] (Unknown position)” at the top :frowning:

Here’s the file. For context, helios is a bunch of extern classes, theia is my own library, while roar is the name of the app itself (music player). The target is Lua. This is the first macro I’ve ever tried to write… :sweat_smile:

package roar.ui;

import haxe.macro.Context;
import haxe.macro.Expr;
import helios.imgui.ImGui;
import roar.Settings.Settings;
import roar.Settings.SettingsType;
import theia.Widget;
import theia.Window;

using StringTools;
using haxe.macro.TypeTools;

class SettingsEditor extends Widget {
    public function new() {}

    macro static function editField(field:Expr, edit:Expr):Expr {
        final exprs:Array<Expr> = [];
        // Call the provided editing function and set new value
        exprs.push(macro $field = $edit($field));
        // Show tooltip based on the field's documentation
        switch (field.expr) {
        case EField(e, field, kind):
            switch (Context.getType("roar.Settings.SettingsType")) {
            case TInst(_.get() => t, _):
                final description = t.findField(field)?.doc?.trim();
                if (description != null && description != "") {
                    exprs.push(macro ImGui.SetItemTooltip($v{description}));
                }
            case _:
                Context.error("roar.Settings.SettingsType is not a class?", Context.currentPos());
            }
        case _:
            Context.error("`field` must be a field expression", Context.currentPos());
        };

        return macro $b{exprs};
    }

    public function draw() {
        editField(Settings.defaultGain, ImGui.SliderFloat.bind("Default Gain", _, 0, 1));
    }

    public static function createWindow():Window {
        final window = new Window();
        window.setTitle("Roar—Settings");
        window.isClosable = Yes;
        window.isOpen = false;

        window.push(new SettingsEditor());

        return window;
    }
}

I moved the macro function into a separate class and file and now it works—I have no idea why.

If anyone can explain it, I would love to know :sweat_smile:

You may need to use conditional compilation to avoid importing classes into the macros that aren’t actually used there (and may not be compatible within a macro context, which probably explains the errors).

#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
#else
import helios.imgui.ImGui;
import roar.Settings.Settings;
import roar.Settings.SettingsType;
import theia.Widget;
import theia.Window;
#end

(The exact imports to put in each section may be slightly different than what I’m suggesting here, but it’s probably not too far off, and it’s mostly just to give you an idea that you can tweak to your needs.)

It may also help to include full package names in classes that you reference in your macros. You won’t necessarily need to import those classes. Example:

exprs.push(macro helios.imgui.ImGui.SetItemTooltip($v{description}));

Personally, I usually put macros in their own classes to avoid this conditional compilation because it feels like it takes too much mental effort to ensure that everything is correctly imported in the correct context.