YouTube Summaries

← All summaries

Designing a Type-Reflecting Command-Line Flag Library in Jai

2026-06-30 Tue ⏱ 2 hr 55 min tsodingdaily

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_line module: you declare an Args struct with typed fields; parse_arguments fills it in and also produces an is_set struct so you can tell which options the user explicitly set versus which still hold defaults. Tsoding is skeptical of the is_set use 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 flag and his own C library flag.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 flag function 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 single flag() (no flag_bool / flag_int as in C).
  • Storage: a Flag_Slot holds an any (a Type_Info pointer plus a void* value pointer), the name, the description, and a Source_Code_Location captured via #caller_location. This lets errors point at the exact file:line where a flag was defined — clickable in Emacs. Slots live in a Flag_Context that supports a default_flag_context via a default argument, with using to 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 on debug_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: #modify runs 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_parse consumes get_command_line_arguments using a bash/Perl-inspired shift helper (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 as rest_args.
  • Per type: bools default to true when present; ints and floats use string_to_int / string_to_float with 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 value by splitting on the first =. Motivated by needing to explicitly set a bool that defaults to true (e.g. =-run=false=); a small parse_boolean accepts 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 order field per slot lets flag_print_options sort (via intro_sort) so flags print in definition order — letting him place help last so errors sit above it. Help is rendered into a String_Builder, prints type hints per flag, and prints default values (which requires deep-cloning the any so the printed default survives later mutation).

Lists (dynamic arrays) as an emergent feature

  • Refactoring the primitive-value parser into parse_primitive_value lets him support dynamic-array flags by recursion: detect Type_Info_Array (resizable), get the element Type_Info, then maybe_grow the abstract resizable array manually (pointer + element runtime_size arithmetic) and parse one primitive into the new slot. Repeating a flag appends: access 69 access 420 accumulates 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 exposing maybe_grow publicly.
  • 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.