llm
Download
1241 lines 61.3 KiBCModified 2026-08-31 raw
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241
// Tests for the command-line argument parser (src/stdlib/cli.c), which is
// shared by the `tomo` compiler and by every program it compiles.
// To run the test: make test-cli
//
// The parser answers by printing and exiting, with errors to stderr and
// --help/--version to stdout, so those cases run in a forked child (see
// run_in_child) and assert on the child's exit status and output.

#include "cli.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include "bool.h"
#include "byte.h"
#include "c_string.h"
#include "datatypes.h"
#include "datatypes/typeinfo.h"
#include "enums.h"
#include "floats.h"
#include "integers.h"
#include "list.h"
#include "metamethods.h"
#include "num.h"
#include "optionals.h"
#include "path.h"
#include "pointers.h"
#include "print.h"
#include "structs.h"
#include "table.h"
#include "text.h"

static int checks = 0;
static int failures = 0;

#define FAIL(...)                                                                                                      \
    do {                                                                                                               \
        fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__);                                                           \
        fprintf(stderr, __VA_ARGS__);                                                                                  \
        fputc('\n', stderr);                                                                                           \
        failures++;                                                                                                    \
    } while (0)

#define CHECK(cond)                                                                                                    \
    do {                                                                                                               \
        checks++;                                                                                                      \
        if (!(cond)) FAIL("%s", #cond);                                                                                \
    } while (0)

// Compare a value against its rendering, using the type's own as_text. Gives a
// readable diff for the compound types (lists, tables, optionals).
#define CHECK_TEXT(value_ptr, type, want)                                                                              \
    do {                                                                                                               \
        checks++;                                                                                                      \
        const char *_got = Text$as_c_string(generic_as_text((value_ptr), false, (type)));                              \
        if (strcmp(_got, (want)) != 0) FAIL("got `%s`, want `%s`", _got, (want));                                      \
    } while (0)

#define CHECK_STR(got, want)                                                                                           \
    do {                                                                                                               \
        checks++;                                                                                                      \
        const char *_g = (got), *_w = (want);                                                                          \
        if (_g == NULL || strcmp(_g, _w) != 0) FAIL("got `%s`, want `%s`", _g ? _g : "(null)", _w);                    \
    } while (0)

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// An argument list, as the parser takes it:
#define ARGS(...) TypedList(const char *, __VA_ARGS__)

// The arguments left over after a pop, joined with spaces, i.e. what the parser
// would go on to treat as positional values.
static const char *leftovers(List_t args) {
    return CString$join(" ", args);
}

// Pop `flag` from `args` and check what it parsed and what it left behind.
// The parsed value is compared through its own type's as_text, so one form
// covers every argument type: a struct renders as `Point{x=1, y=2}`, an
// optional as its value or `none`, a list as `[1, 2]`.
#define CHECK_POP(type, initial, short_flag, flag, args, want_value, want_left)                                        \
    do {                                                                                                               \
        __typeof(initial) _v = (initial);                                                                              \
        List_t _args = (args);                                                                                         \
        checks++;                                                                                                      \
        if (!pop_cli_flag(&_args, (short_flag), (flag), &_v, (type))) FAIL("--%s was not popped", (flag));             \
        else {                                                                                                         \
            CHECK_TEXT(&_v, (type), (want_value));                                                                     \
            CHECK_STR(leftovers(_args), (want_left));                                                                  \
        }                                                                                                              \
    } while (0)

// The same, for a flag that isn't present: nothing is parsed, nothing consumed.
#define CHECK_NO_POP(type, initial, short_flag, flag, args, want_value, want_left)                                     \
    do {                                                                                                               \
        __typeof(initial) _v = (initial);                                                                              \
        List_t _args = (args);                                                                                         \
        checks++;                                                                                                      \
        if (pop_cli_flag(&_args, (short_flag), (flag), &_v, (type))) FAIL("--%s was popped", (flag));                  \
        else {                                                                                                         \
            CHECK_TEXT(&_v, (type), (want_value));                                                                     \
            CHECK_STR(leftovers(_args), (want_left));                                                                  \
        }                                                                                                              \
    } while (0)

// ...and for a value filled positionally rather than by flag.
#define CHECK_POSITIONAL(type, initial, args, allow_dashes, want_value, want_left)                                     \
    do {                                                                                                               \
        __typeof(initial) _v = (initial);                                                                              \
        List_t _args = (args);                                                                                         \
        checks++;                                                                                                      \
        if (!pop_cli_positional(&_args, "arg", &_v, (type), (allow_dashes))) FAIL("nothing was filled");               \
        else {                                                                                                         \
            CHECK_TEXT(&_v, (type), (want_value));                                                                     \
            CHECK_STR(leftovers(_args), (want_left));                                                                  \
        }                                                                                                              \
    } while (0)

// Run `fn(arg)` in a forked child with stdout and stderr captured, and return
// the child's exit status. Used for the cases that print and exit.
static int run_in_child(void (*fn)(void *), void *arg, char *output, size_t cap) {
    fflush(NULL);
    int pipe_fds[2];
    if (pipe(pipe_fds) != 0) {
        perror("pipe");
        exit(1);
    }
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    }
    if (pid == 0) {
        close(pipe_fds[0]);
        dup2(pipe_fds[1], STDOUT_FILENO);
        dup2(pipe_fds[1], STDERR_FILENO);
        close(pipe_fds[1]);
        fn(arg);
        fflush(NULL);
        _exit(0);
    }
    close(pipe_fds[1]);
    size_t len = 0;
    for (;;) {
        ssize_t n = read(pipe_fds[0], output + len, cap - 1 - len);
        if (n <= 0) break;
        len += (size_t)n;
        if (len >= cap - 1) break;
    }
    output[len] = '\0';
    close(pipe_fds[0]);
    int status = 0;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

// A whole `tomo_parse_arg_list` invocation, packaged so it can be run in a
// child process:
typedef struct {
    List_t args;
    cli_help_info_t info;
    int spec_len;
    cli_arg_t *spec;
} parse_case_t;

static void do_parse(void *p) {
    parse_case_t *c = p;
    tomo_parse_arg_list(c->args, c->info, c->spec_len, c->spec);
}

// Assert that parsing exits nonzero with a message containing `want`:
static void expect_parse_error(parse_case_t *c, const char *want, int line) {
    checks++;
    char output[8192];
    int status = run_in_child(do_parse, c, output, sizeof(output));
    if (status == 0) FAIL("(line %d) expected a failure, but it succeeded: %s", line, output);
    else if (strstr(output, want) == NULL)
        FAIL("(line %d) expected an error mentioning `%s`, got: %s", line, want, output);
}

#define EXPECT_PARSE_ERROR(case_ptr, want) expect_parse_error((case_ptr), (want), __LINE__)

// Assert that parsing exits 0 (the --help path) and prints something
// containing `want`:
static void expect_parse_output(parse_case_t *c, const char *want, int line) {
    checks++;
    char output[8192];
    int status = run_in_child(do_parse, c, output, sizeof(output));
    if (status != 0) FAIL("(line %d) expected success, got status %d: %s", line, status, output);
    else if (strstr(output, want) == NULL)
        FAIL("(line %d) expected output mentioning `%s`, got: %s", line, want, output);
}

#define EXPECT_PARSE_OUTPUT(case_ptr, want) expect_parse_output((case_ptr), (want), __LINE__)

// ---------------------------------------------------------------------------
// Type infos for the compound types, synthesized here the way the compiler
// emits them. Used throughout: as argument types, as list/table contents, and
// as the things optional and pointer wrappers wrap.
// ---------------------------------------------------------------------------

static NamedType_t point_fields[] = {{"x", &Int32$info}, {"y", &Int32$info}};
static const TypeInfo_t Point$info = {
    .size = 2 * sizeof(int32_t),
    .align = __alignof__(int32_t),
    .tag = StructInfo,
    .StructInfo.name = "Point",
    .StructInfo.fields = point_fields,
    .StructInfo.num_fields = 2,
    .metamethods = Struct$metamethods,
};
typedef struct {
    int32_t x, y;
} Point_t;
typedef struct {
    Point_t value;
    bool has_value;
} OptionalPoint_t;

// A struct whose leading field isn't numeric, so its values can't start with
// a dash:
static NamedType_t label_fields[] = {{"name", &Text$info}, {"n", &Int32$info}};
static const TypeInfo_t Label$info = {
    .size = sizeof(Text_t) + sizeof(int32_t),
    .align = __alignof__(Text_t),
    .tag = StructInfo,
    .StructInfo.name = "Label",
    .StructInfo.fields = label_fields,
    .StructInfo.num_fields = 2,
    .metamethods = Struct$metamethods,
};
typedef struct {
    Text_t name;
    int32_t n;
} Label_t;

// An enum of plain tags (no associated values), like `enum Color(Red, Green, Blue)`:
static NamedType_t color_tags[] = {{"Red", NULL}, {"Green", NULL}, {"Blue", NULL}};
static const TypeInfo_t Color$info = {
    .size = sizeof(int32_t),
    .align = __alignof__(int32_t),
    .tag = EnumInfo,
    .EnumInfo.name = "Color",
    .EnumInfo.tags = color_tags,
    .EnumInfo.num_tags = 3,
    .metamethods = Enum$metamethods,
};

// An enum whose tags carry values, like `enum Shape(Circle(r:Int32), Rect(x,y:Int32))`:
static NamedType_t circle_fields[] = {{"r", &Int32$info}};
static const TypeInfo_t Circle$info = {
    .size = sizeof(int32_t),
    .align = __alignof__(int32_t),
    .tag = StructInfo,
    .StructInfo.name = "Circle",
    .StructInfo.fields = circle_fields,
    .StructInfo.num_fields = 1,
    .metamethods = Struct$metamethods,
};
static NamedType_t shape_tags[] = {{"Circle", &Circle$info}, {"Rect", &Point$info}};
typedef struct {
    int32_t tag;
    union {
        struct {
            int32_t r;
        } Circle;
        Point_t Rect;
    };
} Shape_t;
static const TypeInfo_t Shape$info = {
    .size = sizeof(int32_t) + 2 * sizeof(int32_t),
    .align = __alignof__(int32_t),
    .tag = EnumInfo,
    .EnumInfo.name = "Shape",
    .EnumInfo.tags = shape_tags,
    .EnumInfo.num_tags = 2,
    .metamethods = Enum$metamethods,
};

// ---------------------------------------------------------------------------
// pop_cli_flag: the long-flag versions
// ---------------------------------------------------------------------------

static void test_long_flags(void) {
    // --flag value, and --flag=value, which consumes only the one token:
    CHECK_POP(&Int$info, I(0), 0, "count", ARGS("--count", "42", "rest"), "42", "rest");
    CHECK_POP(&Int$info, I(0), 0, "count", ARGS("--count=42", "rest"), "42", "rest");

    // A flag can appear anywhere in the list, not just first:
    CHECK_POP(&Int$info, I(0), 0, "count", ARGS("first", "--count", "42", "last"), "42", "first last");

    // An absent flag leaves both the value and the arguments untouched:
    CHECK_NO_POP(&Int$info, I(7), 0, "count", ARGS("a", "b"), "7", "a b");

    // A longer flag that merely starts with this flag's name is not a match:
    CHECK_NO_POP(&Int$info, I(0), 0, "count", ARGS("--counts", "42"), "0", "--counts 42");

    // A bare "--" ends flag parsing: what follows is values, not flags.
    CHECK_NO_POP(&Int$info, I(0), 0, "count", ARGS("--", "--count", "42"), "0", "-- --count 42");
    CHECK_NO_POP(&Int$info, I(-5), 0, "count", ARGS("--", "--count", "-1"), "-5", "-- --count -1");
}

// ---------------------------------------------------------------------------
// pop_cli_flag: the short-flag versions, including clusters
// ---------------------------------------------------------------------------

static void test_short_flags(void) {
    // -f value, -f=value, and -fVALUE (e.g. -O3):
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-n", "42", "rest"), "42", "rest");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-n=42", "rest"), "42", "rest");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-n42", "rest"), "42", "rest");

    // In a cluster, the flag takes its value from the next argument (or from
    // inside the token) and the rest of the cluster is left for the others:
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-abn", "42", "rest"), "42", "-ab rest");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-abn=42", "rest"), "42", "-ab rest");
    // The trailing ";" stops the leftover from being read as another cluster
    // with a value: `-ab1 2` must not parse as b=1, then a=2:
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-abn42", "rest"), "42", "-ab; rest");

    // With no short flag registered, -n is just another argument:
    CHECK_NO_POP(&Int$info, I(0), 0, "count", ARGS("-n", "42"), "0", "-n 42");

    // A long flag and its short alias both fill the same spec entry:
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("--count", "1"), "1", "");
}

