NOCLIP/source/parameters.zig

349 lines
12 KiB
Zig
Raw Normal View History

const std = @import("std");
const converters = @import("./converters.zig");
const ConverterSignature = converters.ConverterSignature;
const ParameterType = enum { Nominal, Ordinal };
pub const FixedCount = u32;
pub const ValueCount = union(enum) {
flag: void,
count: void,
fixed: FixedCount,
};
pub const FlagBias = enum {
falsy,
truthy,
unbiased,
pub fn string(comptime self: @This()) [:0]const u8 {
return switch (comptime self) {
.truthy => "true",
.falsy => "false",
else => @compileError("flag tag with unbiased bias?"),
};
}
};
pub const ParameterGenerics = struct {
UserContext: type = void,
/// If void, do not expose this parameter in the aggregate converted parameter
/// object. The converter for this parameter shall not return a value. This may be
/// useful for implementing complex conversion that produces output through its
/// side effects or by modifying the user context.
OutputType: type = void,
param_type: ParameterType,
value_count: ValueCount,
/// allow this named parameter to be passed multiple times.
/// values will be appended when it is encountered. If false, only the
/// final encountered instance will be used.
// since we now use multi in place of greedy values for simplicity, we may want to
// convert this an enum or add an additional flag to distinguish between the
// many-to-many and the many-to-one cases.
multi: bool,
pub fn fixedValueCount(comptime OutputType: type, comptime value_count: ValueCount) ValueCount {
return comptime if (value_count == .fixed)
switch (@typeInfo(OutputType)) {
2025-03-06 01:01:39 -07:00
.@"struct" => |info| .{ .fixed = info.fields.len },
.array => |info| .{ .fixed = info.len },
// TODO: this is a bit sloppy, but it can be refined later.
// .Pointer covers slices, which may be a many-to-many conversion.
2025-03-06 01:01:39 -07:00
.pointer => value_count,
else => .{ .fixed = 1 },
}
else
value_count;
}
pub fn hasContext(comptime self: @This()) bool {
return comptime self.UserContext != void;
}
pub fn hasOutput(comptime self: @This()) bool {
return self.OutputType != void;
}
pub fn isFlag(comptime self: @This()) bool {
return comptime switch (self.value_count) {
.flag, .count => true,
.fixed => false,
};
}
pub fn ConvertedType(comptime self: @This()) type {
// is this the correct way to collapse this?
return comptime if (self.multi and self.value_count != .count and self.OutputType != void)
std.ArrayList(self.ReturnValue())
else
self.ReturnValue();
}
pub fn IntermediateType(comptime self: @This()) type {
return comptime if (self.multi and self.value_count != .count)
std.ArrayList(self.IntermediateValue())
else
self.IntermediateValue();
}
pub fn ReturnValue(comptime self: @This()) type {
return comptime switch (self.value_count) {
.flag => bool,
.count => usize,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => self.OutputType,
// it's actually impossible to use a list in the general case
// because the result may have varying types. A tuple would
// work, but cannot be iterated over without inline for. It may
// be worth adding a ".structured" value count for a type that
// consumes many inputs but produces a single output. It would
// be nice to parse a tag into a struct directly. For that use
// case, the output type must be decoupled from the input type.
else => self.OutputType,
},
};
}
pub fn IntermediateValue(comptime self: @This()) type {
return comptime switch (self.value_count) {
.flag => [:0]const u8,
.count => usize,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => [:0]const u8,
else => std.ArrayList([:0]const u8),
},
};
}
pub fn nonscalar(comptime self: @This()) bool {
return comptime switch (self.value_count) {
.flag, .count => false,
.fixed => |count| switch (count) {
0 => @compileError("bad fixed-zero parameter"),
1 => false,
else => true,
},
};
}
};
// Consider a "namespace" parameter e.g. -Dfoo=val style. The namespace would be "D" and
// it takes the place of the second "-", but otherwise this is a long-style parameter.
// Could be parsed as forced-fused. Would work for flags as well, e.g. -fno-flag
pub 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,
description: []const u8 = "", // description for output in help text
default: ?generics.OutputType = null,
converter: ?ConverterSignature(generics) = null,
eager: bool = false,
required: bool = generics.param_type == .Ordinal,
global: bool = false,
secret: bool = false,
nice_type_name: ?[]const u8 = null,
flag_bias: FlagBias = .unbiased,
};
}
pub const ShortLongPair = struct {
short_tag: ?[]const u8 = null,
long_tag: ?[]const u8 = null,
};
pub fn FlagConfig(comptime generics: ParameterGenerics) type {
return struct {
name: []const u8,
truthy: ?ShortLongPair = null,
falsy: ?ShortLongPair = null,
env_var: ?[]const u8 = null,
description: []const u8 = "",
default: ?bool = null,
converter: ?ConverterSignature(generics) = null,
eager: bool = false,
required: bool = false,
global: bool = false,
secret: bool = false,
};
}
fn OptionType(comptime generics: ParameterGenerics) type {
return struct {
pub const G: ParameterGenerics = generics;
pub const param_type: ParameterType = generics.param_type;
pub const is_flag: bool = generics.isFlag();
pub const value_count: ValueCount = generics.value_count;
pub const multi: bool = generics.multi;
pub const has_output: bool = generics.hasOutput();
name: []const u8,
short_tag: ?[]const u8,
long_tag: ?[]const u8,
env_var: ?[]const u8,
/// description for output in help text
description: []const u8,
default: ?generics.OutputType,
converter: ConverterSignature(generics),
/// the option converter will be run eagerly, before full command line
/// validation.
eager: bool,
/// the option cannot be omitted from the command line.
required: bool,
/// this option is parsed in a pre-parsing pass that consumes it. It
/// may be present anywhere on the command line. A different way to
/// solve this problem is by using an environment variable. It must be
/// a tagged option.
global: bool,
/// if false, do not expose the resulting value in the output type.
/// the converter must have side effects for this option to do anything.
/// do not print help for this parameter
secret: bool,
/// friendly type name ("string" is better than "[]const u8")
nice_type_name: []const u8,
/// internal field for handling flag value biasing. Do not overwrite unless you
/// want weird things to happen.
flag_bias: FlagBias,
pub fn describe(self: @This(), allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
var buf = std.ArrayList(u8).init(allocator);
try buf.append('"');
try buf.appendSlice(self.name);
try buf.append('"');
if (self.short_tag != null or self.long_tag != null) {
try buf.appendSlice(" (");
if (self.short_tag) |short|
try buf.appendSlice(short);
if (self.short_tag != null and self.long_tag != null)
try buf.appendSlice(", ");
if (self.long_tag) |long|
try buf.appendSlice(long);
try buf.append(')');
}
return try buf.toOwnedSlice();
}
pub fn IntermediateValue(comptime _: @This()) type {
return generics.IntermediateValue();
}
};
}
fn checkShort(comptime short_tag: ?[]const u8) void {
const short = comptime short_tag orelse return;
if (short.len != 2 or short[0] != '-') @compileError("bad short tag: " ++ short);
}
fn checkLong(comptime long_tag: ?[]const u8) void {
const long = comptime long_tag orelse return;
if (long.len < 3 or long[0] != '-' or long[1] != '-') @compileError("bad long tag: " ++ long);
}
pub fn makeOption(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",
);
}
checkShort(opts.short_tag);
checkLong(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.DefaultConverter(generics) orelse @compileError(
"no converter provided for " ++
opts.name ++
"and no default exists",
));
return OptionType(generics){
.name = opts.name,
//
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
//
.description = opts.description,
.default = opts.default,
.converter = converter,
//
.eager = opts.eager,
.required = opts.required,
.global = opts.global,
//
.secret = opts.secret,
.nice_type_name = opts.nice_type_name orelse @typeName(generics.OutputType),
.flag_bias = opts.flag_bias,
};
}
pub fn makeArgument(
comptime generics: ParameterGenerics,
comptime opts: OptionConfig(generics),
) OptionType(generics) {
comptime {
if (opts.short_tag != null or opts.long_tag != null or opts.env_var != null) {
@compileError("argument " ++ opts.name ++ " must not have a long or short tag or an env var");
}
if (opts.global) {
@compileError("argument " ++ opts.name ++ " cannot be global");
}
const converter = opts.converter orelse
(converters.DefaultConverter(generics) orelse @compileError(
"no converter provided for " ++
opts.name ++
"and no default exists",
));
return OptionType(generics){
.name = opts.name,
//
.short_tag = opts.short_tag,
.long_tag = opts.long_tag,
.env_var = opts.env_var,
//
.description = opts.description,
.default = opts.default,
.converter = converter,
//
.eager = opts.eager,
.required = opts.required,
.global = opts.global,
//
.secret = opts.secret,
.nice_type_name = opts.nice_type_name orelse @typeName(generics.OutputType),
.flag_bias = .unbiased,
};
}
}