Designing a Type-Reflecting Command-Line Flag Library in Jai
- https://www.youtube.com/watch?v=DMuahT5Ae08
- Original title: My approach to Parsing Command Line Arguments
A ~3-hour recreational live-coding session in which Tsoding builds his preferred style of command-line argument parser from scratch in Jai. He first tours Jai's built-in command_line module (John Blow's design, where an annotated struct plus a compile-time-generated is_set companion drives parsing), decides he prefers the Go flag / his own flag.h ergonomics instead, and then reimplements that approach in Jai using runtime type reflection (any, Type_Info) and compile-time #modify to constrain the accepted types. The result is a small library where you register plain variables as flags, and the parser infers each flag's type from the variable itself.
Two philosophies of argument parsing
- Jai's built-in
command_linemodule: you declare anArgsstruct with typed fields;parse_argumentsfills it in and also produces anis_setstruct so you can tell which options the user explicitly set versus which still hold defaults. Tsoding is skeptical of theis_setuse case (if you cared, why is it a default?), but concedes it matters for mandatory/required flags. Its big advantage: duplicate flag names are impossible, so that class of error is caught at compile time. - His preferred style (from Go's
flagand his own C libraryflag.h, https://github.com/tsoding/flag.h): you register an existing variable as a flag by pointer. The key ergonomic win is that a plain compile-time =bool run = false= in a build script can be promoted to a runtime flag with almost no change to the surrounding code — the variable stays a variable. This "scales with how I develop things" and is why he wants it in Jai for rewriting his Boomer tool from Nim to Jai.
The core design in Jai
- A
flagfunction takes a pointer to a generic$T, a name, and a description. The variable's current value is treated as the default — no separate default argument. The type is inferred from the pointer, so there is a singleflag()(noflag_bool/flag_intas in C). - Storage: a
Flag_Slotholds anany(aType_Infopointer plus avoid*value pointer), the name, the description, and aSource_Code_Locationcaptured via#caller_location. This lets errors point at the exact file:line where a flag was defined — clickable in Emacs. Slots live in aFlag_Contextthat supports adefault_flag_contextvia a default argument, withusingto flatten field access. - Duplicate detection: slots are kept in a hash table (string →
Flag_Slot); redefining a flag logs an error citing the previous definition's location and aborts (settled ondebug_break/ panic-style crash). He notes this is a runtime check where Jai's built-in module gets it at compile time — the main tradeoff of his approach. Later he argues a hash table is overkill (you'd need millions of flags to justify it) and that a plain array would be fine and would also preserve definition order. - Type restriction:
#modifyruns at compile time on each call and returns false with a custom message ("flag type is not supported") for anything outside int, float, bool, string (and later dynamic arrays), so unsupported types are rejected at compile time and the runtime "unreachable" branch stays unreachable.
Parsing loop and features
flag_parseconsumesget_command_line_argumentsusing a bash/Perl-inspiredshifthelper (borrowed from his SV/su2 code) that pops the first element off a slice. The program name is stored in the context; the first non-dash argument stops flag parsing and the remainder is stored asrest_args.- Per type: bools default to true when present; ints and floats use
string_to_int/string_to_floatwith proper "not a valid integer/float" errors; strings are taken verbatim. - Signature "comment-out" feature (
/): prefixing a flag with a slash makes the parser fully validate the flag (syntax, type) but skip setting it, using the variable's existing value instead — like temporarily commenting the flag out without losing its value. Tsoding stresses he invented this syntax himself, has wanted it for years, thinks it belongs at the shell level, and repeatedly urges other CLI-library authors to steal it ("I want every software to have a variant of that"). Credit optional. - =name=value= syntax added alongside
name valueby splitting on the first =. Motivated by needing to explicitly set a bool that defaults to true (e.g. =-run=false=); a smallparse_booleanaccepts true/false. Booleans are the awkward special case since every other branch expects a value. - Ordered help output: because the hash table has undefined order, an
orderfield per slot letsflag_print_optionssort (viaintro_sort) so flags print in definition order — letting him placehelplast so errors sit above it. Help is rendered into aString_Builder, prints type hints per flag, and prints default values (which requires deep-cloning theanyso the printed default survives later mutation).
Lists (dynamic arrays) as an emergent feature
- Refactoring the primitive-value parser into
parse_primitive_valuelets him support dynamic-array flags by recursion: detectType_Info_Array(resizable), get the elementType_Info, thenmaybe_growthe abstract resizable array manually (pointer + elementruntime_sizearithmetic) and parse one primitive into the new slot. Repeating a flag appends:access 69 access 420accumulates into[]int. - This yields fun emergent behavior: repeated bool flags produce alternating
[true,false,...], and the same comma-splitting idea could nest arbitrarily. He leaves default values for list flags as an unfinished TODO (needs a deep clone of the array), along with folding the near-duplicate array/scalar dispatch code together.
Takeaways
- Tsoding is happy with the ergonomics (register a variable, get a flag; type inferred; comment-out with
/) and impressed at how cleanly Jai's compile-time reflection (#modify,Type_Info,#caller_location,any) lets the pieces fit — "not that different from C" in the array-poking parts, but far less macro machinery. He credits John Blow for exposingmaybe_growpublicly. - The honest downside versus Jai's built-in module: several correctness checks (duplicate names, unsupported types in lists) are pushed to runtime rather than compile time. His overall stance is that this is about options — different parsing styles suit different workflows, and people should pick the one that fits.
- The session closes with a long, off-topic but candid tangent: he's a former Java-enterprise developer, is driven by meaning rather than money, and would rather build durable skills than chase "points in a rigged economic game." He also mentions his ongoing fully-CPU-software-rendered game engine as his current favorite project.