NOCLIP/source/doodle.zig

695 lines
25 KiB
Zig
Raw Normal View History

const std = @import("std");
const StructField = std.builtin.Type.StructField;
const converters = @import("./converters.zig");
const ncmeta = @import("./meta.zig");
const ConverterSignature = converters.ConverterSignature;
const ParameterType = enum {
Nominal,
Ordinal,
Executable,
};
const Errors = error{
BadConfiguration,
MissingTag,
ArgumentWithTags,
ArgumentWithEnvVar,
MissingDefaultConverter,
};
const ParseError = error{
ValueMissing,
FusedShortTagValueMissing,
UnknownLongTagParameter,
UnknownShortTagParameter,
};
const FlagBias = enum {
falsy,
truthy,
unbiased,
pub fn string(self: @This()) []const u8 {
return switch (self) {
.truthy => "true",
else => @compileLog(self),
};
}
};
const OptionResult = union(enum) {
Value: type,
flag: FlagBias,
};
pub const ParameterGenerics = struct {
ContextType: type = void,
result: OptionResult = .{ .Value = []const u8 },
param_type: ParameterType,
pub fn no_context(comptime self: @This()) bool {
return self.ContextType == void;
}
pub fn is_flag(comptime self: @This()) bool {
return self.result == .flag;
}
pub fn clone(comptime self: @This(), comptime NewResult: type) @This() {
return @This(){
.ContextType = self.ContextType,
.result = .{ .Value = NewResult },
};
}
pub fn ResultType(comptime self: @This()) type {
return switch (self.result) {
.Value => |res| res,
.flag => bool,
};
}
};
const ValuedGenericsBasis = struct { ContextType: type = void, Result: type };
const FlagGenericsBasis = struct { ContextType: type = void, flag_bias: FlagBias = .truthy };
fn tag_generics(comptime basis: ValuedGenericsBasis) ParameterGenerics {
return ParameterGenerics{
.ContextType = basis.ContextType,
.result = .{ .Value = basis.Result },
.param_type = .Nominal,
};
}
fn flag_generics(comptime basis: FlagGenericsBasis) ParameterGenerics {
return ParameterGenerics{
.ContextType = basis.ContextType,
.result = .{ .flag = basis.flag_bias },
.param_type = .Nominal,
};
}
fn arg_generics(comptime basis: ValuedGenericsBasis) ParameterGenerics {
return ParameterGenerics{
.ContextType = basis.ContextType,
.result = .{ .Value = basis.Result },
.param_type = .Ordinal,
};
}
fn OptionConfig(comptime generics: ParameterGenerics) type {
return struct {
name: []const u8,
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
env_var: ?[]const u8 = null,
default: ?generics.ResultType() = null,
converter: ?ConverterSignature(generics) = null,
arg_count: u32 = if (generics.is_flag()) 0 else 1,
eager: bool = false,
required: bool = generics.param_type == .Ordinal,
exposed: bool = true,
secret: bool = false,
nice_type_name: []const u8 = @typeName(generics.ResultType()),
};
}
fn OptionType(comptime generics: ParameterGenerics) type {
return struct {
pub const gen = generics;
pub const param_type: ParameterType = generics.param_type;
pub const is_flag: bool = generics.is_flag();
pub const flag_bias: FlagBias = if (generics.is_flag()) generics.result.flag else .unbiased;
name: []const u8,
short_tag: ?[]const u8,
long_tag: ?[]const u8,
env_var: ?[]const u8,
default: ?generics.ResultType(),
converter: ConverterSignature(generics),
description: []const u8 = "", // description for output in help text
arg_count: u32,
eager: bool,
required: bool,
exposed: bool, // do not expose the resulting value in the output type. the handler must have side effects for this option to do anything
secret: bool, // do not print help for this parameter
nice_type_name: ?[]const u8 = null, // friendly type name (string better than []const u8)
};
}
fn check_short(comptime short_tag: ?[]const u8) void {
if (short_tag) |short| {
if (short.len != 2 or short[0] != '-') @compileError("bad short tag");
}
}
fn check_long(comptime long_tag: ?[]const u8) void {
if (long_tag) |long| {
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag");
}
}
fn make_option(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
if (opts.short_tag == null and opts.long_tag == null and opts.env_var == null) {
@compileError(
"option " ++
opts.name ++
" must have at least one of a short tag, a long tag, or an environment variable",
);
}
check_short(opts.short_tag);
check_long(opts.long_tag);
// perform the logic to create the default converter here? Could be done
// when creating the OptionConfig instead. Need to do it here because there
// may be an error. That's the essential distinction between the OptionType
// and the OptionConfig, is the OptionConfig is just unvalidated parameters,
// whereas the OptionType is an instance of an object that has been
// validated.
const converter = opts.converter orelse converters.default_converter(generics) orelse {
@compileLog(opts);
@compileError("implement me");
};
return OptionType(generics){
.name = opts.name,
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
.default = opts.default,
.converter = converter,
.arg_count = opts.arg_count,
.eager = opts.eager,
.required = opts.required,
.exposed = opts.exposed,
.secret = opts.secret,
};
}
fn make_argument(comptime generics: ParameterGenerics, comptime opts: OptionConfig(generics)) OptionType(generics) {
// TODO: it would technically be possible to support specification of
// ordered arguments through environmental variables, but it doesn't really
// make a lot of sense. The algorithm would consume the env var greedily
if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) {
@compileLog(opts);
@compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var");
}
const converter = opts.converter orelse converters.default_converter(generics) orelse {
@compileLog(opts);
@compileError("implement me");
};
return OptionType(generics){
.name = opts.name,
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
.default = opts.default,
.converter = converter,
.arg_count = opts.arg_count,
.eager = opts.eager,
.required = opts.required,
.exposed = opts.exposed,
.secret = opts.secret,
};
}
const ShortLongPair = struct {
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
};
fn FlagBuilderArgs(comptime ContextType: type) type {
return struct {
name: []const u8,
truthy: ?ShortLongPair = null,
falsy: ?ShortLongPair = null,
env_var: ?[]const u8 = null,
default: ?bool = null,
converter: ?ConverterSignature(flag_generics(.{ .ContextType = ContextType })) = null,
eager: bool = false,
exposed: bool = true,
required: bool = false,
secret: bool = false,
};
}
fn CommandBuilder(comptime ContextType: type) type {
return struct {
param_spec: ncmeta.MutableTuple = .{},
pub const UserContextType = ContextType;
pub fn add_argument(
comptime self: *@This(),
comptime Result: type,
comptime args: OptionConfig(arg_generics(.{ .ContextType = ContextType, .Result = Result })),
) void {
self.param_spec.add(make_argument(
arg_generics(.{ .ContextType = ContextType, .Result = Result }),
args,
));
}
pub fn add_option(
comptime self: *@This(),
comptime Result: type,
comptime args: OptionConfig(tag_generics(.{ .ContextType = ContextType, .Result = Result })),
) void {
self.param_spec.add(make_option(
tag_generics(.{ .ContextType = ContextType, .Result = Result }),
args,
));
}
pub fn add_flag(
comptime self: *@This(),
comptime build_args: FlagBuilderArgs(ContextType),
) void {
if (build_args.truthy == null and build_args.falsy == null and build_args.env_var == null) {
@compileError(
"flag " ++
build_args.name ++
" must have at least one of truthy flags, falsy flags, or env_var flags",
);
}
if (build_args.truthy) |truthy_pair| {
if (truthy_pair.short_tag == null and truthy_pair.long_tag == null) {
@compileError(
"flag " ++
build_args.name ++
" truthy pair must have at least short or long tags set",
);
}
const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .truthy });
const args = OptionConfig(generics){
.name = build_args.name,
.short_tag = truthy_pair.short_tag,
.long_tag = truthy_pair.long_tag,
.env_var = null,
.default = build_args.default,
.converter = build_args.converter,
.eager = build_args.eager,
.exposed = build_args.exposed,
.secret = build_args.secret,
};
self.param_spec.add(make_option(generics, args));
}
if (build_args.falsy) |falsy_pair| {
if (falsy_pair.short_tag == null and falsy_pair.long_tag == null) {
@compileError(
"flag " ++
build_args.name ++
" falsy pair must have at least short or long tags set",
);
}
const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .falsy });
const args = OptionConfig(generics){
.name = build_args.name,
.short_tag = falsy_pair.short_tag,
.long_tag = falsy_pair.long_tag,
.env_var = null,
.default = build_args.default,
.converter = build_args.converter,
.eager = build_args.eager,
.secret = build_args.secret,
};
self.param_spec.add(make_option(generics, args));
}
if (build_args.env_var) |env_var| {
const generics = flag_generics(.{ .ContextType = ContextType, .flag_bias = .unbiased });
const args = OptionConfig(generics){
.name = build_args.name,
.env_var = env_var,
.default = build_args.default,
.converter = build_args.converter,
.eager = build_args.eager,
.secret = build_args.secret,
};
self.param_spec.add(make_option(generics, args));
}
}
fn generate(comptime self: @This()) self.param_spec.TupleType() {
return self.param_spec.realTuple();
}
pub fn CallbackSignature(comptime self: @This()) type {
return *const fn (ContextType, self.CommandOutput()) anyerror!void;
}
pub fn CommandOutput(comptime self: @This()) type {
comptime {
const spec = self.generate();
var fields: []const StructField = &[0]StructField{};
var flag_skip = 0;
paramloop: for (spec, 0..) |param, idx| {
if (!param.exposed) continue :paramloop;
while (flag_skip > 0) {
flag_skip -= 1;
continue :paramloop;
}
const PType = @TypeOf(param);
if (PType.is_flag) {
var peek = idx + 1;
var bais_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
bais_seen[@enumToInt(PType.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
const PeekType = @TypeOf(peek_param);
if (PeekType.is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bais_seen[@enumToInt(PeekType.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name ++ " and " ++ peek_param.name);
} else {
bais_seen[@enumToInt(PeekType.flag_bias)] = true;
}
flag_skip += 1;
} else {
break;
}
}
}
// the default field is already the optional type. Stripping
// the optional wrapper is an interesting idea for required
// fields. I do not foresee this greatly increasing complexity here.
const FieldType = if (param.required)
std.meta.Child(std.meta.FieldType(PType, .default))
else
std.meta.FieldType(PType, .default);
// the wacky comptime slice extension hack
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name,
.type = FieldType,
.default_value = @ptrCast(?*const anyopaque, &param.default),
.is_comptime = false,
.alignment = @alignOf(FieldType),
}});
}
return @Type(.{ .Struct = .{
.layout = .Auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
} });
}
}
pub fn Intermediate(comptime self: @This()) type {
comptime {
const spec = self.generate();
var fields: []const StructField = &[0]StructField{};
var flag_skip = 0;
paramloop: for (spec, 0..) |param, idx| {
while (flag_skip > 0) {
flag_skip -= 1;
continue :paramloop;
}
const PType = @TypeOf(param);
if (PType.is_flag) {
var peek = idx + 1;
var bais_seen: [ncmeta.enum_length(FlagBias)]bool = [_]bool{false} ** ncmeta.enum_length(FlagBias);
bais_seen[@enumToInt(PType.flag_bias)] = true;
while (peek < spec.len) : (peek += 1) {
const peek_param = spec[peek];
const PeekType = @TypeOf(peek_param);
if (PeekType.is_flag and std.mem.eql(u8, param.name, peek_param.name)) {
if (bais_seen[@enumToInt(PeekType.flag_bias)] == true) {
@compileError("redundant flag!!!! " ++ param.name ++ " and " ++ peek_param.name);
} else {
bais_seen[@enumToInt(PeekType.flag_bias)] = true;
}
flag_skip += 1;
} else {
break;
}
}
}
// the wacky comptime slice extension hack
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name,
.type = ?[]const u8,
.default_value = @ptrCast(?*const anyopaque, &@as(?[]const u8, null)),
.is_comptime = false,
.alignment = @alignOf(?[]const u8),
}});
}
return @Type(.{ .Struct = .{
.layout = .Auto,
.fields = fields,
.decls = &.{},
.is_tuple = false,
} });
}
}
pub fn bind(comptime self: @This(), comptime callback: self.CallbackSignature()) Parser(self, callback) {
return Parser(self, callback){};
}
};
}
// the parser is generated by the bind method of the CommandBuilder, so we can
// be extremely type-sloppy here, which simplifies the signature.
fn Parser(comptime command: anytype, comptime callback: anytype) type {
_ = callback;
return struct {
const ContextType = @TypeOf(command).UserContextType;
// let there be fields! we can move some things to runtime.
// We can get some better behavior if we defer converting non-eager
// options until the entire command line has been parsed. However,
// to do that, we effectively have to store the parameters as strings until the
// entire line has been parsed.
// a goal is to
intermediate: command.Intermediate() = .{},
consumed_args: u32 = 0,
// pub fn add_subcommand(self: *@This(), verb: []const u8, parser: anytype) void {
// self.subcommands
// }
pub fn parse(
self: *@This(),
alloc: std.mem.Allocator,
argit: *std.process.ArgIterator,
env: std.process.EnvMap,
context: ContextType,
) anyerror!void {
_ = alloc;
// _ = context;
try self.read_environment(env);
var forced_args = false;
argloop: while (argit.next()) |arg| {
if (!forced_args and std.mem.eql(u8, arg, "--")) {
forced_args = true;
continue :argloop;
}
parse_tags: {
if (forced_args or arg.len < 1 or arg[0] != '-') break :parse_tags;
if (arg.len > 2 and arg[1] == '-') {
try self.parse_long_tag(arg, argit, context);
continue :argloop;
} else if (arg.len > 1) {
for (arg[1..], 1..) |short, idx| {
// _ = short;
// _ = idx;
try self.parse_short_tag(short, arg.len - idx - 1, argit, context);
}
continue :argloop;
}
}
try self.parse_argument(arg, argit);
}
}
inline fn parse_long_tag(
self: *@This(),
arg: []const u8,
argit: *std.process.ArgIterator,
context: ContextType,
) ParseError!void {
_ = context;
inline for (comptime command.generate()) |param| {
const PType = @TypeOf(param);
// removing the comptime here causes the compiler to die
comptime if (PType.param_type != .Nominal or param.long_tag == null) continue;
const tag = param.long_tag.?;
if (comptime PType.is_flag) {
if (std.mem.eql(u8, arg, tag)) {
@field(self.intermediate, param.name) = if (comptime PType.flag_bias == .truthy) "true" else "false";
return;
}
} else {
if (std.mem.startsWith(u8, arg, tag)) match: {
// TODO: handle more than one value
const next = if (arg.len == tag.len)
argit.next() orelse return ParseError.ValueMissing
else if (arg[tag.len] == '=')
arg[tag.len + 1 ..]
else
break :match;
@field(self.intermediate, param.name) = next;
// if (comptime param.eager) {
// try param.converter()
// }
return;
}
}
}
return ParseError.UnknownLongTagParameter;
}
inline fn parse_short_tag(
self: *@This(),
arg: u8,
remaining: usize,
argit: *std.process.ArgIterator,
context: ContextType,
) ParseError!void {
_ = context;
inline for (comptime command.generate()) |param| {
const PType = @TypeOf(param);
// removing the comptime here causes the compiler to die
comptime if (PType.param_type != .Nominal or param.short_tag == null) continue;
const tag = param.short_tag.?;
if (comptime PType.is_flag) {
if (arg == tag[1]) {
@field(self.intermediate, param.name) = if (comptime PType.flag_bias == .truthy) "true" else "false";
return;
}
} else {
if (arg == tag[1]) {
if (remaining > 0) return ParseError.FusedShortTagValueMissing;
const next = argit.next() orelse return ParseError.ValueMissing;
@field(self.intermediate, param.name) = next;
return;
}
}
}
return ParseError.UnknownShortTagParameter;
}
inline fn parse_argument(self: *@This(), arg: []const u8, argit: *std.process.ArgIterator) ParseError!void {
_ = argit;
comptime var arg_index: u32 = 0;
inline for (comptime command.generate()) |param| {
if (@TypeOf(param).param_type != .Ordinal) continue;
if (self.consumed_args == arg_index) {
std.debug.print("n: {s}, c: {d}, i: {d}\n", .{ param.name, self.consumed_args, arg_index });
@field(self.intermediate, param.name) = arg;
self.consumed_args += 1;
return;
}
arg_index += 1;
}
}
fn read_environment(self: *@This(), env: std.process.EnvMap) !void {
inline for (comptime command.generate()) |param| {
if (comptime param.env_var) |env_var| {
@field(self.intermediate, param.name) = env.get(env_var);
}
}
}
};
}
fn HelpBuilder(comptime command: anytype) type {
_ = command;
}
pub fn command_builder(comptime ContextType: type) CommandBuilder(ContextType) {
return CommandBuilder(ContextType){};
}
const Choice = enum { first, second };
const cli = cmd: {
var cmd = command_builder(void);
cmd.add_option(u8, .{
.name = "test",
.short_tag = "-t",
.long_tag = "--test",
.env_var = "NOCLIP_TEST",
});
cmd.add_flag(.{
.name = "flag",
.truthy = .{ .short_tag = "-f", .long_tag = "--flag" },
.falsy = .{ .long_tag = "--no-flag" },
.env_var = "NOCLIP_FLAG",
});
cmd.add_argument([]const u8, .{ .name = "arg" });
cmd.add_argument([]const u8, .{ .name = "argtwo" });
break :cmd cmd;
};
fn cli_handler(_: void, result: cli.CommandOutput()) !void {
_ = result;
}
pub fn main() !void {
// std.debug.print("hello\n", .{});
var parser = cli.bind(cli_handler);
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var argit = try std.process.argsWithAllocator(allocator);
var env = try std.process.getEnvMap(allocator);
_ = argit.next();
try parser.parse(allocator, &argit, env, {});
inline for (@typeInfo(@TypeOf(parser.intermediate)).Struct.fields) |field| {
std.debug.print("{s}: {?s}\n", .{ field.name, @field(parser.intermediate, field.name) });
}
}