diff --git a/.gitignore b/.gitignore index 3389c86..4e8cdae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .zig-cache/ zig-out/ +definitions.lua diff --git a/build.zig b/build.zig index 08e024f..ff0d4ad 100644 --- a/build.zig +++ b/build.zig @@ -124,6 +124,19 @@ pub fn build(b: *Build) void { const docs_step = b.step("docs", "Build and install the documentation"); docs_step.dependOn(&install_docs.step); + + // definitions example + const def_exe = b.addExecutable(.{ + .root_source_file = b.path("examples/define-exe.zig"), + .name = "define-zig-types", + .target = target, + }); + def_exe.root_module.addImport("ziglua", ziglua); + var run_def_exe = b.addRunArtifact(def_exe); + run_def_exe.addFileArg(b.path("definitions.lua")); + + const define_step = b.step("define", "Generate definitions.lua file"); + define_step.dependOn(&run_def_exe.step); } fn buildLua(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, upstream: *Build.Dependency, lang: Language, shared: bool) *Step.Compile { diff --git a/examples/define-exe.zig b/examples/define-exe.zig new file mode 100644 index 0000000..f4f3c9e --- /dev/null +++ b/examples/define-exe.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const ziglua = @import("ziglua"); + +const T = struct { foo: i32 }; +const MyEnum = enum { asdf, fdsa, qwer, rewq }; +const SubType = struct { foo: i32, bar: bool, bip: MyEnum, bap: ?[]MyEnum }; +const Bippity = struct { A: ?i32, B: *bool, C: []const u8, D: ?*SubType }; +const TestType = struct { a: i32, b: f32, c: bool, d: SubType, e: [10]Bippity }; +const Foo = struct { far: MyEnum, near: SubType }; + +pub fn main() !void { + const output_file_path = std.mem.sliceTo(std.os.argv[1], 0); + try ziglua.define(std.heap.c_allocator, output_file_path, &.{ T, TestType, Foo }); +} diff --git a/src/define.zig b/src/define.zig new file mode 100644 index 0000000..31f2d18 --- /dev/null +++ b/src/define.zig @@ -0,0 +1,169 @@ +const std = @import("std"); + +const String = std.ArrayList(u8); +const Database = std.StringHashMap(void); + +pub const DefineState = struct { + allocator: std.mem.Allocator, + database: Database, + definitions: std.ArrayList(String), + + pub fn init(alloc: std.mem.Allocator) DefineState { + return DefineState{ + .allocator = alloc, + .database = Database.init(alloc), + .definitions = std.ArrayList(String).init(alloc), + }; + } + + pub fn deinit(self: *@This()) void { + for (self.definitions.items) |def| { + def.deinit(); + } + defer self.database.deinit(); + defer self.definitions.deinit(); + } +}; + +pub fn define( + alloc: std.mem.Allocator, + absolute_output_path: []const u8, + comptime to_define: []const type, +) !void { + var state = DefineState.init(alloc); + defer state.deinit(); + + inline for (to_define) |T| { + _ = try addClass(&state, T); + } + + var file = try std.fs.createFileAbsolute(absolute_output_path, .{}); + defer file.close(); + + try file.seekTo(0); + try file.writeAll(file_header); + + for (state.definitions.items) |def| { + try file.writeAll(def.items); + try file.writeAll("\n"); + } + + try file.setEndPos(try file.getPos()); +} + +const file_header: []const u8 = + \\---@meta + \\ + \\--- This is an autogenerated file, + \\--- Do not modify + \\ + \\ +; + +fn name(comptime T: type) []const u8 { + return (comptime std.fs.path.extension(@typeName(T)))[1..]; +} + +fn addEnum( + state: *DefineState, + comptime T: type, +) !void { + if (state.database.contains(@typeName(T)) == false) { + try state.database.put(@typeName(T), {}); + try state.definitions.append(String.init(state.allocator)); + const index = state.definitions.items.len - 1; + + try state.definitions.items[index].appendSlice("---@alias "); + try state.definitions.items[index].appendSlice(name(T)); + try state.definitions.items[index].appendSlice("\n"); + + inline for (@typeInfo(T).Enum.fields) |field| { + try state.definitions.items[index].appendSlice("---|\' \""); + try state.definitions.items[index].appendSlice(field.name); + try state.definitions.items[index].appendSlice("\" \'\n"); + } + } +} + +pub fn addClass( + state: *DefineState, + comptime T: type, +) !void { + if (state.database.contains(@typeName(T)) == false) { + try state.database.put(@typeName(T), {}); + try state.definitions.append(String.init(state.allocator)); + const index = state.definitions.items.len - 1; + + try state.definitions.items[index].appendSlice("---@class (exact) "); + try state.definitions.items[index].appendSlice(name(T)); + try state.definitions.items[index].appendSlice("\n"); + + inline for (@typeInfo(T).Struct.fields) |field| { + try state.definitions.items[index].appendSlice("---@field "); + try state.definitions.items[index].appendSlice(field.name); + + if (field.default_value != null) { + try state.definitions.items[index].appendSlice("?"); + } + try state.definitions.items[index].appendSlice(" "); + try luaTypeName(state, index, field.type); + try state.definitions.items[index].appendSlice("\n"); + } + } +} + +fn luaTypeName( + state: *DefineState, + index: usize, + comptime T: type, +) !void { + switch (@typeInfo(T)) { + .Struct => { + try state.definitions.items[index].appendSlice(name(T)); + try addClass(state, T); + }, + .Pointer => |info| { + if (info.child == u8 and info.size == .Slice) { + try state.definitions.items[index].appendSlice("string"); + } else switch (info.size) { + .One => { + try state.definitions.items[index].appendSlice("lightuserdata"); + }, + .C, .Many, .Slice => { + try luaTypeName(state, index, info.child); + try state.definitions.items[index].appendSlice("[]"); + }, + } + }, + .Array => |info| { + try luaTypeName(state, index, info.child); + try state.definitions.items[index].appendSlice("[]"); + }, + + .Vector => |info| { + try luaTypeName(state, index, info.child); + try state.definitions.items[index].appendSlice("[]"); + }, + .Optional => |info| { + try luaTypeName(state, index, info.child); + try state.definitions.items[index].appendSlice(" | nil"); + }, + .Enum => { + try state.definitions.items[index].appendSlice(name(T)); + try addEnum(state, T); + }, + .Int => { + try state.definitions.items[index].appendSlice("integer"); + }, + .Float => { + try state.definitions.items[index].appendSlice("number"); + }, + .Bool => { + try state.definitions.items[index].appendSlice("boolean"); + }, + else => { + @compileLog(T); + @compileError("Type not supported"); + }, + } +} diff --git a/src/lib.zig b/src/lib.zig index 308e422..03cba41 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,5 +1,8 @@ const std = @import("std"); +pub const def = @import("define.zig"); +pub const define = def.define; + const c = @cImport({ @cInclude("luaconf.h"); @cInclude("lua.h"); diff --git a/src/tests.zig b/src/tests.zig index d470df5..50ad4b1 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2816,3 +2816,69 @@ test "doFile" { try expectEqualStrings("testing", try lua.get([]const u8, "GLOBAL")); } + +test "define" { + const expected = + \\---@class (exact) T + \\---@field foo integer + \\ + \\---@class (exact) TestType + \\---@field a integer + \\---@field b number + \\---@field c boolean + \\---@field d SubType + \\---@field e Bippity[] + \\ + \\---@class (exact) SubType + \\---@field foo integer + \\---@field bar boolean + \\---@field bip MyEnum + \\---@field bap MyEnum[] | nil + \\ + \\---@alias MyEnum + \\---|' "asdf" ' + \\---|' "fdsa" ' + \\---|' "qwer" ' + \\---|' "rewq" ' + \\ + \\---@class (exact) Bippity + \\---@field A integer | nil + \\---@field B lightuserdata + \\---@field C string + \\---@field D lightuserdata | nil + \\ + \\---@class (exact) Foo + \\---@field far MyEnum + \\---@field near SubType + \\ + \\ + ; + + const T = struct { foo: i32 }; + const MyEnum = enum { asdf, fdsa, qwer, rewq }; + const SubType = struct { foo: i32, bar: bool, bip: MyEnum, bap: ?[]MyEnum }; + const Bippity = struct { A: ?i32, B: *bool, C: []const u8, D: ?*SubType }; + const TestType = struct { a: i32, b: f32, c: bool, d: SubType, e: [10]Bippity }; + const Foo = struct { far: MyEnum, near: SubType }; + + const a = std.testing.allocator; + + var state = ziglua.def.DefineState.init(a); + defer state.deinit(); + + const to_define: []const type = &.{ T, TestType, Foo }; + inline for (to_define) |my_type| { + _ = try ziglua.def.addClass(&state, my_type); + } + + var buffer: [10000]u8 = .{0} ** 10000; + var buffer_stream = std.io.fixedBufferStream(&buffer); + var writer = buffer_stream.writer(); + + for (state.definitions.items) |def| { + try writer.writeAll(def.items); + try writer.writeAll("\n"); + } + + try std.testing.expectEqualSlices(u8, expected, buffer_stream.getWritten()); +}