feat(Lua): get lua parsing working

WIP, still has memory leaks and no error handling.
This commit is contained in:
Titanium Brain 2026-05-31 13:29:33 +01:00
commit 995650919e
2 changed files with 134 additions and 38 deletions

View file

@ -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);
}
};
}

View file

@ -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",