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");
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 noclip = b.addModule("noclip", .{
.root_source_file = b.path("source/noclip.zig"),
.source_file = .{ .path = "source/noclip.zig" },
});
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 tests = b.addTest(.{
.name = "tests",
.root_source_file = b.path("source/noclip.zig"),
.root_source_file = .{ .path = "source/noclip.zig" },
.target = target,
.optimize = optimize,
});
@@ -24,18 +24,18 @@ pub fn build(b: *std.Build) void {
fn demo(
b: *std.Build,
noclip: *std.Build.Module,
target: std.Build.ResolvedTarget,
target: std.zig.CrossTarget,
optimize: std.builtin.Mode,
) void {
const demo_step = b.step("demo", "Build and install CLI demo program");
const exe = b.addExecutable(.{
.name = "noclip-demo",
.root_source_file = b.path("demo/demo.zig"),
.root_source_file = .{ .path = "demo/demo.zig" },
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("noclip", noclip);
exe.addModule("noclip", noclip);
const install_demo = b.addInstallArtifact(exe, .{});
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
Requires Zig `0.13.x`. May work with `0.12.x`.
Requires Zig `0.11.x`.
=== Features

View File

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

View File

@@ -21,16 +21,16 @@ pub fn DefaultConverter(comptime gen: ParameterGenerics) ?ConverterSignature(gen
return if (comptime gen.multi)
MultiConverter(gen)
else switch (@typeInfo(gen.OutputType)) {
.bool => FlagConverter(gen),
.int => IntConverter(gen),
.pointer => |info| if (info.size == .slice and info.child == u8)
.Bool => FlagConverter(gen),
.Int => IntConverter(gen),
.Pointer => |info| if (info.size == .Slice and info.child == u8)
StringConverter(gen)
else
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
// 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)
else
null,
@@ -102,7 +102,7 @@ fn IntConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const StructType = gen.OutputType;
const type_info = @typeInfo(StructType).@"struct";
const type_info = @typeInfo(StructType).Struct;
const Intermediate = gen.IntermediateType();
return struct {
@@ -120,7 +120,7 @@ fn StructConverter(comptime gen: ParameterGenerics) ConverterSignature(gen) {
const Converter = comptime DefaultConverter(
ncmeta.copyStruct(ParameterGenerics, gen, .{
.OutputType = field.type,
.value_count = @as(parameters.ValueCount, .{ .fixed = 1 }),
.value_count = .{ .fixed = 1 },
}),
) orelse
@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
// indented.
var splitter = std.mem.splitScalar(u8, text, '\n');
var splitter = std.mem.split(u8, text, "\n");
var location: usize = indent;
while (splitter.next()) |line| {
@@ -484,7 +484,7 @@ pub fn optInfo(comptime command: anytype) CommandHelp {
// than just the tag name. Roll our own eventually.
blk: {
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");
break :blk;
},

View File

@@ -10,7 +10,7 @@ pub fn UpdateDefaults(comptime input: type, comptime defaults: anytype) type {
comptime {
const inputInfo = @typeInfo(input);
const fieldcount = switch (inputInfo) {
.@"struct" => |spec| blk: {
.Struct => |spec| blk: {
if (spec.decls.len > 0) {
@compileError("UpdateDefaults only works on structs " ++
"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;
for (inputInfo.@"struct".fields, 0..) |field, idx| {
for (inputInfo.Struct.fields, 0..) |field, idx| {
fields[idx] = .{
.name = field.name,
.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
// the appropriate type, which is nice for ergonomics. Not sure
// 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)))
else
field.default_value_ptr,
field.default_value,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
};
}
return @Type(.{ .@"struct" = .{
.layout = inputInfo.@"struct".layout,
.backing_integer = inputInfo.@"struct".backing_integer,
return @Type(.{ .Struct = .{
.layout = inputInfo.Struct.layout,
.backing_integer = inputInfo.Struct.backing_integer,
.fields = &fields,
.decls = inputInfo.@"struct".decls,
.is_tuple = inputInfo.@"struct".is_tuple,
.decls = inputInfo.Struct.decls,
.is_tuple = inputInfo.Struct.is_tuple,
} });
}
}
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 {
@@ -210,11 +210,11 @@ pub fn MutatingZSplitter(comptime T: type) type {
pub fn copyStruct(comptime T: type, source: T, field_overrides: anytype) T {
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);
};
inline for (comptime @typeInfo(T).@"struct".fields) |field| {
inline for (comptime @typeInfo(T).Struct.fields) |field| {
if (comptime @hasField(@TypeOf(field_overrides), field.name))
@field(result, field.name) = @field(field_overrides, field.name)
else
@@ -257,15 +257,15 @@ pub const TupleBuilder = struct {
fields[idx] = .{
.name = std.fmt.comptimePrint("{d}", .{idx}),
.type = Type,
.default_value_ptr = null,
.default_value = null,
// TODO: is this the right thing to do?
.is_comptime = false,
.alignment = if (@sizeOf(Type) > 0) @alignOf(Type) else 0,
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
return @Type(.{ .Struct = .{
.layout = .Auto,
.fields = &fields,
.decls = &.{},
.is_tuple = true,

View File

@@ -48,11 +48,11 @@ pub const ParameterGenerics = struct {
pub fn fixedValueCount(comptime OutputType: type, comptime value_count: ValueCount) ValueCount {
return comptime if (value_count == .fixed)
switch (@typeInfo(OutputType)) {
.@"struct" => |info| .{ .fixed = info.fields.len },
.array => |info| .{ .fixed = info.len },
.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.
.pointer => value_count,
.Pointer => value_count,
else => .{ .fixed = 1 },
}
else