NOCLIP/source/parser.zig

335 lines
12 KiB
Zig
Raw Normal View History

2025-02-21 23:15:59 -07:00
fn Short(comptime spec: type) type {
return struct {
param: noclip.Codepoint,
eager: bool,
mutator: Mutator(spec),
};
2025-02-21 23:15:59 -07:00
}
2025-02-21 23:15:59 -07:00
fn Long(comptime spec: type) type {
return struct {
2025-02-21 23:15:59 -07:00
param: []const u8,
eager: bool,
mutator: Mutator(spec),
};
}
2025-02-21 23:15:59 -07:00
pub fn Parser(comptime spec: type) type {
return struct {
// this gets heap allocated because it cannot survive being copied
parser: shove an arena allocator in there Stay a while and listen to my story. Due to the design of the parser execution flow, the only reasonable way to avoid leaking memory in the parser is to use an arena allocator because the parser itself doesn't have direct access to everything it allocates, and everything it allocates needs to live for the duration of whatever callbacks are called. Now, you might say, if the items it allocates are stored for the lifetime of whatever callbacks, then that means that the items it allocates stay allocated for effectively the entire life of the program. In which case there's really not much point in freeing them at all, as it's just extra work on exit that the OS should normally clean up. And you'd be right, except for two details: if the user uses the current GeneralPurposeAllocator, it will complain about leaks when deinitialized, which simply isn't groovy. The other detail is that technically the user can run any code they want after the parser execution finishes, so forcing the user to leak memory by having an incomplete API is rude. The other option would be, as before, forcing the user to supply their own arena allocator if they don't want to leak, but that's kind of a rude thing to do and goes against the "all allocators look the same" design of the standard library, which is what makes it so easy to use and create allocators with advanced functionality. That seems like an ugly thing to do, so, instead, each parser gets to eat the memory cost of storing a pointer to its arena allocator (and the heap cost of the arena allocator itself). In theory, subcommands could borrow the arena allocator of their parent command to save a bit of heap space, but that would make a variety of creation and cleanup-related tasks less isomorphic between the parents and the subcommands. I like the current design where commands and subcommands are the same thing, and I'm not in a rush to disturb that. I don't think the overhead cost of the arena allocator itself, which can be measured in double digit bytes, is a particularly steep price to pay.
2023-07-20 23:15:37 -07:00
arena: *std.heap.ArenaAllocator,
2025-02-21 23:15:59 -07:00
context: ContextType(spec),
globals: GlobalParams,
locals: LocalParams,
pub fn init(alloc: std.mem.Allocator, context: ContextType(spec)) !Self {
const arena = try alloc.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(alloc);
const globals: GlobalParams, const locals: LocalParams = comptime blk: {
var params: struct { global: GlobalParams, local: LocalParams } = .{
.global = .{ .short = &.{}, .long = &.{} },
.local = .{ .short = &.{}, .long = &.{}, .args = &.{} },
};
for (@typeInfo(@TypeOf(spec.parameters)).@"struct".decls) |dinf| {
const decl = @field(@TypeOf(spec.parameters), dinf.name);
switch (@TypeOf(decl).param_type) {
.flag => {
for (.{ "truthy", "falsy" }, .{ true, false }) |bias, value| {
for (.{ "short", "long" }) |style| {
if (@field(@field(decl, bias), style)) |unw| {
@field(@field(params, @tagName(decl.scope)), style) = @field(@field(params, @tagName(decl.scope)), style) ++ &.{
.{
.param = unw,
.mutator = implicitSetter(spec, dinf.name, value),
},
};
}
}
}
},
.option => {
for (.{ "short", "long" }) |style| {
if (@field(decl, style)) |unw| {
@field(@field(params, @tagName(decl.scope)), style) = @field(@field(params, @tagName(decl.scope)), style) ++ &.{.{
.param = unw,
.mutator = defaultMutator(spec, dinf.name),
}};
}
}
},
.argument => {},
}
break :blk .{ params.global, params.local };
}
2025-02-21 23:15:59 -07:00
};
2025-02-21 23:15:59 -07:00
return .{
.arena = arena,
.context = context,
.globals = globals,
.locals = locals,
};
}
2025-02-21 23:15:59 -07:00
pub fn deinit(self: Self) void {
const pa = self.arena.child_allocator;
parser: shove an arena allocator in there Stay a while and listen to my story. Due to the design of the parser execution flow, the only reasonable way to avoid leaking memory in the parser is to use an arena allocator because the parser itself doesn't have direct access to everything it allocates, and everything it allocates needs to live for the duration of whatever callbacks are called. Now, you might say, if the items it allocates are stored for the lifetime of whatever callbacks, then that means that the items it allocates stay allocated for effectively the entire life of the program. In which case there's really not much point in freeing them at all, as it's just extra work on exit that the OS should normally clean up. And you'd be right, except for two details: if the user uses the current GeneralPurposeAllocator, it will complain about leaks when deinitialized, which simply isn't groovy. The other detail is that technically the user can run any code they want after the parser execution finishes, so forcing the user to leak memory by having an incomplete API is rude. The other option would be, as before, forcing the user to supply their own arena allocator if they don't want to leak, but that's kind of a rude thing to do and goes against the "all allocators look the same" design of the standard library, which is what makes it so easy to use and create allocators with advanced functionality. That seems like an ugly thing to do, so, instead, each parser gets to eat the memory cost of storing a pointer to its arena allocator (and the heap cost of the arena allocator itself). In theory, subcommands could borrow the arena allocator of their parent command to save a bit of heap space, but that would make a variety of creation and cleanup-related tasks less isomorphic between the parents and the subcommands. I like the current design where commands and subcommands are the same thing, and I'm not in a rush to disturb that. I don't think the overhead cost of the arena allocator itself, which can be measured in double digit bytes, is a particularly steep price to pay.
2023-07-20 23:15:37 -07:00
self.arena.deinit();
2025-02-21 23:15:59 -07:00
pa.destroy(self.arena);
parser: shove an arena allocator in there Stay a while and listen to my story. Due to the design of the parser execution flow, the only reasonable way to avoid leaking memory in the parser is to use an arena allocator because the parser itself doesn't have direct access to everything it allocates, and everything it allocates needs to live for the duration of whatever callbacks are called. Now, you might say, if the items it allocates are stored for the lifetime of whatever callbacks, then that means that the items it allocates stay allocated for effectively the entire life of the program. In which case there's really not much point in freeing them at all, as it's just extra work on exit that the OS should normally clean up. And you'd be right, except for two details: if the user uses the current GeneralPurposeAllocator, it will complain about leaks when deinitialized, which simply isn't groovy. The other detail is that technically the user can run any code they want after the parser execution finishes, so forcing the user to leak memory by having an incomplete API is rude. The other option would be, as before, forcing the user to supply their own arena allocator if they don't want to leak, but that's kind of a rude thing to do and goes against the "all allocators look the same" design of the standard library, which is what makes it so easy to use and create allocators with advanced functionality. That seems like an ugly thing to do, so, instead, each parser gets to eat the memory cost of storing a pointer to its arena allocator (and the heap cost of the arena allocator itself). In theory, subcommands could borrow the arena allocator of their parent command to save a bit of heap space, but that would make a variety of creation and cleanup-related tasks less isomorphic between the parents and the subcommands. I like the current design where commands and subcommands are the same thing, and I'm not in a rush to disturb that. I don't think the overhead cost of the arena allocator itself, which can be measured in double digit bytes, is a particularly steep price to pay.
2023-07-20 23:15:37 -07:00
}
2025-02-21 23:15:59 -07:00
const Self = @This();
const GlobalParams = struct {
short: []const Short(spec),
long: []const Long(spec),
};
const LocalParams = struct {
short: []const Short(spec),
long: []const Long(spec),
args: []const Mutator(spec),
};
};
}
2025-02-21 23:15:59 -07:00
pub fn Result(comptime spec: type) type {
comptime {
var out: std.builtin.Type = .{
.@"struct" = .{
.layout = .auto,
.fields = &.{},
.decls = &.{},
.is_tuple = false,
},
};
2025-02-21 23:15:59 -07:00
for (@typeInfo(@TypeOf(spec.parameters)).@"struct".decls) |df| {
const decl = @field(spec.parameters, df.name);
const ftype = if (decl.default != null) @TypeOf(decl).Result else ?@TypeOf(decl).Result;
out.@"struct".fields = out.@"struct".fields ++ &.{.{
.name = df.name,
.type = ftype,
.default_value = decl.default orelse null,
.is_comptime = false,
.alignment = @alignOf(ftype),
}};
}
2025-02-21 23:15:59 -07:00
return @Type(out);
}
}
2025-02-21 23:15:59 -07:00
pub fn FieldType(comptime T: type, comptime field: []const u8) type {
// return @FieldType(T, field);
return switch (@typeInfo(T)) {
.Enum => |ti| ti.tag_type,
inline .Union, .Struct => |tf| l: for (tf.fields) |tfield| {
if (std.mem.eql(u8, tfield.name, field)) break :l tfield.type;
} else unreachable,
else => unreachable,
};
}
2025-02-21 23:15:59 -07:00
pub fn ResultFT(comptime spec: type, comptime field: []const u8) type {
return FieldType(Result(spec), field);
}
2025-02-21 23:15:59 -07:00
pub fn ContextType(comptime spec: type) type {
return spec.options.context_type;
}
2025-02-21 23:15:59 -07:00
pub fn Mutator(comptime spec: type) type {
return *const fn (std.mem.Allocator, ContextType(spec), *Result(spec), []const u8) noclip.Status(void);
}
2025-02-21 23:15:59 -07:00
pub fn TrivialConverter(comptime T: type) type {
return *const fn () noclip.Status(T);
}
2025-02-21 23:15:59 -07:00
pub fn SimpleConverter(comptime T: type, comptime alloc: bool) type {
return if (alloc)
*const fn (std.mem.Allocator, []const u8) noclip.Status(T)
else
*const fn ([]const u8) noclip.Status(T);
}
2025-02-21 23:15:59 -07:00
pub fn Converter(comptime spec: type, comptime FType: type) type {
const Type = enum {
trivial,
implicit,
simple,
context,
result,
full,
alloc_simple,
alloc_context,
alloc_result,
alloc_full,
};
2025-02-21 23:15:59 -07:00
return union(Type) {
trivial: TrivialConverter(FType),
simple: SimpleConverter(FType, false),
context: *const fn (ContextType(spec), []const u8) noclip.Status(FType),
result: *const fn (*const Result(spec), []const u8) noclip.Status(FType),
full: *const fn (ContextType(spec), *const Result(spec), []const u8) noclip.Status(FType),
alloc_simple: SimpleConverter(FType, true),
alloc_context: *const fn (std.mem.Allocator, ContextType(spec), []const u8) noclip.Status(FType),
alloc_result: *const fn (std.mem.Allocator, *const Result(spec), []const u8) noclip.Status(FType),
alloc_full: *const fn (std.mem.Allocator, ContextType(spec), *const Result(spec), []const u8) noclip.Status(FType),
pub fn wrap(function: anytype) @This() {
const t: Type = comptime blk: {
const FuncType: type = switch (@typeInfo(@TypeOf(function))) {
.pointer => |ptr| ptr.child,
.@"fn" => @TypeOf(function),
else => unreachable,
};
for (std.meta.fields(Type)) |tf| {
if (@typeInfo(FieldType(@This(), tf.name)).pointer.child == FuncType)
break :blk @field(Type, tf.name);
} else unreachable;
};
2025-02-21 23:15:59 -07:00
return @unionInit(@This(), @tagName(t), function);
}
2025-02-21 23:15:59 -07:00
pub fn invoke(
self: @This(),
alloc: std.mem.Allocator,
context: ContextType(spec),
res: *const Result(spec),
rawvalue: []const u8,
) noclip.Status(FType) {
return switch (self) {
.trivial => |call| call(),
.simple => |call| call(rawvalue),
.context => |call| call(context, rawvalue),
.result => |call| call(res, rawvalue),
.full => |call| call(context, res, rawvalue),
.alloc_simple => |call| call(alloc, rawvalue),
.alloc_context => |call| call(alloc, context, rawvalue),
.alloc_result => |call| call(alloc, res, rawvalue),
.alloc_full => |call| call(alloc, context, res, rawvalue),
};
}
2025-02-21 23:15:59 -07:00
};
}
2025-02-21 23:15:59 -07:00
pub fn defaultConverter(comptime spec: type, comptime FType: type) Converter(spec, FType) {
if (FType == noclip.String) {
return convertString;
}
return switch (@typeInfo(FType)) {
.int => Converter(spec, FType).wrap(convertInt(FType, 0)),
.@"enum" => Converter(spec, FType).wrap(convertEnum(FType)),
};
}
2025-02-21 23:15:59 -07:00
fn defaultMutator(
comptime spec: type,
comptime field: []const u8,
) Mutator(spec) {
const converter = defaultConverter(spec, ResultFT(spec, field));
return struct {
fn mut(alloc: std.mem.Allocator, ctx: ContextType(spec), res: *Result(spec), rawvalue: []const u8) noclip.Status(void) {
switch (converter.invoke(alloc, ctx, res, rawvalue)) {
.success => |val| @field(res, field) = val,
.failure => |val| return .{ .failure = val },
}
2025-02-21 23:15:59 -07:00
return .success;
}
2025-02-21 23:15:59 -07:00
}.mut;
}
2025-02-21 23:15:59 -07:00
pub fn convertInt(comptime T: type, base: u8) SimpleConverter(T, true) {
return struct {
fn conv(alloc: std.mem.Allocator, input: []const u8) noclip.Status(T) {
return if (std.fmt.parseInt(FieldType, input, base)) |res|
.{ .success = res }
else |_|
2025-02-21 23:15:59 -07:00
.{ .failure = .{
.message = std.fmt.allocPrint(
alloc,
"could not parse {s} as an integer",
.{input},
) catch "out of memory",
} };
}
2025-02-21 23:15:59 -07:00
}.conv;
}
2025-02-21 23:15:59 -07:00
pub fn convertEnum(comptime T: type) SimpleConverter(T, true) {
return struct {
2025-02-21 23:15:59 -07:00
fn conv(alloc: std.mem.Allocator, input: []const u8) noclip.Status(T) {
return if (std.meta.stringToEnum(T, input)) |val|
.{ .success = val }
else
.{
.failure = .{ .message = std.fmt.allocPrint(
alloc,
"`{s}` is not a member of {s}",
.{ input, @typeName(T) } catch "out of memory",
) },
};
}
2025-02-21 23:15:59 -07:00
}.conv;
}
2025-02-21 23:15:59 -07:00
pub fn convertString(alloc: std.mem.Allocator, input: []const u8) noclip.Status(noclip.String) {
return if (alloc.dupe(input)) |copy|
.{ .success = .{ .bytes = copy } }
else |_|
.{ .failure = .{ .message = "out of memory" } };
}
2025-02-21 23:15:59 -07:00
fn incrementor(
comptime spec: type,
comptime field: []const u8,
comptime step: ResultFT(spec, field),
) Mutator(spec) {
return struct {
fn mut(_: std.mem.Allocator, _: ContextType(spec), res: *Result(spec), _: []const u8) noclip.Status(void) {
@field(res, field) += step;
return .success;
}
2025-02-21 23:15:59 -07:00
}.mut;
}
2025-02-21 23:15:59 -07:00
fn implicitSetter(
comptime spec: type,
comptime field: []const u8,
comptime value: ResultFT(spec, field),
) Mutator(spec) {
return struct {
fn mut(_: std.mem.Allocator, _: ContextType(spec), res: *Result(spec), _: []const u8) noclip.Status(void) {
@field(res, field) = value;
return .success;
}
2025-02-21 23:15:59 -07:00
}.mut;
}
2025-02-21 23:15:59 -07:00
fn setter(
comptime spec: type,
comptime field: []const u8,
comptime converter_fn: anytype,
) Mutator(spec) {
const converter = Converter(spec, ResultFT(spec, field)).wrap(converter_fn);
return struct {
fn mut(alloc: std.mem.Allocator, ctx: ContextType(spec), res: *Result(spec), rawvalue: []const u8) noclip.Status(void) {
switch (converter.invoke(alloc, ctx, res, rawvalue)) {
.success => |val| @field(res, field) = val,
.failure => |val| return .{ .failure = val },
}
2025-02-21 23:15:59 -07:00
return .success;
}
}.mut;
}
2025-02-21 23:15:59 -07:00
const std = @import("std");
const noclip = @import("./noclip.zig");