// ---------------------------------------------------------------------------
// Boolean flags, which have their own versions (--no-flag, --flag=yes, ...)
// ---------------------------------------------------------------------------

static void test_boolean_flags(void) {
    // A bare --flag means yes, --no-flag means no, and both are consumed whole:
    CHECK_POP(&Bool$info, false, 'f', "force", ARGS("--force", "rest"), "yes", "rest");
    CHECK_POP(&Bool$info, true, 'f', "force", ARGS("--no-force", "rest"), "no", "rest");

    // --flag=<boolean> and -f=<boolean>, in every form Bool$parse accepts:
    static const char *const yes_values[] = {"yes", "true", "on", "1"};
    static const char *const no_values[] = {"no", "false", "off", "0"};
    for (size_t i = 0; i < sizeof(yes_values) / sizeof(yes_values[0]); i++) {
        CHECK_POP(&Bool$info, false, 'f', "force", ARGS(String("--force=", yes_values[i])), "yes", "");
        CHECK_POP(&Bool$info, false, 'f', "force", ARGS(String("-f=", yes_values[i])), "yes", "");
        CHECK_POP(&Bool$info, true, 'f', "force", ARGS(String("--force=", no_values[i])), "no", "");
        CHECK_POP(&Bool$info, true, 'f', "force", ARGS(String("-f=", no_values[i])), "no", "");
    }

    // A bare short flag takes no value, so it can be clustered with others:
    CHECK_POP(&Bool$info, false, 'f', "force", ARGS("-abfc", "rest"), "yes", "-abc rest");
    CHECK_POP(&Bool$info, true, 'f', "force", ARGS("-abf=no", "rest"), "no", "-ab rest");

    // A boolean flag never swallows the following argument:
    CHECK_POP(&Bool$info, false, 'f', "force", ARGS("--force", "no"), "yes", "no");

    // A longer flag that merely starts with this one's name is not a match, in
    // any of the boolean values:
    CHECK_NO_POP(&Bool$info, false, 'f', "force", ARGS("--forced"), "no", "--forced");
    CHECK_NO_POP(&Bool$info, false, 'f', "force", ARGS("--forced=yes"), "no", "--forced=yes");
    CHECK_NO_POP(&Bool$info, false, 'f', "force", ARGS("--no-forced"), "no", "--no-forced");

    // ...and it stops at a bare "--" like every other flag:
    CHECK_NO_POP(&Bool$info, false, 'f', "force", ARGS("--", "--force"), "no", "-- --force");
}

// ---------------------------------------------------------------------------
// Value types
// ---------------------------------------------------------------------------

