2 Commits

Author SHA1 Message Date
0d091611dd parser: produce helpful error messages on command line errors
This is the first cut at providing human-readable context for command
line parsing failures. Since these failures are due to incorrect
input (normally produced by a human), closing the information loop at
the human layer makes a hell of a lot more sense than dumping an error
traceback with a (possibly cryptic) error name and calling it a day.

This approach doesn't print anything out by default and still depends
on the user to choose exactly how the handle and print the error
message. Errors are propagated from subcommands, though they end up
being copied, which shouldn't be strictly necessary. Maybe this can be
improved in the future. OutOfMemory has been added to ParseError to
simplify the code a bit.

The demo has been updated with a simplistic example of what presenting
error messages to the user may look like. I don't know that this
produces useful messages for every possible failure scenario, but it
does for the most common ones.
2024-04-07 11:23:25 -07:00
5f45290423 parser: run subcommand parsing/callbacks iteratively
This changes the parse/callback flow so that subcommands are run
iteratively from the base command rather than recursively. The primary
advantages of this approach are some stack space savings and much less
convoluted backtraces for deeply nested command hierarchies. The
overall order of operations has not changed, i.e. the full command
line is parsed before command callback dispatch starts.

Fixes: #12
2024-04-06 23:36:43 -07:00
8 changed files with 50 additions and 64 deletions

View File

