NOCLIP/source/parser.zig

716 lines
26 KiB
Zig
Raw Normal View History

2025-03-06 00:45:36 -07:00
pub fn Parser(comptime spec: type, comptime root: bool) type {
2025-02-21 23:15:59 -07:00
return struct {
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,
2025-03-06 00:45:36 -07:00
subcommands: Subcommands(spec, root),
2025-02-21 23:15:59 -07:00
2025-03-14 00:17:58 -06:00
const OptKeyVal = struct { []const u8, tokenizer.TokenContext.OptionContext };
const SubContext = struct { []const u8, *const tokenizer.TokenContext };
const OptAmalgam = struct { tokenizer.TokenContext.Options, []const OptKeyVal };
pub fn shortOptions(
globals: []const OptKeyVal,
level: tokenizer.TokenContext.NestLevel,
) OptAmalgam {
comptime {
return if (@hasDecl(spec, "parameters")) blk: {
var list: []const OptKeyVal = globals;
var glob: []const OptKeyVal = globals;
for (@typeInfo(spec.parameters).@"struct".decls) |decl| {
const param = @field(spec.parameters, decl.name);
for (param.shorts()) |short| {
const okv = &[_]OptKeyVal{.{ short.opt, .{
.global = if (short.scope == .global) level else .none,
.value = short.value,
} }};
list = list ++ okv;
if (short.scope == .global)
glob = glob ++ okv;
}
}
break :blk .{ .initComptime(list), glob };
} else .{ .initComptime(&.{}), &.{} };
}
}
pub fn longOptions(
globals: []const OptKeyVal,
level: tokenizer.TokenContext.NestLevel,
) OptAmalgam {
comptime {
return if (@hasDecl(spec, "parameters")) blk: {
var list: []const OptKeyVal = globals;
var glob: []const OptKeyVal = globals;
for (@typeInfo(spec.parameters).@"struct".decls) |decl| {
const param = @field(spec.parameters, decl.name);
for (param.longs()) |long| {
const okv = &[_]OptKeyVal{.{ long.opt, .{
.global = if (long.scope == .global) level else .none,
.value = long.value,
} }};
list = list ++ okv;
if (long.scope == .global)
glob = glob ++ okv;
}
}
break :blk .{ .initComptime(list), glob };
} else .{ .initComptime(&.{}), &.{} };
}
}
pub const tokenizerContext = if (root) rootTokenizerContext else subcommandTokenizerContext;
fn rootTokenizerContext() *const tokenizer.TokenContext {
comptime {
return subcommandTokenizerContext(.{}, .root);
}
}
fn subcommandTokenizerContext(
comptime globals: struct { short: []const OptKeyVal = &.{}, long: []const OptKeyVal = &.{} },
comptime level: tokenizer.TokenContext.NestLevel,
) *const tokenizer.TokenContext {
comptime {
const short, const short_glob = shortOptions(globals.short, level);
const long, const long_glob = longOptions(globals.long, level);
const subcommands: tokenizer.TokenContext.Subcommands = if (@hasDecl(spec, "subcommands")) blk: {
var subs: []const SubContext = &.{};
for (@typeInfo(spec.subcommands).@"struct".decls) |decl| {
subs = subs ++ &[_]SubContext{.{
decl.name,
Parser(@field(spec.subcommands, decl.name), false).tokenizerContext(
.{ .short = short_glob, .long = long_glob },
level.incr(),
),
}};
}
break :blk .initComptime(subs);
} else .initComptime(&.{});
return &.{
.short = short,
.long = long,
.positional = &.{},
.subcommands = subcommands,
};
}
}
2025-02-21 23:15:59 -07:00
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 = &.{} },
};
2025-03-14 00:17:58 -06:00
for (@typeInfo(spec.parameters).@"struct".decls) |dinf| {
2025-02-21 23:15:59 -07:00
const decl = @field(@TypeOf(spec.parameters), dinf.name);
switch (@TypeOf(decl).param_type) {
2025-03-06 00:45:36 -07:00
.bool_group => {
2025-02-21 23:15:59 -07:00
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,
2025-03-06 00:45:36 -07:00
.eager = decl.eager,
.takes_value = false,
2025-02-21 23:15:59 -07:00
.mutator = implicitSetter(spec, dinf.name, value),
},
};
}
}
}
},
2025-03-06 00:45:36 -07:00
.constant => {
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,
.eager = decl.eager,
.takes_value = false,
.mutator = implicitSetter(spec, dinf.name, dinf.value),
}};
}
}
},
.counter => {},
2025-02-21 23:15:59 -07:00
.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,
2025-03-06 00:45:36 -07:00
.eager = decl.eager,
.takes_value = true,
2025-02-21 23:15:59 -07:00
.mutator = defaultMutator(spec, dinf.name),
}};
}
}
},
2025-03-06 00:45:36 -07:00
.argument => {
params.local.args = params.local.args ++ &.{
defaultMutator(spec, dinf.name),
};
},
.group => {},
2025-02-21 23:15:59 -07:00
}
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-03-14 00:17:58 -06:00
pub fn parse(self: Self, args: []const [:0]const u8, _: std.process.EnvMap) noclip.Status(void) {
2025-03-06 00:45:36 -07:00
const alloc = self.arena.allocator();
var argt = ArgTraveler.fromSlice(alloc, args) catch return .fail("out of memory");
// pre-parse globals. globals can only be named, which simplifies things
var result = defaultInit(Result(spec));
while (argt.current) |node| : (argt.next()) {
const arg = node.data;
if (arg.len > 2 and arg[0] == '-' and arg[1] == '-') {
if (self.globals.long.get(arg[2..])) |pctx| {
argt.drop();
const value: [:0]const u8 = if (pctx.takes_value)
argt.popNext()
else
"";
switch (pctx.mutator(alloc, self.context, &result, value)) {}
}
} else if (arg.len > 1 and arg[0] == '-') {
const view = std.unicode.Utf8View.init(arg[1..]) catch return .fail("thats not valid utf8");
var iter = view.iterator();
while (iter.nextCodepointSlice()) |seq| {
if (self.globals.short.get(seq)) {
// we have to drop this byte sequence within the fused short params. ugly..................... hrngrk
}
}
}
}
// var parse_mode: enum { mixed, ordered } = .mixed;
// for (args) |arg| {
// if (arg.len > 2 and arg[0] == '-' and arg[1] == '-') {}
// // if (arg.len > 0 and arg[0] == '-')
// }
}
2025-02-21 23:15:59 -07:00
const Self = @This();
2025-03-06 00:45:36 -07:00
const NamedParameter = struct {
eager: bool,
takes_value: bool,
mutator: Mutator(spec),
};
const PMap = std.StaticStringMap(NamedParameter);
const ArgList = std.SinglyLinkedList([:0]const u8);
pub const ArgTraveler = struct {
first: ?*Node = null,
current: ?*Node = null,
prev: ?*Node = null,
mem: []const Node,
pub fn fromSlice(alloc: std.mem.Allocator, slice: []const [:0]const u8) error{OutOfMemory}!ArgTraveler {
if (slice.len == 0) return .{ .mem = &.{} };
const nmem = try alloc.alloc(Node, slice.len);
nmem[0] = slice[0];
for (slice[1..], nmem[1..], nmem[0 .. nmem.len - 1]) |arg, *current, *prev| {
current.* = .{ .data = arg };
prev.next = current;
}
return .{ .first = &nmem[0], .current = &nmem[0], .mem = nmem };
}
pub fn reset(self: *ArgTraveler) void {
self.current = self.first;
}
pub fn next(self: *ArgTraveler) void {
self.prev = self.current;
if (self.current) |current| {
self.current = current.next;
}
}
pub fn drop(self: *ArgTraveler) void {
if (self.current == null) return;
if (self.current == self.first)
self.first = self.current.?.next
else if (self.prev) |prev|
prev.next = self.current.?.next;
}
pub fn popNext(self: *ArgTraveler) ?*Node {
self.next();
defer self.drop();
return self.current;
}
pub const Node = struct {
data: [:0]const u8,
next: ?*Node = null,
};
};
// const PMap = std.StringHashMap(NamedParameter);
2025-02-21 23:15:59 -07:00
const GlobalParams = struct {
2025-03-06 00:45:36 -07:00
short: PMap,
long: PMap,
2025-02-21 23:15:59 -07:00
};
const LocalParams = struct {
2025-03-06 00:45:36 -07:00
short: PMap,
long: PMap,
2025-02-21 23:15:59 -07:00
args: []const Mutator(spec),
};
};
}
2025-02-21 23:15:59 -07:00
pub fn Result(comptime spec: type) type {
comptime {
2025-03-14 00:17:58 -06:00
var fields: []const std.builtin.Type.StructField = &.{};
2025-03-14 00:17:58 -06:00
for (@typeInfo(spec.parameters).@"struct".decls) |df| {
2025-03-06 00:45:36 -07:00
const param = @field(spec.parameters, df.name);
if (@TypeOf(param).Result == void) continue;
const FType = ResultFieldType(param);
2025-03-14 00:17:58 -06:00
fields = fields ++ &[_]std.builtin.Type.StructField{.{
2025-03-06 00:45:36 -07:00
.name = df.name ++ "",
.type = FType,
2025-03-14 00:17:58 -06:00
.default_value_ptr = resultFieldDefault(param),
2025-02-21 23:15:59 -07:00
.is_comptime = false,
2025-03-06 00:45:36 -07:00
.alignment = @alignOf(FType),
2025-02-21 23:15:59 -07:00
}};
}
2025-03-14 00:17:58 -06:00
return @Type(.{
.@"struct" = .{
.layout = .auto,
.fields = &.{},
.decls = &.{},
.is_tuple = false,
},
});
2025-02-21 23:15:59 -07:00
}
}
2025-03-06 00:45:36 -07:00
pub fn defaultInit(comptime T: type) T {
var result: T = undefined;
for (@typeInfo(T).Struct.fields) |field| {
2025-03-14 00:17:58 -06:00
if (field.default_value_ptr) |def| {
2025-03-06 00:45:36 -07:00
@field(result, field.name) = @as(*const field.type, @ptrCast(@alignCast(def))).*;
} else switch (@typeInfo(field.type)) {
.@"struct" => @field(result, field.name) = defaultInit(field.type),
else => {},
}
}
return result;
}
pub fn ResultFieldType(comptime param: anytype) type {
if (param.mode() == .accumulate) {
return param.Type();
}
2025-03-14 00:17:58 -06:00
// if (@typeInfo(param.Type()) == .optional) {
return if (param.default != null or param.required)
param.Type()
else
?param.Type();
// } else @compileError("you stepped in it now " ++ @typeName(param.Type()));
2025-03-06 00:45:36 -07:00
}
pub fn resultFieldDefault(comptime param: anytype) ?*anyopaque {
if (param.mode() == .accumulate) {
return &param.default;
}
2025-03-14 00:17:58 -06:00
// if (@typeInfo(param.Type()) == .optional) {
return if (param.default) |def|
@constCast(&@as(param.Type(), def))
else
null;
// } else @compileError("doom");
2025-03-06 00:45:36 -07:00
}
pub fn Subcommands(comptime spec: type, comptime root: bool) type {
comptime {
if (!@hasDecl(spec, "subcommands")) return void;
2025-03-14 00:17:58 -06:00
const decls = @typeInfo(spec.subcommands).@"struct".decls;
2025-03-06 00:45:36 -07:00
if (decls.len == 0) return void;
2025-03-14 00:17:58 -06:00
var fields: []const std.builtin.Type.StructField = &.{};
2025-03-06 00:45:36 -07:00
for (decls) |dinf| {
2025-03-14 00:17:58 -06:00
const decl = @field(spec.subcommands, dinf.name);
2025-03-06 00:45:36 -07:00
const FType = Parser(decl, false);
2025-03-14 00:17:58 -06:00
fields = fields ++ &[_]std.builtin.Type.StructField{.{
.name = dinf.name ++ "",
2025-03-06 00:45:36 -07:00
.type = FType,
2025-03-14 00:17:58 -06:00
.default_value_ptr = null,
2025-03-06 00:45:36 -07:00
.is_comptime = false,
.alignment = @alignOf(FType),
}};
}
if (root) {
2025-03-14 00:17:58 -06:00
help: switch (spec.options.create_help_command) {
.if_subcommands => if (fields.len > 0) continue :help .always,
.always => {
2025-03-06 00:45:36 -07:00
const FType = Parser(HelpCommand(spec), false);
2025-03-14 00:17:58 -06:00
fields = fields ++ &[_]std.builtin.Type.StructField{.{
2025-03-06 00:45:36 -07:00
.name = "help",
.type = FType,
2025-03-14 00:17:58 -06:00
.default_value_ptr = null,
2025-03-06 00:45:36 -07:00
.is_comptime = false,
.alignment = @alignOf(FType),
}};
},
.never => {},
}
if (spec.options.create_completion_helper) {}
}
2025-03-14 00:17:58 -06:00
return @Type(.{
.@"struct" = .{
.layout = .auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
},
});
2025-03-06 00:45:36 -07:00
}
}
pub fn HelpCommand(comptime rootspec: type) type {
return struct {
pub const description =
\\Get detailed help for a subcommand
;
pub const options: noclip.CommandOptions = .{};
pub const parameters = struct {
command_path: noclip.Argument(noclip.Aggregate(noclip.String)) = .{
.description =
\\The name of the subcommand to print help for. Nested subcommands
\\can be requested as well.
,
},
};
pub fn run(args: Result(@This())) void {
HelpGenerator(rootspec).lookupHelp(args.command_path);
}
};
}
pub fn HelpGenerator(comptime rootspec: type) type {
return struct {
pub fn lookupHelp(command_path: []const noclip.String) void {
_ = rootspec;
_ = command_path;
std.debug.print("This is a stub\n", .{});
}
};
}
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|
2025-03-06 00:45:36 -07:00
.succeed(res)
else |_|
2025-03-06 00:45:36 -07:00
.fail(
std.fmt.allocPrint(
2025-02-21 23:15:59 -07:00
alloc,
"could not parse {s} as an integer",
.{input},
) catch "out of memory",
2025-03-06 00:45:36 -07:00
);
}
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|
2025-03-06 00:45:36 -07:00
.succeed(val)
2025-02-21 23:15:59 -07:00
else
2025-03-06 00:45:36 -07:00
.fail(
std.fmt.allocPrint(
2025-02-21 23:15:59 -07:00
alloc,
"`{s}` is not a member of {s}",
2025-03-06 00:45:36 -07:00
.{ 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|
2025-03-06 00:45:36 -07:00
.succeed(.{ .bytes = copy })
2025-02-21 23:15:59 -07:00
else |_|
2025-03-06 00:45:36 -07:00
.fail("out of memory");
2025-02-21 23:15:59 -07:00
}
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) {
2025-03-06 00:45:36 -07:00
@field(res, field) +|= step;
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
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");
2025-03-14 00:17:58 -06:00
const tokenizer = @import("./tokenizer.zig");
const Choice = enum { first, second };
const Basic = struct {
pub const description = "A basic test";
pub const options: noclip.CommandOptions = .{
.create_help_command = .never,
};
pub const parameters = struct {
pub const choice: noclip.Option(Choice) = .{
.short = 'c',
.long = "choice",
.env = "NOCLIP_CHOICE",
.description = "enum choice option",
};
pub const default: noclip.Option(u32) = .{
.description = "default value integer option",
.short = 'd',
.long = "default",
.env = "NOCLIP_DEFAULT",
.default = 100,
.scope = .global,
};
pub const flag: noclip.BoolGroup = .{
.truthy = .{ .short = 'f', .long = "flag" },
.falsy = .{ .short = 'F', .long = "no-flag" },
.env = "NOCLIP_FLAG",
.description = "boolean flag",
};
};
pub const subcommands = struct {
pub const @"test" = struct {
pub const description = "a nested test";
pub const options: noclip.CommandOptions = .{};
pub const parameters = struct {
pub const flag: noclip.BoolGroup = .{
.truthy = .{ .short = 'f', .long = "flag" },
.falsy = .{ .short = 'F', .long = "no-flag" },
.env = "NOCLIP_FLAG",
.description = "boolean flag",
};
};
};
};
};
test "hmm" {
const P = Parser(Basic, true);
const tc = comptime P.tokenizerContext();
for (tc.short.keys()) |key| {
std.debug.print("short: {s}\n", .{key});
}
for (tc.long.keys()) |key| {
std.debug.print("long: {s}\n", .{key});
}
for (tc.subcommands.keys()) |key| {
std.debug.print("subcommand: {s}\n", .{key});
for (tc.subcommands.get(key).?.short.keys()) |skey| {
std.debug.print("short: {s}\n", .{skey});
}
}
}