static void test_scalar_types(void) {
    // Each numeric type parses up to its own limits:
    CHECK_POP(&Int64$info, (int64_t)0, 0, "n", ARGS("--n", "9223372036854775807"), "9223372036854775807", "");
    CHECK_POP(&Int32$info, (int32_t)0, 0, "n", ARGS("--n=2147483647"), "2147483647", "");
    CHECK_POP(&Int16$info, (int16_t)0, 0, "n", ARGS("--n=32767"), "32767", "");
    CHECK_POP(&Int8$info, (int8_t)0, 0, "n", ARGS("--n=127"), "127", "");
    CHECK_POP(&Byte$info, (uint8_t)0, 0, "n", ARGS("--n=255"), "0xff", ""); // bytes render in hex
    // Big integers are arbitrary-precision, so they aren't capped at 64 bits:
    CHECK_POP(&Int$info, I(0), 0, "n", ARGS("--n=123456789012345678901234567890"), "123456789012345678901234567890",
              "");
    CHECK_POP(&Float64$info, 0.0, 0, "x", ARGS("--x=1.5"), "1.5", "");
    CHECK_POP(&Float32$info, 0.0f, 0, "x", ARGS("--x=1.5"), "1.5", "");
    CHECK_POP(&Num$info, NONE_NUM, 0, "x", ARGS("--x=1.5"), "1.5", "");

    // The negative extremes are reachable positionally, where dashes are
    // allowed (as a flag value they need the numeric exception; see
    // test_negative_numbers):
    CHECK_POSITIONAL(&Int64$info, (int64_t)0, ARGS("-9223372036854775808"), true, "-9223372036854775808", "");
    CHECK_POSITIONAL(&Int8$info, (int8_t)0, ARGS("-128", "rest"), true, "-128", "rest");

    // Text is taken verbatim, and a leading backslash escapes a value that
    // would otherwise look like a flag:
    CHECK_POP(&Text$info, EMPTY_TEXT, 0, "name", ARGS("--name", "Åsa Ünicode"), "\"Åsa Ünicode\"", "");
    CHECK_POP(&Text$info, EMPTY_TEXT, 0, "name", ARGS("--name", "\\-dash"), "\"-dash\"", "");
    CHECK_POP(&CString$info, (const char *)NULL, 0, "name", ARGS("--name=hello"), "\"hello\"", "");
    CHECK_POP(&Path$info, (Path_t)NULL, 0, "file", ARGS("--file", "/tmp/x.txt"), "/tmp/x.txt", "");

    // Booleans are also parseable as a plain value type:
    CHECK_POP(&Bool$info, false, 'f', "force", ARGS("--force=yes"), "yes", "");

    // A pointer argument allocates the pointed-to value and parses into it:
    {
        Int32_t *ptr = NULL;
        List_t args = ARGS("--n", "42");
        CHECK(pop_cli_flag(&args, 0, "n", &ptr, Pointer$info("@", &Int32$info)));
        CHECK(ptr && *ptr == 42);
    }
}

// An optional argument renders as its value or as `none`, which is exactly
// what has to be checked, whichever way the type represents none internally
// (a has_value byte, a NaN, a NULL pointer, a reserved bit pattern).
#define OPT(t, inner) Optional$info(sizeof(t), __alignof__(t), (inner))

static void test_optional_types(void) {
    const TypeInfo_t *opt_int = OPT(Int_t, &Int$info);
    const TypeInfo_t *opt_i64 = OPT(OptionalInt64_t, &Int64$info);
    const TypeInfo_t *opt_byte = OPT(OptionalByte_t, &Byte$info);
    const TypeInfo_t *opt_text = OPT(Text_t, &Text$info);
    const TypeInfo_t *opt_path = OPT(Path_t, &Path$info);
    const TypeInfo_t *opt_f64 = OPT(double, &Float64$info);

    // "none" produces the none value for each representation...
    CHECK_POP(opt_int, I(1), 0, "n", ARGS("--n=none"), "none", "");
    CHECK_POP(opt_i64, ((OptionalInt64_t){.value = 1, .has_value = true}), 0, "n", ARGS("--n=none"), "none", "");
    CHECK_POP(opt_text, (OptionalText_t)Text("x"), 0, "name", ARGS("--name=none"), "none", "");
    CHECK_POP(opt_path, (OptionalPath_t)Path("/x"), 0, "file", ARGS("--file=none"), "none", "");
    CHECK_POP(opt_f64, 1.0, 0, "x", ARGS("--x=none"), "none", "");

    // ...and a real value sets both the value and, where there is one, the flag:
    CHECK_POP(opt_int, NONE_INT, 0, "n", ARGS("--n=42"), "42", "");
    CHECK_POP(opt_i64, ((OptionalInt64_t){0}), 0, "n", ARGS("--n=42"), "42", "");
    CHECK_POP(opt_byte, ((OptionalByte_t){0}), 0, "n", ARGS("--n=7"), "0x07", "");

    // An optional struct carries its has_value flag in a byte past the value:
    CHECK_POP(OPT(OptionalPoint_t, &Point$info), ((OptionalPoint_t){0}), 0, "at", ARGS("--at", "3", "4"),
              "Point{x=3, y=4}", "");
}

static void test_compound_types(void) {
    const TypeInfo_t *texts = List$info(&Text$info), *ints = List$info(&Int32$info);
    const TypeInfo_t *table = Table$info(&Text$info, &Int32$info), *set = Set$info(&Text$info);

    // A list flag consumes every following non-flag argument...
    CHECK_POP(texts, EMPTY_LIST, 0, "files", ARGS("--files", "a", "b", "c", "--other"), "[\"a\", \"b\", \"c\"]",
              "--other");
    // ...or splits on commas when the values are inside the token:
    CHECK_POP(texts, EMPTY_LIST, 0, "files", ARGS("--files=a,b,c", "rest"), "[\"a\", \"b\", \"c\"]", "rest");
    CHECK_POP(texts, EMPTY_LIST, 'f', "files", ARGS("-fa,b,c"), "[\"a\", \"b\", \"c\"]", "");
    CHECK_POP(ints, EMPTY_LIST, 0, "nums", ARGS("--nums", "1", "2", "3"), "[1, 2, 3]", "");
    // A flag with nothing parseable after it is an empty list, not a failure:
    CHECK_POP(ints, EMPTY_LIST, 0, "nums", ARGS("--nums", "--other"), "[]", "--other");

    // A table takes key:value pairs, and stops at the first thing that isn't one:
    CHECK_POP(table, EMPTY_TABLE, 0, "defs", ARGS("--defs", "a:1", "b:2", "not-a-pair"), "{\"a\": 1, \"b\": 2}",
              "not-a-pair");
    CHECK_POP(table, EMPTY_TABLE, 0, "defs", ARGS("--defs=a:1,b:2"), "{\"a\": 1, \"b\": 2}", "");
    // A set (a table with no values) takes bare keys:
    CHECK_POP(set, EMPTY_TABLE, 0, "tags", ARGS("--tags", "a", "b", "--other"), "{\"a\", \"b\"}", "--other");

    // A struct consumes one argument per field, in declaration order:
    CHECK_POP(&Point$info, ((Point_t){0, 0}), 0, "at", ARGS("--at", "3", "4", "rest"), "Point{x=3, y=4}", "rest");

    // An enum of plain tags is matched by name...
    CHECK_POP(&Color$info, (int32_t)0, 0, "color", ARGS("--color", "Green"), "Green", "");
    // ...and a tag with associated values parses them from what follows. The
    // tag number is 1-based, as the compiler numbers tags; the payload is
    // checked field by field rather than by rendering, since how this
    // synthesized enum prints is beside the point.
    {
        Shape_t shape = {0};
        List_t args = ARGS("--shape", "Circle", "5");
        CHECK(pop_cli_flag(&args, 0, "shape", &shape, &Shape$info));
        CHECK(shape.tag == 1 && shape.Circle.r == 5);
    }
    {
        Shape_t shape = {0};
        List_t args = ARGS("--shape", "Rect", "2", "3");
        CHECK(pop_cli_flag(&args, 0, "shape", &shape, &Shape$info));
        CHECK(shape.tag == 2 && shape.Rect.x == 2 && shape.Rect.y == 3);
    }
}

