From 995650919e17734aa338b31d4dd60a2dfb3e8ba4 Mon Sep 17 00:00:00 2001 From: Titanium Brain Date: Sun, 31 May 2026 13:29:33 +0100 Subject: [PATCH] feat(Lua): get lua parsing working WIP, still has memory leaks and no error handling. --- ly-core/src/root.zig | 124 ++++++++++++++++++++++++++++++++++--------- src/main.zig | 48 ++++++++++++----- 2 files changed, 134 insertions(+), 38 deletions(-) diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index fab9bc3..6df7592 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -9,17 +9,48 @@ pub const UidRange = @import("UidRange.zig"); pub const LogFile = @import("LogFile.zig"); pub const SharedError = @import("SharedError.zig"); +pub fn Parser(comptime T: type) type { + return union(enum) { + ini: IniParser(T), + lua: LuaParser(T), + + pub fn errors(self: *const @This()) std.ArrayList(Error) { + return switch (self.*) { + inline else => |p| p.errors, + }; + } + + pub fn maybe_load_error(self: *const @This()) ?anyerror { + return switch (self.*) { + inline else => |p| p.maybe_load_error, + }; + } + pub fn structure(self: *const @This()) T { + return switch (self.*) { + inline else => |p| p.structure, + }; + } + + pub fn deinit(self: *@This()) void { + switch (self.*) { + inline else => |*p| p.deinit(), + } + } + }; +} + +pub const Error = struct { + type_name: []const u8, + key: []const u8, + value: []const u8, + error_name: []const u8, +}; + pub fn IniParser(comptime Struct: type) type { return struct { const Self = @This(); const temporary_allocator = std.heap.page_allocator; - pub const Error = struct { - type_name: []const u8, - key: []const u8, - value: []const u8, - error_name: []const u8, - }; pub var global_errors: std.ArrayList(Error) = .empty; ini_struct: ini.Ini(Struct), @@ -82,26 +113,33 @@ pub fn IniParser(comptime Struct: type) type { pub fn LuaParser(comptime Struct: type) type { return struct { const Self = @This(); + const temporary_allocator = std.heap.page_allocator; + + pub var global_errors: std.ArrayList(Error) = .empty; structure: Struct, + errors: std.ArrayList(Error), + maybe_load_error: ?anyerror, pub fn init( allocator: std.mem.Allocator, - io: std.Io, + // io: std.Io, path: []const u8, ) !Self { - var lua: Lua = try .init(allocator); + var lua: *Lua = try .init(allocator); defer lua.deinit(); lua.openLibs(); // convert to sentinel terminated slice const spath: [:0]const u8 = try allocator.dupeSentinel(u8, path, 0); + defer allocator.free(spath); lua.doFile(spath) catch { const error_str = lua.toString(-1) catch unreachable; const lua_str = try allocator.dupeSentinel(u8, error_str, 0); _ = lua_str; // TODO add error logging }; + var data: Struct = .{}; switch (@typeInfo(Struct)) { .@"struct" => |struc| { @@ -112,26 +150,64 @@ pub fn LuaParser(comptime Struct: type) type { else => @compileError("Expected a struct."), } - return data; + return .{ .structure = data, .errors = .empty, .maybe_load_error = null }; } pub fn setField(allocator: std.mem.Allocator, lua: *Lua, comptime field: std.builtin.Type.StructField, data: *Struct) !void { - try lua.getGlobal("ly"); + _ = lua.getGlobal("ly"); defer lua.pop(1); // pop ly table - switch (@typeInfo(field.type)) { - .int => { - _ = lua.getField(-1, field.name); - const value = try lua.toInteger(-1); - lua.pop(1); - @field(data, field.name) = @intCast(@as(field.type, @truncate(value))); - }, - .float => { - _ = lua.getField(-1, field.name); - const value = try lua.toNumber(-1); - lua.pop(1); - @field(data, field.name) = @floatCast(@as(field.type, value)); - }, - else => @compileError("unimplemented!"), + const type_info = @typeInfo(field.type); + const actual_type, const is_optional = blk: { + if (type_info == .optional) { + break :blk .{ type_info.optional.child, true }; + } + break :blk .{ field.type, false }; + }; + // push value to top of stack + _ = lua.getField(-1, field.name); + defer lua.pop(1); + + // handle null, i.e. undefined fields + if (is_optional and lua.isNil(-1)) { + @field(data, field.name) = null; + return; } + + // dispatch depending on type + if (type_info == .int) { + const value = try lua.toInteger(-1); + @field(data, field.name) = @intCast(value); + } else if (type_info == .float) { // all floats + const value = try lua.toNumber(-1); + @field(data, field.name) = @floatCast(value); + } else if (type_info == .bool) { + const value = lua.toBoolean(-1); + @field(data, field.name) = value; + } else if (field.type == []const u8 or field.type == [:0]const u8) { + const value = try lua.toString(-1); + if (actual_type == []const u8) { + const duped = try allocator.dupe(u8, value); + @field(data, field.name) = duped; + } else { + const duped = try allocator.dupeSentinel(u8, value, 0); + @field(data, field.name) = duped; + } + } else if (type_info == .@"enum") { + const value = try lua.toString(-1); + const variant = std.meta.stringToEnum(actual_type, value) orelse return error.InvalidVariant; + @field(data, field.name) = variant; + } + } + + pub fn deinit(self: *Self) void { + _ = self; // autofix + for (0..global_errors.items.len) |i| { + const err = global_errors.items[i]; + temporary_allocator.free(err.type_name); + temporary_allocator.free(err.key); + temporary_allocator.free(err.value); + } + + global_errors.deinit(temporary_allocator); } }; } diff --git a/src/main.zig b/src/main.zig index 31a8519..f74844d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,7 +19,9 @@ const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; +const Parser = ly_core.Parser; const IniParser = ly_core.IniParser; +const LuaParser = ly_core.LuaParser; const ini = ly_core.ini; const Ini = ini.Ini; @@ -198,22 +200,28 @@ pub fn main(init: std.process.Init) !void { if (res.args.config) |path| config_parent_path = path; if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; if (res.args.@"validate-config") |path| { - var parser = try IniParser(Config).init( - state.allocator, - state.io, - path, - migrator.configFieldHandler, - ); + var parser: Parser(Config) = blk: { + if (std.mem.endsWith(u8, path, ".ini")) { + break :blk .{ .ini = try IniParser(Config).init( + state.allocator, + state.io, + path, + migrator.configFieldHandler, + ) }; + } else { + break :blk .{ .lua = try LuaParser(Config).init(state.allocator, path) }; + } + }; defer parser.deinit(); - for (parser.errors.items) |err| { + for (parser.errors().items) |err| { std.log.err( "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ err.value, err.key, err.type_name, err.error_name }, ); } - if (parser.maybe_load_error) |err| { + if (parser.maybe_load_error()) |err| { std.log.err("failed to load config file: {s}", .{@errorName(err)}); std.process.exit(1); } @@ -229,12 +237,24 @@ pub fn main(init: std.process.Init) !void { state.allocator.free(state.old_save_path); }; + // TODO do not hardcode config file name const config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); defer state.allocator.free(config_path); custom.binds = .empty; custom.labels = .empty; - var config_parser = try IniParser(Config).init(state.allocator, state.io, config_path, migrator.configFieldHandler); + var config_parser: Parser(Config) = blk: { + if (std.mem.endsWith(u8, config_path, ".ini")) { + break :blk .{ .ini = try IniParser(Config).init( + state.allocator, + state.io, + config_path, + migrator.configFieldHandler, + ) }; + } else { + break :blk .{ .lua = try LuaParser(Config).init(state.allocator, config_path) }; + } + }; defer config_parser.deinit(); defer if (!shutdown or !restart) { var iter = custom.binds.iterator(); @@ -253,7 +273,7 @@ pub fn main(init: std.process.Init) !void { custom.labels.deinit(temporary_allocator); }; - state.config = config_parser.structure; + state.config = config_parser.structure(); var lang_buffer: [16]u8 = undefined; const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{state.config.lang}); @@ -271,8 +291,8 @@ pub fn main(init: std.process.Init) !void { state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); } - if (config_parser.maybe_load_error == null) { - migrator.lateConfigFieldHandler(&state.config, state.lang); + if (config_parser.maybe_load_error() == null) { + migrator.lateConfigFieldHandler(&state.config); } var maybe_uid_range_error: ?anyerror = null; @@ -640,7 +660,7 @@ pub fn main(init: std.process.Init) !void { ); } - if (config_parser.maybe_load_error) |load_error| { + if (config_parser.maybe_load_error()) |load_error| { // We can't localize this since the config failed to load so we'd fallback to the default language anyway try state.info_line.addMessage( "unable to parse config file", @@ -654,7 +674,7 @@ pub fn main(init: std.process.Init) !void { .{@errorName(load_error)}, ); - for (config_parser.errors.items) |err| { + for (config_parser.errors().items) |err| { try state.log_file.err( state.io, "conf",