How to easily map to Lua tables?

Hello.
I am trying to write some externs to some Lua libraries that require to pass dictionary tables and I want to make them type safe.
So far, I have been declaring abstract classes with public inline constructors, but this gets tedious really fast:

abstract JobOpts(Table<String, Dynamic>) {
	public inline function new(command:String, args:Array<String>) {
		this = Table.create(null, {
			command: command,
			arguments: Table.create(args)
		});
	}
}

Is there a better way that allows me to keep things properly typed but that does not require that much boilerplate?

Hello,
Without knowing too much about your specific requirements to map onto your API, you probably could get away with using typedefs for most dictionaries
i.e

typedef JobOpts = {
    command : String,
    arguments : lua.Table<Int,String> //haxe arrays and lua arrays are too different, so you would have to convert
}

In the generated lua, the only difference between typedefs and lua tables are as far as I can see:
It has an extra field __fields__, containing a dictionary of what fields are present
It has it’s own metatable set for __newindex and __tostring
As long as the API is not traversing using some sort of pairs method, then the extra field shouldn’t be a bother.

For extra convenience with arrays, you can use an abstract with generics

abstract LuaArray<T>(lua.Table<Int,T>) from lua.Table<Int,T> to lua.Table<Int,T> {
    @:from
    public static function from<T>(arr:Array<T>):LuaArray<T> {
        return lua.Table.fromArray(arr);
    }
}

typedef JobOpts = {
    command : String,
    arguments : LuaArray<String>
}

class Main {
    public static function main() {
        var dummyObject:JobOpts = {
            command: "command",
            arguments : ["arg1","arg2"]
        }
        trace(dummyObject);
    }
}

Will generate something like this:

local dummyObject = _hx_o({__fields__={command=true,arguments=true},command="command",arguments=___Main_LuaArray_Impl_.from(_hx_tab_array({[0]="arg1", "arg2"}, 2))});

My objective is to do my neovim configuration in Haxe, and also build plugins for neovim. So, my main target is calling neovim api methods from Lua. The example I posted is probably one of the simplest ones, they are externs for Plenary.nvim job module. Most of neovim libraries and neovim api methods will iterste all the table properties and inspect them. In my first attempt I got an exception telling me that I was providing an invalid table property, fields, so mapping to clean Lua tables is very important.

Thank you for your time

In that case the easiest way would probably be to undo the haxe changes to the table using another generic abstract

@:arrayAccess
abstract LuaArray<T>(lua.Table<Int,T>) from lua.Table<Int,T> to lua.Table<Int,T> {
    @:from
    public static function from<T>(arr:Array<T>):LuaArray<T> {
        return lua.Table.fromArray(arr);
    }
}

@:forward //automatically get fields from underlying type
abstract CleanTable<T>(T) {
    @:from
    public static inline function fromType<T>(obj:T):CleanTable<T> {
        untyped obj.__fields__ = null;
        lua.Lua.setmetatable(cast obj,null);
        return cast obj;
    }

    //could add a @:from lua table, but that will automatically accept/convert any lua table, probably want to make it explict 

    @:to
    public function to():lua.Table<String,Dynamic> {
        return cast this;
    }
}

typedef Settings = {
    setting : Bool
}

typedef JobOpts = {
    var command : String;
    var arguments : LuaArray<String>;
    var settings : CleanTable<Settings>;
}

extern class API {
    static function require(tbl:CleanTable<JobOpts>):Void;
}

class Main {
    public static function main() {
        API.require({
            command : "hello",
            arguments : ["one","two"],
            settings : {
                setting : true
            }
        });
    }
}

Generates

  local obj = ___Main_LuaArray_Impl_.from(_hx_tab_array({[0]="one", "two"}, 2));
  local obj1 = _hx_o({__fields__={setting=true},setting=true});
  obj1.__fields__ = nil;
  _G.setmetatable(obj1, nil);
  local obj = _hx_o({__fields__={command=true,arguments=true,settings=true},command="hello",arguments=obj,settings=obj1});
  obj.__fields__ = nil;
  _G.setmetatable(obj, nil);
  API.require(obj);

Another problem you might run into is the way haxe treats functions and typedefs, where it ends up generating a bind function in certain scenarios. I think there’s some forum posts about it if you run into that.
The only other approach I can think of would be to use macros to generate the kind of abstract code you wrote already, but that is a rabbit hole that can end up quite painful :sweat_smile:

Really appreciate the effort you are putting into helping me, but the generated output is quite unreadable. It is not that I want the code to be fully human-readable, but the less indirect calls and unneeded object allocations the better, and some of the API calls that I want to do may happen as often as in cursor move or every-time I open a buffer, so I’m not sure I want all that fields removal dance on every step.

That, or just use snippets seems to be my only reasonable options. Lucky LuaSnip is quite powerful, and it can even have recursive snippets.
That said, if the macros are type checked, I’m open to give them a try, after all I was ready to have some very internal, very well tested untyped lua calls, and anything that type checks is going to be better.

All that said, the ideal solution should be Haxe compiling to plain Lua tables, just like they do with javascript.

Someone in Stack-overflow already suggested to use a macro:

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

class TableBuilder {
    public static macro function build():Array<Field> {
        var fields = Context.getBuildFields();
        for (field in fields) {
            if (field.name != "_new") continue; // look for new()
            var f = switch (field.kind) { // ... that's a function
                case FFun(_f): _f;
                default: continue;
            }
            // abstract "constructors" transform `this = val;`
            // into `{ var this; this = val; return this; }`
            var val = switch (f.expr.expr) {
                case EBlock([_decl, macro this = $x, _ret]): x;
                default: continue;
            }
            //
            var objFields:Array<ObjectField> = [];
            for (arg in f.args) {
                var expr = macro $i{arg.name};
                if (arg.type.match(TPath({ name: "Array", pack: [] } ))) {
                    // if the argument's an array, make an unwrapper for it
                    expr = macro lua.Table.create($expr, null);
                }
                objFields.push({ field: arg.name, expr: expr });
            }
            var objExpr:Expr = { expr: EObjectDecl(objFields), pos: Context.currentPos() };
            val.expr = (macro lua.Table.create(null, $objExpr)).expr;
        }
        return fields;
    }
}
import lua.Table;

class Test {
    public static function main() {
        var q = new JobOpts("cmd", ["a", "b"]);
        Sys.println(q);
    }
}
@:build(TableBuilder.build())
abstract JobOpts(Table<String, Dynamic>) {
    extern public inline function new(command:String, args:Array<String>) this = throw "no macro!";
}

But that has the limitation that I can not pass tables from arguments, they must be inlined, so it is not very useful to create libraries and things that I want to give arguments to.
Can this macro be extended or improved in any way to allow this?