// ---------------------------------------------------------------------------
// Optional arguments whose value type is itself a container or a boolean.
// These carry their own `none` (a bit pattern a parsed value can't collide
// with) rather than a separate has_value flag.
// ---------------------------------------------------------------------------

static void test_optional_containers(void) {
    const TypeInfo_t *ints = List$info(&Int32$info), *table = Table$info(&Text$info, &Int32$info);
    const TypeInfo_t *set = Set$info(&Text$info);
    const TypeInfo_t *opt_list = OPT(List_t, ints), *opt_table = OPT(Table_t, table), *opt_set = OPT(Table_t, set);

    // These start out as NONE_LIST/NONE_TABLE (a NULL data pointer), which is
    // what the compiler emits for an argument with no default:
    CHECK_POP(opt_list, (OptionalList_t)NONE_LIST, 0, "nums", ARGS("--nums", "1", "2"), "[1, 2]", "");
    CHECK_POP(opt_list, (OptionalList_t)NONE_LIST, 0, "nums", ARGS("--nums=1,2"), "[1, 2]", "");
    CHECK_POP(opt_table, (OptionalTable_t)NONE_TABLE, 0, "defs", ARGS("--defs", "a:1", "b:2"), "{\"a\": 1, \"b\": 2}",
              "");
    CHECK_POP(opt_set, (OptionalTable_t)NONE_TABLE, 0, "tags", ARGS("--tags", "a", "b"), "{\"a\", \"b\"}", "");

    // A flag given with no values of its own is empty, not none, since the flag
    // was still given:
    CHECK_POP(opt_list, (OptionalList_t)NONE_LIST, 0, "nums", ARGS("--nums", "--other"), "[]", "--other");
    CHECK_POP(opt_table, (OptionalTable_t)NONE_TABLE, 0, "defs", ARGS("--defs", "--other"), "{}", "--other");
    CHECK_POP(opt_set, (OptionalTable_t)NONE_TABLE, 0, "tags", ARGS("--tags", "--other"), "{}", "--other");

    // "none" restores it, and a value already there is appended to:
    CHECK_POP(opt_list, (OptionalList_t)EMPTY_LIST, 0, "nums", ARGS("--nums=none"), "none", "");
    CHECK_POP(opt_table, (OptionalTable_t)EMPTY_TABLE, 0, "defs", ARGS("--defs=none"), "none", "");
    CHECK_POP(opt_set, (OptionalTable_t)EMPTY_TABLE, 0, "tags", ARGS("--tags=none"), "none", "");
    CHECK_POP(opt_list, (OptionalList_t)TypedList(int32_t, 9), 0, "nums", ARGS("--nums", "1"), "[9, 1]", "");
}