@@ -1,11 +1,11 @@
const std = @import("std"); const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target: std.Build.ResolvedTarget = b.standardTargetOptions(.{}); const target: std.zig.CrossTarget = b.standardTargetOptions(.{});
const optimize: std.builtin.Mode = b.standardOptimizeOption(.{}); const optimize: std.builtin.Mode = b.standardOptimizeOption(.{});
const noclip = b.addModule("noclip", .{ const noclip = b.addModule("noclip", .{
.root_source_file = b.path("source/noclip.zig"), .source_file = .{ .path = "source/noclip.zig" },
}); });
demo(b, noclip, target, optimize); demo(b, noclip, target, optimize);
@@ -13,7 +13,7 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
const tests = b.addTest(.{ const tests = b.addTest(.{
.name = "tests", .name = "tests",
.root_source_file = b.path("source/noclip.zig"), .root_source_file = .{ .path = "source/noclip.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
@@ -24,18 +24,18 @@ pub fn build(b: *std.Build) void {
fn demo( fn demo(
b: *std.Build, b: *std.Build,
noclip: *std.Build.Module, noclip: *std.Build.Module,
target: std.Build.ResolvedTarget, target: std.zig.CrossTarget,
optimize: std.builtin.Mode, optimize: std.builtin.Mode,
) void { ) void {
const demo_step = b.step("demo", "Build and install CLI demo program"); const demo_step = b.step("demo", "Build and install CLI demo program");
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "noclip-demo", .name = "noclip-demo",
.root_source_file = b.path("demo/demo.zig"), .root_source_file = .{ .path = "demo/demo.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
exe.root_module.addImport("noclip", noclip); exe.addModule("noclip", noclip);
const install_demo = b.addInstallArtifact(exe, .{}); const install_demo = b.addInstallArtifact(exe, .{});
demo_step.dependOn(&install_demo.step); demo_step.dependOn(&install_demo.step);

View File

@@ -1,13 +0,0 @@
.{
.name = .NOCLIP,
.fingerprint = 0xE4C223E8CB9C8ADF,
.version = "0.1.0-pre",
.minimum_zig_version = "0.14.0",
.dependencies = .{},
.paths = .{
"source",
"build.zig",
"build.zig.zon",
"license",
},
}

View File

@@ -22,7 +22,7 @@ ____
== Hello == Hello
Requires Zig `0.13.x`. May work with `0.12.x`. Requires Zig `0.11.x`.
=== Features === Features

View File

@@ -118,8 +118,8 @@ pub const InterfaceContextCategory = union(enum) {
pub fn fromType(comptime ContextType: type) InterfaceContextCategory { pub fn fromType(comptime ContextType: type) InterfaceContextCategory {
return switch (@typeInfo(ContextType)) { return switch (@typeInfo(ContextType)) {
.void => .empty, .Void => .empty,
.pointer => |info| if (info.size == .slice) .{ .value = ContextType } else .{ .pointer = ContextType }, .Pointer => |info| if (info.size == .Slice) .{ .value = ContextType } else .{ .pointer = ContextType },
// technically, i0, u0, and struct{} should be treated as empty, probably // technically, i0, u0, and struct{} should be treated as empty, probably
else => .{ .value = ContextType }, else => .{ .value = ContextType },
}; };
@@ -172,8 +172,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
}; };
} }
pub const ifc = InterfaceCreator(@This()); pub usingnamespace InterfaceCreator(@This());
pub const createInterface = ifc.createInterface;
fn _createInterfaceImpl( fn _createInterfaceImpl(
comptime self: @This(), comptime self: @This(),
@@ -377,7 +376,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
// [:0]const u8. // [:0]const u8.
.name = short ++ "", .name = short ++ "",
.type = void, .type = void,
.default_value_ptr = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = 0, .alignment = 0,
}}; }};
@@ -386,7 +385,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
tag_fields = tag_fields ++ &[_]StructField{.{ tag_fields = tag_fields ++ &[_]StructField{.{
.name = long ++ "", .name = long ++ "",
.type = void, .type = void,
.default_value_ptr = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = 0, .alignment = 0,
}}; }};
@@ -395,7 +394,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
env_var_fields = env_var_fields ++ &[_]StructField{.{ env_var_fields = env_var_fields ++ &[_]StructField{.{
.name = env_var ++ "", .name = env_var ++ "",
.type = void, .type = void,
.default_value_ptr = null, .default_value = null,
.is_comptime = false, .is_comptime = false,
.alignment = 0, .alignment = 0,
}}; }};
@@ -440,28 +439,28 @@ pub fn CommandBuilder(comptime UserContext: type) type {
fields = fields ++ &[_]StructField{.{ fields = fields ++ &[_]StructField{.{
.name = param.name ++ "", .name = param.name ++ "",
.type = FieldType, .type = FieldType,
.default_value_ptr = @ptrCast(default), .default_value = @ptrCast(default),
.is_comptime = false, .is_comptime = false,
.alignment = @alignOf(FieldType), .alignment = @alignOf(FieldType),
}}; }};
} }
_ = @Type(.{ .@"struct" = .{ _ = @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = tag_fields, .fields = tag_fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,
} }); } });
_ = @Type(.{ .@"struct" = .{ _ = @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = env_var_fields, .fields = env_var_fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,
} }); } });
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = fields, .fields = fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,
@@ -510,7 +509,7 @@ pub fn CommandBuilder(comptime UserContext: type) type {
fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{ fields = &(@as([fields.len]StructField, fields[0..fields.len].*) ++ [1]StructField{.{
.name = param.name ++ "", .name = param.name ++ "",
.type = FieldType, .type = FieldType,
.default_value_ptr = @ptrCast(&@as( .default_value = @ptrCast(&@as(
FieldType, FieldType,
if (PType.value_count == .count) 0 else null, if (PType.value_count == .count) 0 else null,
)), )),
@@ -519,8 +518,8 @@ pub fn CommandBuilder(comptime UserContext: type) type {
}}); }});
} }
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = fields, .fields = fields,
.decls = &.{}, .decls = &.{},
.is_tuple = false, .is_tuple = false,

View File

