# Argument Parser Design Research ## Existing Paradigms ### 1. Ad-hoc / Scattered Parser (Game Engine Style) **Description:** `argv` is passed around the program, and individual subsystems parse what they need on-the-spot using simple string matching or helper functions. **Examples:** - Many game engines (UE, Unity command-line tools) - Simple C programs with `strcmp()` loops - Shell scripts with `case` statements **Pros:** - Extremely simple to implement - Zero overhead - no framework needed - Very flexible - anyone can add arguments anywhere - Scales well with codebase size - Perfect for plugin architectures - No initialization order dependencies - Easy to add temporary debug flags **Cons:** - No automatic help generation - No validation of argument conflicts - Typos go unnoticed (silent failures) - Hard to audit what arguments exist - No standardization across modules - Duplicate parsing code everywhere - Hard to maintain consistency **Use Cases:** - Large codebases with many contributors - Plugin/module systems - Debug/development builds with experimental flags - When flexibility > user experience --- ### 2. Declarative Schema Parser (argparse / Builder Style) **Description:** Define all arguments upfront in a schema/configuration, then parse once. The parser uses this schema to validate and generate help. This includes both declarative schemas (Python argparse) and builder patterns (Rust clap's builder API, cxxopts) - both require assembling the complete argument specification before parsing. **Examples:** - Python's `argparse` - Rust's `clap` (builder API with `.arg()` chaining) - Go's `flag` package - Node.js `commander` / `yargs` - C++ `cxxopts` - Java `JCommander` **Pros:** - Excellent help generation - Centralized documentation - Validation built-in (types, conflicts, requirements) - IDE autocomplete for defined args - Can generate man pages, shell completions - User-friendly error messages - Clear contract of what's supported **Cons:** - All arguments must be known at startup - Harder to add plugin-specific arguments - More boilerplate for simple cases - Initialization overhead - Tight coupling between parser and business logic - Can become verbose for complex scenarios **Use Cases:** - CLI tools with stable interfaces - Public-facing user applications - When documentation is critical - Standard Unix-style utilities --- ### 3. Type-Driven Parser (Compile-Time) **Description:** Define arguments through struct fields with annotations/attributes. Parser reflects on types to derive behavior. **Examples:** - Rust's `clap` (derive macro): `#[derive(Parser)]` - Rust's `structopt` (now merged into clap) - Zig's potential with comptime reflection - Haskell's `optparse-applicative` **Pros:** - Minimal boilerplate - Type safety enforced at compile time - Help generated from struct - Arguments become regular struct fields - Documentation co-located with types - Compile errors for invalid configs **Cons:** - Limited to languages with strong metaprogramming - Less dynamic - can't add args at runtime - Learning curve for annotations - Magic can be hard to debug - Inflexible for plugin architectures **Use Cases:** - Type-safe languages with good metaprogramming - When compile-time guarantees are valuable - Static CLI tools --- ### 4. Subcommand-Oriented Parser (Git-Style) **Description:** Hierarchical commands where each subcommand has its own parser. Think `git commit`, `git push`, etc. **Examples:** - Git - Docker CLI - Kubernetes `kubectl` - Cargo **Pros:** - Natural organization for complex tools - Each subcommand isolated - Easy to add new subcommands - Clear mental model for users - Help can be hierarchical **Cons:** - Overkill for simple tools - More complex routing logic - Harder to share common flags - Can fragment the interface too much **Use Cases:** - Multi-function tools (package managers, version control) - When functionality naturally groups - Large CLI applications --- ### 5. Context-Based Parser (Implicit State) **Description:** Parser maintains context/state that different parts of the program query, often with defaults and cascading priorities. **Examples:** - Configuration systems (environment vars → config files → CLI args) - Viper (Go) - Click (Python) with context objects **Pros:** - Unified configuration from multiple sources - Priorities handled automatically - Can layer defaults elegantly - Good for complex applications - Handles environment variables naturally **Cons:** - Global state can be problematic - Hard to reason about precedence - Testing becomes harder - Implicit behavior can surprise users **Use Cases:** - Applications with multiple config sources - When env vars and files matter as much as CLI args - Complex deployment scenarios --- ### 6. Parser Combinators (Functional Style) **Description:** Build complex parsers by composing smaller parser functions. Very flexible but requires functional thinking. **Examples:** - Haskell's `optparse-applicative` - Some functional-style libraries in Scala, OCaml **Pros:** - Extremely composable - Very expressive for complex scenarios - Reusable parser pieces - Elegant in functional languages - Can still generate help **Cons:** - Steep learning curve - Verbose for simple cases - Requires functional programming mindset - Can be overkill **Use Cases:** - Functional programming languages - When you need maximum composability - Complex parsing logic --- ### 7. Streaming/Event Parser **Description:** Parse arguments as a stream of events, allowing handlers to react to each argument in sequence. **Examples:** - SAX-style XML parsing applied to arguments - Some minimal C libraries **Pros:** - Memory efficient - Can short-circuit early - Good for very large argument lists - Handlers decoupled **Cons:** - Awkward programming model - Hard to validate dependencies between args - No natural help generation - Uncommon pattern **Use Cases:** - Embedded systems with memory constraints - Processing huge argument lists - Rare in practice --- ## Comparative Analysis ### Documentation Quality 1. **Best:** Type-driven, Declarative schema, Builder 2. **Good:** Subcommand-oriented, Context-based 3. **Poor:** Ad-hoc, Streaming ### Flexibility 1. **Best:** Ad-hoc, Context-based 2. **Good:** Builder, Parser combinators 3. **Poor:** Type-driven, Declarative schema ### Performance 1. **Best:** Ad-hoc, Streaming 2. **Good:** All others (negligible difference for most uses) ### Ease of Use (Simple Cases) 1. **Best:** Type-driven, Declarative 2. **Good:** Builder 3. **Poor:** Parser combinators, Ad-hoc ### Ease of Use (Complex Cases) 1. **Best:** Parser combinators, Context-based 2. **Good:** Builder, Subcommand 3. **Poor:** Ad-hoc --- ## Hybrid Approaches Several modern parsers combine paradigms: ### 1. **Layered Parser** - Core declarative schema for main arguments - Extensibility hooks for plugins to register additional args - Best of both worlds: good docs + flexibility ### 2. **Two-Pass Parser** - First pass: lightweight scan for special flags (e.g., `--help`, `--version`) - Second pass: full validation and parsing - Common in practice ### 3. **Schema + Callback** - Define schema for structure and docs - Callbacks for complex custom validation - Used by many mature libraries --- ## Recommendations for Zig Given Zig's philosophy and strengths, here are some architectural considerations: ### Leverage Comptime Zig's compile-time execution is powerful. A type-driven approach using struct tags could work well: ```zig const Args = struct { verbose: bool = false, output: ?[]const u8 = null, count: u32 = 1, pub const meta = .{ .verbose = .{ .short = 'v', .help = "Enable verbose output" }, .output = .{ .short = 'o', .help = "Output file path" }, .count = .{ .short = 'n', .help = "Number of iterations" }, }; }; ``` ### Hybrid Design: "Structured Ad-hoc" 1. Allow scattered parsing for flexibility 2. But require registration in a central registry 3. Registry generates help automatically 4. Get both flexibility AND documentation ```zig pub const ArgParser = struct { registry: Registry, argv: [][]const u8, pub fn register(comptime name: []const u8, comptime T: type, comptime opts: Options) void { // Register at comptime } pub fn parse(self: *ArgParser, comptime name: []const u8) ?T { // Parse on demand, but from registered args only } pub fn generateHelp(self: *ArgParser) []const u8 { // Use registry to generate } }; ``` ### Module-Scoped Parsers Each module gets its own parser instance but they all feed into a global registry: ```zig // In physics module const args = ArgParser.forModule("physics"); const use_simd = args.parse("use_simd", bool, .{ .default = true }); // In renderer module const args = ArgParser.forModule("renderer"); const vsync = args.parse("vsync", bool, .{ .default = true }); // Global help combines all modules ``` This approach: - Maintains scattered parsing flexibility - Generates comprehensive help - Zig-idiomatic (comptime for registration) - Scales to large codebases - No runtime overhead if help not requested --- ## Open Questions 1. How to handle argument conflicts between modules? 2. Should we support subcommands natively? 3. How to integrate with existing Zig std.process.args()? 4. Should we generate shell completions? 5. How to handle environment variables? 6. Do we need config file integration? 7. What's the story for validation (ranges, enums, etc.)? --- ## Next Steps 1. Prototype the comptime registration system 2. Design the help generation format 3. Create examples for common use cases 4. Benchmark different approaches 5. Get community feedback