// An optional boolean is a boolean flag too, since is_bool_arg() looks through
// the optional, so the help text advertises it as a toggle, with `--flag=none`
// for the third value.
static void test_optional_booleans(void) {
    const TypeInfo_t *opt_bool = OPT(OptionalBool_t, &Bool$info);

    // Every boolean value, plus `none` for the third value:
    static const struct {
        const char *arg, *want;
    } cases[] = {
        {"--force", "yes"},       {"--no-force", "no"},  {"--force=yes", "yes"}, {"--force=no", "no"},
        {"--force=true", "yes"},  {"--force=off", "no"}, {"--force=1", "yes"},   {"--force=0", "no"},
        {"--force=none", "none"}, {"-f", "yes"},         {"-f=no", "no"},        {"-f=none", "none"},
    };
    for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++)
        CHECK_POP(opt_bool, (OptionalBool_t)99, 'f', "force", ARGS(cases[i].arg), cases[i].want, "");

    // It behaves like any other boolean flag: it takes no value from the
    // following argument, it clusters, and an absent flag changes nothing.
    CHECK_POP(opt_bool, NONE_BOOL, 'f', "force", ARGS("--force", "yes"), "yes", "yes");
    CHECK_POP(opt_bool, NONE_BOOL, 'f', "force", ARGS("-abfc"), "yes", "-abc");
    CHECK_NO_POP(opt_bool, NONE_BOOL, 'f', "force", ARGS("--other"), "none", "--other");

    // Given positionally it goes through the general value path instead, and
    // has to reach the same three values:
    CHECK_POSITIONAL(opt_bool, (OptionalBool_t)99, ARGS("yes"), false, "yes", "");
    CHECK_POSITIONAL(opt_bool, (OptionalBool_t)99, ARGS("no"), false, "no", "");
    CHECK_POSITIONAL(opt_bool, (OptionalBool_t)no, ARGS("none"), false, "none", "");

    // "none" is a value, not a way to smuggle a bad one past the parser:
    {
        OptionalBool_t force = no;
        cli_arg_t spec[] = {{.name = "force", .dest = &force, .type = opt_bool, .short_flag = 'f'}};
        cli_help_info_t info = {.usage = Text("Usage: prog")};
        parse_case_t c = {.args = ARGS("--force=banana"), .info = info, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Invalid boolean value for flag force");
        parse_case_t short_c = {.args = ARGS("-f=banana"), .info = info, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&short_c, "Invalid boolean value for flag -f");
    }

    // ...and a *non*-optional boolean has no "none" at all:
    {
        bool force = false;
        cli_arg_t spec[] = {{.name = "force", .dest = &force, .type = &Bool$info}};
        parse_case_t c = {
            .args = ARGS("--force=none"), .info = {.usage = Text("Usage: prog")}, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Invalid boolean value for flag force");
    }

    // The help text advertises it as the toggle it is:
    {
        OptionalBool_t force = NONE_BOOL;
        cli_arg_t spec[] = {{.name = "force", .dest = &force, .type = opt_bool, .short_flag = 'f', .required = true}};
        CHECK_STR(Text$as_c_string(tomo_generate_usage(Text("prog"), 1, spec)), "Usage: prog --force|-f");
    }
}

// ---------------------------------------------------------------------------
// Negative numbers: a dashed token is a value, not a flag, when the argument
// it fills is numeric.
// ---------------------------------------------------------------------------

static void test_negative_numbers(void) {
    const TypeInfo_t *ints = List$info(&Int32$info), *texts = List$info(&Text$info);

    // Make sure negative values are parsed right:
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("--count", "-1"), "-1", "");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("--count=-1"), "-1", "");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-n", "-1"), "-1", "");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-n=-1"), "-1", "");
    CHECK_POP(&Int$info, I(0), 'n', "count", ARGS("-n-1"), "-1", "");

    // Every numeric type takes one, in both integer and fractional forms:
    CHECK_POP(&Int64$info, (int64_t)0, 0, "n", ARGS("--n=-64"), "-64", "");
    CHECK_POP(&Int32$info, (int32_t)0, 0, "n", ARGS("--n=-32"), "-32", "");
    CHECK_POP(&Int16$info, (int16_t)0, 0, "n", ARGS("--n=-16"), "-16", "");
    CHECK_POP(&Int8$info, (int8_t)0, 0, "n", ARGS("--n=-8"), "-8", "");
    CHECK_POP(&Float64$info, 0.0, 0, "x", ARGS("--x", "-.5"), "-0.5", ""); // may lead with the dot
    CHECK_POP(&Float32$info, 0.0f, 0, "x", ARGS("--x=-1.5"), "-1.5", "");
    CHECK_POP(&Num$info, NONE_NUM, 0, "x", ARGS("--x=-1.5e3"), "-1500", "");
    CHECK_POP(OPT(OptionalInt64_t, &Int64$info), ((OptionalInt64_t){0}), 0, "n", ARGS("--n=-42"), "-42", "");

    // A dashed token is still a flag to a non-numeric argument, so "-1" here
    // is left over rather than becoming the name. (Giving it to --name
    // explicitly is an error; see test_errors.)
    CHECK_NO_POP(&Text$info, (Text_t)Text("unset"), 0, "name", ARGS("-1"), "\"unset\"", "-1");

    // Containers defer to what they hold: a list of numbers keeps taking
    // negative values, while a list of text stops at the first dashed token.
    CHECK_POP(ints, EMPTY_LIST, 0, "nums", ARGS("--nums", "-1", "2", "-3", "--other"), "[-1, 2, -3]", "--other");
    CHECK_POP(ints, EMPTY_LIST, 0, "nums", ARGS("--nums=-1,-2"), "[-1, -2]", "");
    CHECK_POP(OPT(List_t, ints), NONE_LIST, 0, "nums", ARGS("--nums=-1,2"), "[-1, 2]", "");
    CHECK_POP(texts, EMPTY_LIST, 0, "files", ARGS("--files", "a", "-1", "b"), "[\"a\"]", "-1 b");
    CHECK_POP(Table$info(&Int32$info, &Text$info), EMPTY_TABLE, 0, "defs", ARGS("--defs", "-1:a", "2:b"),
              "{-1: \"a\", 2: \"b\"}", "");

    // A struct of numbers takes them in every field, and so does one behind an
    // optional, where the check sees the wrapper rather than the fields:
    CHECK_POP(&Point$info, ((Point_t){0, 0}), 0, "at", ARGS("--at", "-3", "-4"), "Point{x=-3, y=-4}", "");
    CHECK_POP(OPT(OptionalPoint_t, &Point$info), ((OptionalPoint_t){0}), 0, "at", ARGS("--at", "-3", "-4"),
              "Point{x=-3, y=-4}", "");
    // A struct whose leading field isn't numeric can still take a negative in
    // the fields that are (its leading token is rejected; see test_errors):
    CHECK_POP(&Label$info, ((Label_t){0}), 0, "label", ARGS("--label", "x", "-2"), "Label{name=\"x\", n=-2}", "");

    // A pointer looks through to what it points at:
    {
        Int32_t *ptr = NULL;
        List_t args = ARGS("--n", "-42");
        CHECK(pop_cli_flag(&args, 0, "n", &ptr, Pointer$info("@", &Int32$info)));
        CHECK(ptr && *ptr == -42);
    }
    {
        List_t *nums = NULL;
        List_t args = ARGS("--nums", "-1", "2");
        CHECK(pop_cli_flag(&args, 0, "nums", &nums, Pointer$info("@", ints)));
        CHECK(nums != NULL);
        CHECK_TEXT(nums, ints, "[-1, 2]");
    }

    // Infinity is a value a float can be given, even though it isn't written
    // with digits:
    CHECK_POP(&Float64$info, 0.0, 0, "x", ARGS("--x", "-inf"), "-inf", "");
    CHECK_POP(&Float32$info, 0.0f, 0, "x", ARGS("--x=-INFINITY"), "-inf", "");

    // A negative number is never a cluster of short flags, so a flag whose
    // letter happens to appear inside one doesn't claim it:
    CHECK_NO_POP(&Int32$info, (int32_t)7, 'e', "expr", ARGS("--x", "-1e5"), "7", "--x -1e5");
    CHECK_NO_POP(&Int32$info, (int32_t)7, 'x', "hex", ARGS("--n", "-0x10"), "7", "--n -0x10");
    CHECK_NO_POP(&Bool$info, false, 'e', "expr", ARGS("--x", "-1e5"), "no", "--x -1e5");
    // ...including one written with a leading dot:
    CHECK_NO_POP(&Int32$info, (int32_t)7, 'e', "expr", ARGS("--x", "-.5e3"), "7", "--x -.5e3");
    // ...but a token that only looks like a word still is one (`-inf` could be
    // `-i -n -f`), and a short flag followed by digits is unaffected:
    CHECK_POP(&Bool$info, false, 'n', "no", ARGS("-inf"), "yes", "-if");
    CHECK_POP(&Int32$info, (int32_t)0, 'O', "opt", ARGS("-O3"), "3", "");

    // Recognizing a number is more permissive than accepting one: a token that
    // is a number but not one this argument can hold still reaches the parse
    // error naming the argument and the value, rather than the flag error.
    {
        uint8_t byte = 0;
        cli_arg_t spec[] = {{.name = "n", .dest = &byte, .type = &Byte$info}};
        parse_case_t c = {
            .args = ARGS("--n", "-1"), .info = {.usage = Text("Usage: prog")}, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Could not parse argument for n: -1");
    }
    {
        // A malformed number is still a number, not an unknown flag, so it
        // reports the same way its positive form would:
        Int_t n = I(0);
        cli_arg_t spec[] = {{.name = "n", .dest = &n, .type = &Int$info}};
        parse_case_t c = {
            .args = ARGS("--n", "-1abc"), .info = {.usage = Text("Usage: prog")}, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Could not parse argument for n: -1abc");
    }
    {
        // ...while `-inf` is no kind of number for an integer argument:
        Int_t n = I(0);
        cli_arg_t spec[] = {{.name = "n", .dest = &n, .type = &Int$info}};
        parse_case_t c = {
            .args = ARGS("--n", "-inf"), .info = {.usage = Text("Usage: prog")}, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Not a valid flag: -inf");
    }
    {
        // Num has no infinity, though Float64 takes it:
        Num_t limit = NONE_NUM;
        cli_arg_t spec[] = {{.name = "limit", .dest = &limit, .type = &Num$info}};
        parse_case_t c = {
            .args = ARGS("--limit", "-inf"), .info = {.usage = Text("Usage: prog")}, .spec_len = 1, .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Could not parse argument for limit: -inf");
    }
}

// ---------------------------------------------------------------------------
// tomo_parse_arg_list: positional filling and "--"
// ---------------------------------------------------------------------------

// Parse `args` against a two-Text spec and summarize what each entry ended up
// with, as "<first>/<second>", writing "-" for one that nothing filled. The
// positional-filling rules are all about which entry gets what, so one line
// of this says the whole thing.
static const char *fill_two(List_t args, cli_help_info_t info, bool second_is_positional) {
    Text_t first = EMPTY_TEXT, second = EMPTY_TEXT;
    cli_arg_t spec[] = {
        {.name = "first", .dest = &first, .type = &Text$info},
        {.name = "second", .dest = &second, .type = &Text$info, .positional = second_is_positional},
    };
    tomo_parse_arg_list(args, info, 2, spec);
    return String(spec[0].populated ? Text$as_c_string(first) : "-", "/",
                  spec[1].populated ? Text$as_c_string(second) : "-");
}

// The same for a one-argument spec, checked through the value's own rendering.
#define CHECK_FILL(type_info, initial, args, info, want)                                                               \
    do {                                                                                                               \
        __typeof(initial) _v = (initial);                                                                              \
        cli_arg_t _spec[] = {{.name = "arg", .dest = &_v, .type = (type_info)}};                                       \
        tomo_parse_arg_list((args), (info), 1, _spec);                                                                 \
        CHECK_TEXT(&_v, (type_info), (want));                                                                          \
    } while (0)

static void test_positionals(void) {
    const cli_help_info_t plain = {0}, strict = {.strict_positionals = true};

    // Unpopulated entries are filled positionally, in spec order...
    CHECK_STR(fill_two(ARGS("a", "b"), plain, false), "a/b");
    // ...but an entry a flag already filled is skipped, so the lone positional
    // goes to `first`:
    CHECK_STR(fill_two(ARGS("--second", "b", "a"), plain, false), "a/b");
    // Everything after "--" is a value, even if it starts with a dash:
    CHECK_STR(fill_two(ARGS("a", "--", "-b"), plain, false), "a/-b");

    // With strict_positionals, only entries marked .positional are filled from
    // bare words, before and after "--" alike. (This is the mode
    // hand-written CLIs like the compiler's use.)
    CHECK_STR(fill_two(ARGS("a"), strict, true), "-/a");
    CHECK_STR(fill_two(ARGS("--", "-a"), strict, true), "-/-a"); // the dash is part of the value here

    // A list positional after "--" takes the dashed arguments too:
    CHECK_FILL(List$info(&Text$info), EMPTY_LIST, ARGS("--", "-a", "--b"), plain, "[\"-a\", \"--b\"]");

    // A negative number fills a numeric positional, both directly and after
    // the "--" that allows dashes generally:
    CHECK_FILL(&Int$info, I(0), ARGS("-1"), plain, "-1");
    CHECK_FILL(&Int$info, I(0), ARGS("--", "-1"), plain, "-1");

    // An unpopulated, non-required entry is left at its default:
    CHECK_FILL(&Text$info, (Text_t)Text("default"), ARGS(), plain, "\"default\"");
}

static void test_help_flag(void) {
    Text_t name = EMPTY_TEXT;
    cli_arg_t spec[] = {{.name = "name", .dest = &name, .type = &Text$info}};
    cli_help_info_t info = {.usage = Text("Usage: prog"), .help = Text("HELP TEXT HERE"), .help_short = 'h'};

    parse_case_t with_long = {.args = ARGS("--help"), .info = info, .spec_len = 1, .spec = spec};
    EXPECT_PARSE_OUTPUT(&with_long, "HELP TEXT HERE");

    parse_case_t with_short = {.args = ARGS("-h"), .info = info, .spec_len = 1, .spec = spec};
    EXPECT_PARSE_OUTPUT(&with_short, "HELP TEXT HERE");

    // --no-help is an explicit "no", so it's consumed but no help is shown and
    // parsing carries on normally:
    tomo_parse_arg_list(ARGS("--no-help", "bob"), info, 1, spec);
    CHECK_STR(Text$as_c_string(name), "bob");
}

// ---------------------------------------------------------------------------
// Failure modes
// ---------------------------------------------------------------------------

// Somewhere for a failing parse to write into: these cases only check the
// message, and the process exits before the value is ever read.
static union {
    Int_t i;
    Text_t text;
    int32_t i32;
    uint8_t byte;
    List_t list;
    Table_t table;
    bool b;
} error_dest;

#define BAD_ARG(flag, type_info, ...) {.name = (flag), .dest = &error_dest, .type = (type_info), __VA_ARGS__}

// Parse `args` against a spec and expect it to fail with a message containing
// `want`. The usage line is always shown, so it is checked once, below.
#define CHECK_PARSE_ERROR(args_, want, ...)                                                                            \
    do {                                                                                                               \
        cli_arg_t _spec[] = {__VA_ARGS__};                                                                             \
        parse_case_t _c = {.args = (args_),                                                                            \
                           .info = {.usage = Text("Usage: prog [--name text]")},                                       \
                           .spec_len = sizeof(_spec) / sizeof(_spec[0]),                                               \
                           .spec = _spec};                                                                             \
        EXPECT_PARSE_ERROR(&_c, (want));                                                                               \
    } while (0)

static void test_errors(void) {
    // A required argument that nothing fills, named as a flag or as an
    // argument depending on how it is declared:
    CHECK_PARSE_ERROR(ARGS(), "Missing required flag: name", BAD_ARG("name", &Text$info, .required = true));
    CHECK_PARSE_ERROR(ARGS(), "Missing required argument: file",
                      BAD_ARG("file", &Text$info, .required = true, .positional = true));

    // Arguments nothing in the spec can absorb:
    CHECK_PARSE_ERROR(ARGS("a", "b"), "Unknown flag values: b", BAD_ARG("name", &Text$info));

    // A flag at the very end, with nothing left to consume:
    CHECK_PARSE_ERROR(ARGS("--count"), "No value provided for flag: count",
                      BAD_ARG("count", &Int$info, .short_flag = 'n'));
    CHECK_PARSE_ERROR(ARGS("-n"), "No value provided for flag: -n", BAD_ARG("count", &Int$info, .short_flag = 'n'));

    // A value the type can't parse, and a flag where a value was expected:
    CHECK_PARSE_ERROR(ARGS("--count", "banana"), "Could not parse argument for count: banana",
                      BAD_ARG("count", &Int$info));
    CHECK_PARSE_ERROR(ARGS("--count", "--name"), "Not a valid flag: --name", BAD_ARG("count", &Int$info),
                      BAD_ARG("name", &Text$info));

    // A dashed value is a flag to a non-numeric argument even when it represents a
    // number, and a dashed non-number is a flag even to a numeric one:
    CHECK_PARSE_ERROR(ARGS("--name", "-1"), "Not a valid flag: -1", BAD_ARG("name", &Text$info));
    CHECK_PARSE_ERROR(ARGS("--count", "-x"), "Not a valid flag: -x", BAD_ARG("count", &Int$info));
    CHECK_PARSE_ERROR(ARGS("--count", "-"), "Not a valid flag: -", BAD_ARG("count", &Int$info));

    // A wrapped container stops at a dashed token exactly where a bare one
    // does, since the wrapper defers to it rather than erroring first, so
    // the token is left over rather than rejected:
    CHECK_PARSE_ERROR(ARGS("--files", "-1"), "Unknown flag values: -1",
                      BAD_ARG("files", Pointer$info("@", List$info(&Text$info))));
    // ...while a wrapper around a struct still rejects it, since a struct
    // consumes a fixed number of values rather than stopping on its own:
    CHECK_PARSE_ERROR(ARGS("--label", "-1", "2"), "Not a valid flag: -1",
                      BAD_ARG("label", Pointer$info("@", &Label$info)));

    // An enum tag that doesn't exist, with the valid names listed:
    CHECK_PARSE_ERROR(ARGS("--color", "Purple"), "Invalid enum name for Color: Purple", BAD_ARG("color", &Color$info));
    CHECK_PARSE_ERROR(ARGS("--color", "Purple"), "Valid names are: ", BAD_ARG("color", &Color$info));

    // A non-boolean value given to a boolean flag:
    CHECK_PARSE_ERROR(ARGS("--force=banana"), "Invalid boolean value for flag force",
                      BAD_ARG("force", &Bool$info, .short_flag = 'f'));
    CHECK_PARSE_ERROR(ARGS("-f=banana"), "Invalid boolean value for flag -f",
                      BAD_ARG("force", &Bool$info, .short_flag = 'f'));

    CHECK_PARSE_ERROR(ARGS(), "Usage: prog [--name text]", BAD_ARG("name", &Text$info, .required = true));

    // ...and the command hint is appended too, so a mistyped subcommand is
    // reported as such rather than as a stray argument:
    {
        Text_t name = EMPTY_TEXT;
        cli_arg_t spec[] = {{.name = "name", .dest = &name, .type = &Text$info}};
        parse_case_t c = {.args = ARGS("bulid", "x"),
                          .info = {.usage = Text("Usage: prog"),
                                   .command_hint = Text("\nbulid isn't a command. Did you mean build?")},
                          .spec_len = 1,
                          .spec = spec};
        EXPECT_PARSE_ERROR(&c, "Did you mean build?");
    }
}

// ---------------------------------------------------------------------------
// Generated usage and help text
// ---------------------------------------------------------------------------

// Somewhere for a spec to point at. Usage generation never reads `dest`, but
// the field is there, so give it something real rather than NULL.
static union {
    Int_t i;
    Text_t text;
    List_t list;
    Table_t table;
    int32_t i32;
    OptionalBool_t b;
} usage_dest;

// The usage line generated from a spec:
#define CHECK_USAGE(want, ...)                                                                                         \
    do {                                                                                                               \
        cli_arg_t _spec[] = {__VA_ARGS__};                                                                             \
        CHECK_STR(Text$as_c_string(tomo_generate_usage(Text("prog"), sizeof(_spec) / sizeof(_spec[0]), _spec)),        \
                  (want));                                                                                             \
    } while (0)

#define ARG(flag, type_info, ...) {.name = (flag), .dest = &usage_dest, .type = (type_info), __VA_ARGS__}

static void test_usage_text(void) {
    // Flags come first, then positionals; optional ones are bracketed, and
    // each flag shows a value placeholder derived from its type:
    CHECK_USAGE("Usage: prog --name|-n text [--force|-f] [--count N] [files...]",
                ARG("name", &Text$info, .short_flag = 'n', .required = true),
                ARG("force", &Bool$info, .short_flag = 'f'), ARG("count", &Int$info),
                ARG("files", List$info(&Path$info), .positional = true));

    // A metavar overrides the type-derived placeholder:
    CHECK_USAGE("Usage: prog --out FILE", ARG("out", &Text$info, .metavar = "FILE", .required = true));

    // An optional shows the placeholder of the type it wraps, and an
    // optional boolean is still a boolean flag, with no placeholder at all:
    CHECK_USAGE("Usage: prog --count N", ARG("count", OPT(Int_t, &Int$info), .required = true));
    CHECK_USAGE("Usage: prog --force", ARG("force", OPT(OptionalBool_t, &Bool$info), .required = true));

    // A list *flag* shows that it takes many values (a list positional is
    // displayed differently; see the first spec above):
    CHECK_USAGE("Usage: prog --files path1 path2...", ARG("files", List$info(&Path$info), .required = true));
    // An optional list takes many values too, as a flag and as a positional:
    CHECK_USAGE("Usage: prog --files path1 path2...",
                ARG("files", OPT(List_t, List$info(&Path$info)), .required = true));
    CHECK_USAGE("Usage: prog [files...]", ARG("files", OPT(List_t, List$info(&Path$info)), .positional = true));

    // Tables, sets, and enums get their own placeholders:
    CHECK_USAGE("Usage: prog --defs text1:N1 text2:N2... --tags text1 text2... --color Red|Green|Blue",
                ARG("defs", Table$info(&Text$info, &Int$info), .required = true),
                ARG("tags", Set$info(&Text$info), .required = true), ARG("color", &Color$info, .required = true));
}

// ---------------------------------------------------------------------------
// tomo_dispatch_command: the git-style command tree
// ---------------------------------------------------------------------------

// What the most recent dispatch ran, so the tests can assert on it:
static const char *ran_command = NULL;
static const char *ran_extra_args = NULL;

static int record_handler(cli_command_t *self, List_t extra_args) {
    ran_command = self->name ? self->name : "(root)";
    ran_extra_args = CString$join(" ", extra_args);
    return 0;
}

// A `prog [--verbose] [add <file>|submodule init <path>|<file>]` CLI, rebuilt
// for each test so that materialized help text and populated flags don't leak
// between cases.
typedef struct {
    cli_spec_t cli;
    Text_t add_file;
    Path_t init_path;
    Text_t root_file;
    bool verbose;
    cli_arg_t add_spec[1];
    cli_arg_t init_spec[1];
    cli_arg_t root_spec[1];
    cli_arg_t global_spec[1];
    cli_command_t add, init, submodule;
    cli_command_t *root_children[2];
    cli_command_t *submodule_children[1];
} test_cli_t;

static test_cli_t *make_test_cli(void) {
    test_cli_t *t = GC_MALLOC(sizeof(test_cli_t));
    memset(t, 0, sizeof(*t));
    t->add_spec[0] =
        (cli_arg_t){.name = "file", .dest = &t->add_file, .type = &Text$info, .required = true, .positional = true};
    t->init_spec[0] = (cli_arg_t){.name = "path", .dest = &t->init_path, .type = &Path$info, .positional = true};
    t->root_spec[0] = (cli_arg_t){.name = "file", .dest = &t->root_file, .type = &Text$info, .positional = true};
    t->global_spec[0] = (cli_arg_t){
        .name = "verbose", .dest = &t->verbose, .type = &Bool$info, .short_flag = 'V', .description = "Say more"};
    t->add = (cli_command_t){
        .name = "add", .summary = "Add a file", .spec_len = 1, .spec = t->add_spec, .handler = record_handler};
    t->init = (cli_command_t){
        .name = "init", .summary = "Initialize", .spec_len = 1, .spec = t->init_spec, .handler = record_handler};
    t->submodule_children[0] = &t->init;
    t->submodule = (cli_command_t){.name = "submodule", .num_children = 1, .children = t->submodule_children};
    t->root_children[0] = &t->add;
    t->root_children[1] = &t->submodule;
    t->cli = (cli_spec_t){
        .name = "prog",
        .summary = "A test program",
        .version = "prog 1.2.3",
        .version_short = 'v',
        .global_len = 1,
        .global_spec = t->global_spec,
        .root = {.spec_len = 1,
                 .spec = t->root_spec,
                 .num_children = 2,
                 .children = t->root_children,
                 .handler = record_handler},
    };
    return t;
}

typedef struct {
    cli_spec_t *cli;
    const char **argv;
} dispatch_case_t;

static _Noreturn void do_dispatch(void *p) {
    dispatch_case_t *c = p;
    int argc = 0;
    while (c->argv[argc])
        argc++;
    int status = tomo_dispatch_command(argc, (char **)c->argv, c->cli);
    fflush(NULL);
    _exit(status);
}

// Dispatch in a child process (so --help/--version output and parse failures
// can be inspected), asserting on the exit status and what it printed:
static void check_dispatch_output(cli_spec_t *cli, const char **argv, int want_status, const char *want, int line) {
    checks++;
    char output[16384];
    dispatch_case_t c = {.cli = cli, .argv = argv};
    int status = run_in_child(do_dispatch, &c, output, sizeof(output));
    if (status != want_status) FAIL("(line %d) got status %d, want %d. Output: %s", line, status, want_status, output);
    else if (strstr(output, want) == NULL)
        FAIL("(line %d) expected output mentioning `%s`, got: %s", line, want, output);
}

#define CHECK_DISPATCH_OF(cli, want_status, want, ...)                                                                 \
    check_dispatch_output((cli), (const char *[]){"prog", __VA_ARGS__ __VA_OPT__(, ) NULL}, want_status, want, __LINE__)

// The usual case: a freshly built test CLI, so materialized help text and
// populated flags don't leak between cases.
#define CHECK_DISPATCH(want_status, want, ...)                                                                         \
    CHECK_DISPATCH_OF(&make_test_cli()->cli, want_status, want __VA_OPT__(, ) __VA_ARGS__)

// Dispatch in-process, for the cases that succeed and have side effects to
// check. argv is counted here rather than at each call site, where a
// hand-written argc is one edit away from being wrong.
static int dispatch_argv(cli_spec_t *cli, const char **argv) {
    int argc = 0;
    while (argv[argc])
        argc++;
    ran_command = NULL;
    ran_extra_args = NULL;
    return tomo_dispatch_command(argc, (char **)argv, cli);
}

#define DISPATCH_OF(cli, ...) dispatch_argv((cli), (const char *[]){"prog", __VA_ARGS__ __VA_OPT__(, ) NULL})

// The usual case again, returned so the test can check what the command's
// arguments were filled with.
static test_cli_t *dispatch(const char **argv) {
    test_cli_t *t = make_test_cli();
    dispatch_argv(&t->cli, argv);
    return t;
}

#define DISPATCH(...) dispatch((const char *[]){"prog", __VA_ARGS__ __VA_OPT__(, ) NULL})

static void test_dispatch(void) {
    { // A named command runs with its own arguments:
        test_cli_t *t = DISPATCH("add", "hello.txt");
        CHECK_STR(ran_command, "add");
        CHECK_STR(Text$as_c_string(t->add_file), "hello.txt");
    }

    { // Commands nest arbitrarily deep:
        test_cli_t *t = DISPATCH("submodule", "init", "libs/x");
        CHECK_STR(ran_command, "init");
        CHECK_STR(Path$as_c_string(t->init_path), "libs/x");
    }

    { // A first word that isn't a command falls through to the root handler:
        test_cli_t *t = DISPATCH("hello.txt");
        CHECK_STR(ran_command, "(root)");
        CHECK_STR(Text$as_c_string(t->root_file), "hello.txt");
    }

    { // Global flags are valid before the command name...
        test_cli_t *t = DISPATCH("--verbose", "add", "x");
        CHECK(t->verbose);
        CHECK_STR(ran_command, "add");
    }
    { // ...and after it, and by their short alias:
        test_cli_t *t = DISPATCH("add", "x", "-V");
        CHECK(t->verbose);
        CHECK_STR(ran_command, "add");
    }

    { // A namespace with no handler of its own prints help and fails:
        CHECK_DISPATCH(1, "Commands:", "submodule");
    }

    { // An unrecognized word under a namespace is reported, with a suggestion:
        CHECK_DISPATCH(1, "Unrecognized command: inti", "submodule", "inti");
        CHECK_DISPATCH(1, "Did you mean init?", "submodule", "inti");
    }

    { // A word that resembles a top-level command is only blamed once the
        // parse actually fails. The root command here can absorb one file
        // argument, so the second word is what triggers the error:
        CHECK_DISPATCH(1, "isn't a command. Did you mean add?", "adk", "x");
    }

    { // ...but a lone word that parses fine just runs, even though it's one
        // edit away from a command name:
        test_cli_t *t = DISPATCH("adk");
        CHECK_STR(ran_command, "(root)");
        CHECK_STR(Text$as_c_string(t->root_file), "adk");
    }

    { // --help prints the help for whichever command was named:
        CHECK_DISPATCH(0, "prog add: Add a file", "add", "--help");
        CHECK_DISPATCH(0, "prog submodule init: Initialize", "submodule", "init", "--help");
        CHECK_DISPATCH(0, "A test program", "--help");
        // -h is the short alias, since nothing else claims it:
        CHECK_DISPATCH(0, "prog add: Add a file", "add", "-h");
    }

    { // Generated help lists the global flags (with their descriptions) and
        // the subcommands:
        CHECK_DISPATCH(0, "Global flags", "--help");
        CHECK_DISPATCH(0, "Say more", "--help");
        CHECK_DISPATCH(0, "add", "--help");
    }

    { // --version prints the version...
        CHECK_DISPATCH(0, "prog 1.2.3", "--version");
        // ...but -v is claimed by nothing here, so it works as the alias:
        CHECK_DISPATCH(0, "prog 1.2.3", "-v");
    }
}

// A CLI that defines its own `help` and `version` arguments, plus a `-v` of its
// own: the automatic flags must step aside rather than answering first.
static void test_dispatch_flag_shadowing(void) {
    static Text_t help_value, version_value;
    static bool verbose;
    static cli_arg_t spec[] = {
        {.name = "help", .dest = &help_value, .type = &Text$info},
        {.name = "version", .dest = &version_value, .type = &Text$info},
        {.name = "verbose", .dest = &verbose, .type = &Bool$info, .short_flag = 'v'},
    };
    static cli_command_t root;
    static cli_spec_t cli;
    root = (cli_command_t){.spec_len = 3, .spec = spec, .handler = record_handler};
    cli = (cli_spec_t){.name = "prog", .version = "prog 9.9.9", .version_short = 'v', .root = root};

    help_value = EMPTY_TEXT;
    version_value = EMPTY_TEXT;
    CHECK(DISPATCH_OF(&cli, "--help", "topic", "--version", "3") == 0);
    CHECK_STR(ran_command, "(root)");
    CHECK_STR(Text$as_c_string(help_value), "topic");
    CHECK_STR(Text$as_c_string(version_value), "3");

    // -v belongs to --verbose, not to the automatic --version:
    verbose = false;
    help_value = EMPTY_TEXT;
    version_value = EMPTY_TEXT;
    CHECK(DISPATCH_OF(&cli, "-v", "--help", "x", "--version", "1") == 0);
    CHECK(verbose);
}

// A program that claims -v (or -h) for something of its own keeps it: the
// automatic flags are popped before the command's spec is parsed, so without
// this they would shadow it. The long --version/--help still work, since only
// the short alias is contested.
static void test_dispatch_short_flag_shadowing(void) {
    static bool verbose, human;
    static cli_arg_t spec[] = {
        {.name = "verbose", .dest = &verbose, .type = &Bool$info, .short_flag = 'v'},
        {.name = "human", .dest = &human, .type = &Bool$info, .short_flag = 'h'},
    };
    static cli_spec_t cli;
    cli = (cli_spec_t){.name = "prog",
                       .version = "prog 9.9.9",
                       .version_short = 'v',
                       .root = {.spec_len = 2, .spec = spec, .handler = record_handler}};

    verbose = human = false;
    CHECK(DISPATCH_OF(&cli, "-v", "-h") == 0);
    CHECK_STR(ran_command, "(root)"); // i.e. it ran, rather than printing help/version
    CHECK(verbose && human);

    // The same applies when the short flag is claimed by a *global* flag:
    static bool global_verbose;
    static cli_arg_t global_spec[] = {{.name = "loud", .dest = &global_verbose, .type = &Bool$info, .short_flag = 'v'}};
    static cli_spec_t global_cli;
    static cli_arg_t empty_spec[1];
    global_cli = (cli_spec_t){.name = "prog",
                              .version = "prog 9.9.9",
                              .version_short = 'v',
                              .global_len = 1,
                              .global_spec = global_spec,
                              .root = {.spec_len = 0, .spec = empty_spec, .handler = record_handler}};
    global_verbose = false;
    CHECK(DISPATCH_OF(&global_cli, "-v") == 0);
    CHECK_STR(ran_command, "(root)");
    CHECK(global_verbose);

    // ...while the long versions, which nothing here contests, still work:
    CHECK_DISPATCH_OF(&cli, 0, "prog 9.9.9", "--version");
    CHECK_DISPATCH_OF(&cli, 0, "Usage: prog", "--help");
}

// Everything after the first bare "--" is handed to the handler untouched when
// the program asks for pass-through:
static void test_dispatch_passthrough(void) {
    test_cli_t *t = make_test_cli();
    t->cli.passthrough_after_double_dash = true;
    DISPATCH_OF(&t->cli, "add", "x", "--", "--not-my-flag", "-a");
    CHECK_STR(ran_command, "add");
    CHECK_STR(Text$as_c_string(t->add_file), "x");
    CHECK_STR(ran_extra_args, "--not-my-flag -a");
}

// after_globals runs once globals are popped, before any command is dispatched:
static int after_globals_calls = 0;
static void count_after_globals(void) {
    after_globals_calls++;
}

static void test_after_globals(void) {
    test_cli_t *t = make_test_cli();
    t->cli.after_globals = count_after_globals;
    after_globals_calls = 0;
    DISPATCH_OF(&t->cli, "add", "x");
    CHECK(after_globals_calls == 1);
}

// ---------------------------------------------------------------------------

int main(void) {
    // Keep the expected strings free of ANSI escapes:
    setenv("NO_COLOR", "1", 1);
    tomo_init();

    test_long_flags();
    test_short_flags();
    test_boolean_flags();
    test_scalar_types();
    test_optional_types();
    test_compound_types();
    test_optional_containers();
    test_optional_booleans();
    test_negative_numbers();
    test_positionals();
    test_help_flag();
    test_errors();
    test_usage_text();
    test_dispatch();
    test_dispatch_flag_shadowing();
    test_dispatch_short_flag_shadowing();
    test_dispatch_passthrough();
    test_after_globals();

    if (failures > 0) {
        fprintf(stderr, "%d of %d CLI checks FAILED\n", failures, checks);
        return 1;
    }
    printf("All %d CLI argument parsing checks passed.\n", checks);
    return 0;
}