@@ -21,16 +21,16 @@ pub fn DefaultConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen
return if (comptime gen.multi) return if (comptime gen.multi)
MultiConverter(gen) MultiConverter(gen)
else switch (@typeInfo(gen.OutputType)) { else switch (@typeInfo(gen.OutputType)) {
.bool => FlagConverter(gen), .Bool => FlagConverter(gen),
.int => IntConverter(gen), .Int => IntConverter(gen),
.pointer => |info| if (info.size == .slice and info.child == u8) .Pointer => |info| if (info.size == .Slice and info.child == u8)
StringConverter(gen) StringConverter(gen)
else else
null, null,
.@"enum" => |info| if (info.is_exhaustive) ChoiceConverter(gen) else null, .Enum => |info| if (info.is_exhaustive) ChoiceConverter(gen) else null,
// TODO: how to handle structs with field defaults? maybe this should only work // TODO: how to handle structs with field defaults? maybe this should only work
// for tuples, which I don't think can have defaults. // for tuples, which I don't think can have defaults.
.@"struct" => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len) .Struct => |info| if (gen.value_count == .fixed and gen.value_count.fixed == info.fields.len)
StructConverter(gen) StructConverter(gen)
else else
null, null,
@@ -102,7 +102,7 @@ fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) { fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const StructType = gen.OutputType; const StructType = gen.OutputType;
const type_info = @typeInfo(StructType).@"struct"; const type_info = @typeInfo(StructType).Struct;
const Intermediate = gen.IntermediateType(); const Intermediate = gen.IntermediateType();
return struct { return struct {
@@ -120,7 +120,7 @@ fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const Converter = comptime DefaultConverter( const Converter = comptime DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{ ncmeta.copyStruct(ParameterGenerics, gen, .{
.OutputType = field.type, .OutputType = field.type,
.value_count = @as(parameters.ValueCount, .{ .fixed = 1 }), .value_count = .{ .fixed = 1 },
}), }),
) orelse ) orelse
@compileError("cannot get converter for field" ++ field.name); @compileError("cannot get converter for field" ++ field.name);

View File

@@ -57,7 +57,7 @@ pub fn StructuredPrinter(comptime Writer: type) type {
// this assumes output stream has already had the first line properly // this assumes output stream has already had the first line properly
// indented. // indented.
var splitter = std.mem.splitScalar(u8, text, '\n'); var splitter = std.mem.split(u8, text, "\n");
var location: usize = indent; var location: usize = indent;
while (splitter.next()) |line| { while (splitter.next()) |line| {
@@ -484,7 +484,7 @@ pub fn optInfo(comptime command: anytype) CommandHelp {
// than just the tag name. Roll our own eventually. // than just the tag name. Roll our own eventually.
blk: { blk: {
switch (@typeInfo(@TypeOf(def))) { switch (@typeInfo(@TypeOf(def))) {
.pointer => |info| if (info.size == .Slice and info.child == u8) { .Pointer => |info| if (info.size == .Slice and info.child == u8) {
writer.print("{s}", .{def}) catch @compileError("no"); writer.print("{s}", .{def}) catch @compileError("no");
break :blk; break :blk;
}, },

View File

@@ -10,7 +10,7 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime { comptime {
const inputInfo = @typeInfo(input); const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) { const fieldcount = switch (inputInfo) {
.@"struct" => |spec| blk: { .Struct => |spec| blk: {
if (spec.decls.len > 0) { if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++ @compileError("UpdateDefaults only works on structs " ++
"without decls due to limitations in @Type."); "without decls due to limitations in @Type.");
@@ -21,7 +21,7 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
}; };
var fields: [fieldcount]StructField = undefined; var fields: [fieldcount]StructField = undefined;
for (inputInfo.@"struct".fields, 0..) |field, idx| { for (inputInfo.Struct.fields, 0..) |field, idx| {
fields[idx] = .{ fields[idx] = .{
.name = field.name, .name = field.name,
.field_type = field.field_type, .field_type = field.field_type,
@@ -29,27 +29,27 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
// setting null defaults work, and it converts comptime_int to // setting null defaults work, and it converts comptime_int to
// the appropriate type, which is nice for ergonomics. Not sure // the appropriate type, which is nice for ergonomics. Not sure
// if it introduces weird edge cases. Probably it's fine? // if it introduces weird edge cases. Probably it's fine?
.default_value_ptr = if (@hasField(@TypeOf(defaults), field.name)) .default_value = if (@hasField(@TypeOf(defaults), field.name))
@ptrCast(&@as(field.field_type, @field(defaults, field.name))) @ptrCast(&@as(field.field_type, @field(defaults, field.name)))
else else
field.default_value_ptr, field.default_value,
.is_comptime = field.is_comptime, .is_comptime = field.is_comptime,
.alignment = field.alignment, .alignment = field.alignment,
}; };
} }
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = inputInfo.@"struct".layout, .layout = inputInfo.Struct.layout,
.backing_integer = inputInfo.@"struct".backing_integer, .backing_integer = inputInfo.Struct.backing_integer,
.fields = &fields, .fields = &fields,
.decls = inputInfo.@"struct".decls, .decls = inputInfo.Struct.decls,
.is_tuple = inputInfo.@"struct".is_tuple, .is_tuple = inputInfo.Struct.is_tuple,
} }); } });
} }
} }
pub fn enumLength(comptime T: type) comptime_int { pub fn enumLength(comptime T: type) comptime_int {
return @typeInfo(T).@"enum".fields.len; return @typeInfo(T).Enum.fields.len;
} }
pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T { pub fn partition(comptime T: type, input: []const T, wedge: []const []const T) [3][]const T {
@@ -210,11 +210,11 @@ pub fn MutatingZSplitter(comptime T: type) type {
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T { pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
var result: T = undefined; var result: T = undefined;
comptime for (@typeInfo(@TypeOf(field_overrides)).@"struct".fields) |field| { comptime for (@typeInfo(@TypeOf(field_overrides)).Struct.fields) |field| {
if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ field); if (!@hasField(T, field.name)) @compileError("override contains bad field" ++ field);
}; };
inline for (comptime @typeInfo(T).@"struct".fields) |field| { inline for (comptime @typeInfo(T).Struct.fields) |field| {
if (comptime @hasField(@TypeOf(field_overrides), field.name)) if (comptime @hasField(@TypeOf(field_overrides), field.name))
@field(result, field.name) = @field(field_overrides, field.name) @field(result, field.name) = @field(field_overrides, field.name)
else else
@@ -257,15 +257,15 @@ pub const TupleBuilder = struct {
fields[idx] = .{ fields[idx] = .{
.name = std.fmt.comptimePrint("{d}", .{idx}), .name = std.fmt.comptimePrint("{d}", .{idx}),
.type = Type, .type = Type,
.default_value_ptr = null, .default_value = null,
// TODO: is this the right thing to do? // TODO: is this the right thing to do?
.is_comptime = false, .is_comptime = false,
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0, .alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
}; };
} }
return @Type(.{ .@"struct" = .{ return @Type(.{ .Struct = .{
.layout = .auto, .layout = .Auto,
.fields = &fields, .fields = &fields,
.decls = &.{}, .decls = &.{},
.is_tuple = true, .is_tuple = true,

View File

@@ -48,11 +48,11 @@ pub const ParameterGenerics = struct {
pub fn fixedValueCount(comptime OutputType: type, comptime value_count: ValueCount) ValueCount { pub fn fixedValueCount(comptime OutputType: type, comptime value_count: ValueCount) ValueCount {
return comptime if (value_count == .fixed) return comptime if (value_count == .fixed)
switch (@typeInfo(OutputType)) { switch (@typeInfo(OutputType)) {
.@"struct" => |info| .{ .fixed = info.fields.len }, .Struct => |info| .{ .fixed = info.fields.len },
.array => |info| .{ .fixed = info.len }, .Array => |info| .{ .fixed = info.len },
// TODO: this is a bit sloppy, but it can be refined later. // TODO: this is a bit sloppy, but it can be refined later.
// .Pointer covers slices, which may be a many-to-many conversion. // .Pointer covers slices, which may be a many-to-many conversion.
.pointer => value_count, .Pointer => value_count,
else => .{ .fixed = 1 }, else => .{ .fixed = 1 },
} }
else else