20260812 (v0.8.0)
ArgMojo v0.8.0 moves to the first stable Mojo release, v1.0.0. It also fixes a dozen parser bugs, repairs an FFI signature clash that could break the build of a project using ArgMojo, cuts compile time by about a fifth, and fills several gaps in the declarative API.
ArgMojo v0.8.0 targets Mojo v1.0.0.
⭐️ New in v0.8.0
-
Add
ParseResult.was_provided(name)— True only when the user really supplied the argument, be it on the command line, at a prompt, or through animplies()rule.has(name)cannot tell you that, because a default is stored in the same place as a parsed value. Group constraints use it internally, and it is often what you want in your own code too:if result.was_provided("format"): print("the user asked for " + result.get_string("format")) else: print("falling back to the default format")
-
Add
ParseResult.get_float(name)— reads a value asFloat64, the wayget_int()reads it asInt. -
Option[Float64]andPositional[Float64]now work end to end, defaults included.has_rangestays integer-only, so combining it withFloat64is rejected at compile time instead of failing later with a puzzling "expected an integer". -
Countcan be unwrapped withInt(), asFlagcan withBool(). WriteInt(args.verbose)instead ofargs.verbose.value. -
The four declarative wrappers now take a consistent set of parameters.
alias_namewas onOptiononly anddeprecatedonOptionandFlagonly; both are now available wherever they make sense.Positionalgainshidden,deprecated,prompt,prompt_text,password, and the range parameters (has_range,range_min,range_max,clamp) thatOptionalready had. Until now, needing any one of these on a positional meant dropping to the builder API for that one field.
🔧 Fixes in v0.8.0
- Fix an FFI signature clash in
_read_password_asterisk(). Theread(2)binding passed its buffer asInt(ptr), which declaresreadas(Int, Int, Int) -> Int, while the standard library declares the same symbol as(Int, Pointer, Int) -> Int. Any module linking both failed to lower to LLVM IR. The buffer is passed as a real pointer now (PR #59). - Group constraints counted a default as user input.
mutually_exclusive(),required_together(),one_required()andrequired_if()all askedhas(), which is True for an argument that merely carries a.default()— so--jsonalone could conflict with a--formatnobody had typed. All four consultwas_provided()now. - A required positional could be satisfied by a later positional's default. Filling the later slot pads the positional list up to that index, and the earlier, still-empty slot then looked provided:
src(required) followed bydst(with a default) parsed happily with no arguments at all. A default on the required argument itself still satisfies it, as in clap. app --=helloused to set the first positional. The empty name left after splitting on=matched_long_name == "", which is true for every positional and every short-only option. It is now reported asInvalid option '--=hello': missing option name.- An argument carrying both
.prompt()and.default()was never prompted, because defaults were applied first and prompting skips anything that already holds a value. Prompting runs first now, and the default is what an empty answer — or a non-interactive stdin — falls back to, which is what the "(default)" hint always promised. - Defaults now reach the accessor that belongs to the argument. Every default used to be written to the string store, so
.flag().default["true"]()was invisible toget_flag(), and.append().default["x"]()leftget_list()empty whileget_string()returned the value. Defaults go to_flags,_counts,_lists,_mapsor_valuesaccording to the kind of the argument. - Bad defaults are caught at registration instead of reaching the user.
add_argument()now raises if a default is not one of the declared.choice[…]()values, if a.flag()default is not a boolean literal, if a.count()default is not an integer, or if a.map_option()default is not inkey=valueform;default_if_no_valueis checked against the choices too. The declarative wrappers already checked the choices at compile time; the builder API checked nothing. - An option no longer swallows another option as its value:
--output --verboseused to store the literal string"--verbose". Only registered options and the--marker are refused, so--offset -5and--pattern -foostill work, and.allow_hyphen_values()opts out entirely. The guard covers short options and.number_of_values[N]()as well. - Persistent-argument conflicts were detected in only one registration order. The check lived in
add_subcommand(), so callingadd_subcommand()first andadd_argument(... .persistent())afterwards slipped past it, and the flag then appeared twice in the child's help.add_argument()runs the mirror check now, and the injection at dispatch time skips an argument the child already owns. - Non-ASCII passwords came back corrupted.
_read_password_asterisk()rebuilt the typed string byte by byte throughchr(), which treats each byte as a code point and re-encodes it, soé(C3 A9) arrived asé(C3 83 C2 A9) and never matched. The bytes are decoded as UTF-8 in one step now, and the buffer is zeroed before it is freed. - An
implies()rule could fire from a default value — fix 2 again, by another route. The trigger washas(trigger), so a--modethat merely defaults tofaststill set--parallel; since an implied argument counts as user input, typing only--quietthen reported a conflict with a--parallelnobody had asked for. Implications fire onwas_provided(trigger). parse_known_arguments()applied defaults before implications, the reverse ofparse_arguments(), so the two entry points could disagree about which arguments counted as supplied. Both run implications first now.
🔄 Mojo v1.0.0 migration (PR #59)
- Bump the Mojo dependency from
==1.0.0b2to>=1.0.0, <1.1.0inpixi.toml, so ArgMojo builds against any Mojo v1.0.x release. - Replace the removed
_constrained_field_conforms_tohelper withcomptime assert conforms_to(...)plus_field_conforms_to_error, which is how the standard library now writes reflection-driven trait defaults. - Drop the
trait_downcast[…]()calls inParsable. Acomptime if conforms_to(...)guard (or acomptime assert) is now enough for the compiler to resolve trait methods on a reflected field. - Replace
__struct_field_ref(i, x)with the publicreflect[Self].field_ref[i](x). - Rename
ImplicitlyDestructibletoDeinitable, and dropMovablefrom theDefaultable & Copyable & Movablebounds, which the compiler now reports as redundant. - Rename the move constructor argument from
deinit take:todeinit move:inArgument,Command,ParseResult, and the four argument wrappers. - Replace
UnsafePointer(to=f).init_pointee_move(v)withPointer(to=f).unsafe_write(v). - Give
CommandandParseResultan explicit, empty__deinit__(). Both hold aList[Self]field, and deducingList[Command]: Deinitablenow requiresCommand: Deinitable— exactly what is being deduced. Declaring the destructor breaks the cycle, and the fields are still destroyed automatically. - Introduce temporary variables where a
Stringis rebuilt from a slice of itself (e.g.key = String(key[byte=:eq])), because such statements now trip the exclusivity checker. - Dispatch the declarative wrappers on
reflect[T].name()compared against the reflected names of the supported types, instead of against string literals. In Mojo v1.0.0,Intbecame an alias ofScalar[DType.int]and so reflects as"SIMD[DType.int, 1]"; comparing one reflected name with another keeps working across such renames. - Update
tests/test_wrappers.mojoto call the move constructor as(move=a^).
⚡️ Performance in v0.8.0
-
Compiling a program that uses ArgMojo is about 20% faster. Measured cold, with the compiler cache wiped, over the eight examples: 50.2 s down to 39.8 s in total, or 6.08 s down to 4.81 s per example. Nothing about the behaviour changes — help text, the three completion scripts and the error messages are byte-for-byte identical before and after. Four changes get there:
examples/build.shbuilds against theargmojo.mojocit already produces (-I .) instead of recompilingsrc/argmojointo all eight binaries (-I src). This is the bulk of it: 50.2 s → 42.7 s on its own. Its timing summary now reports hundredths of a second and a total, rather than whole seconds fromdate +%s.- Argument lookup returns an index instead of a copy.
_find_by_long()and_find_by_short()deep-copied the wholeArgumenton each option token, only to read two or three fields; they are_find_index_by_long()/_find_index_by_short()now, and callers bind the result withref. The per-loop copies in_prompt_missing_arguments(),_validate(),_apply_defaults()and the three completion generators becamerefbindings too. This is the largest runtime saving in the release as well: dozens of heap allocations disappear from every parse. - Long
+chains becameString(...)calls. Building a message as"a" + x + "b" + y + ...emits a separate inlined concatenation for every+; passing the same pieces toString(...)emits one call. 154 sites acrosscommand.mojo. _looks_like_number()and_levenshtein()compare bytes rather than one-character strings.token[byte=j:j+1] >= "0"writes out a full string comparison per character; aUInt8against a byte constant is one instruction._looks_like_number()alone dropped from 17,850 lines of unoptimised IR to 4,431.
Together the source changes take the unoptimised IR of a one-argument program from 332,047 lines down to 264,929 (−20%).
-
Replace the fixed-size
Listscratch buffers with the inlineArraytype, removing four heap allocations from the terminal-handling paths: the 8-byteioctl(TIOCGWINSZ)buffer in_help_line_width(), the 96-byte termios buffers in_disable_echo()and_read_password_asterisk(), and the 1-byte read buffer of the password loop._disable_echo()returnsOptional[Array[UInt32, 24]]now, instead of signalling failure with an empty list (PR #59). -
Use a stack-allocated
Array[String, 2]for the argument-name scratch buffer inrequired_if()(PR #59).
📖 Documentation in v0.8.0
docs/declarative_api_planning.mdgains a "Known Gaps" section: what went wrong in the wrappers, what has been fixed, and why the rest is still open.- The user manual documents
has()versuswas_provided(), which accessor reads back each kind of default, the registration-time rules for default values, how values that look like options are treated, the value types the declarative wrappers accept, and theBool()/Int()unwrapping shortcuts. Argument.remainder()now says what happens when the remainder is the first positional: collection starts at the very first token, so--helpis swallowed as a remainder value and the command has no help flag. That is what a wrapper command such asenvwants, but it is worth knowing before you hit it. Declare a positional ahead of the remainder, or callhelp_on_no_arguments().- CI now runs
tests/test_dispatch.mojotoo. It was in the localtesttask but in none of the workflow jobs, so the auto-dispatch tests added in v0.6.0 had never run on a pull request. - The
_allow_hyphen_valuesfield docstring claimed it accepts "the literal token-". It has always accepted any dash-prefixed token, and it now also switches off the check described in fix 8.
What's Changed
- [mojo] Update the codebase to Mojo v1.0.0 by @forfudan in #59
- [core][doc] Update documents for release + Parser fixes + faster compiles by @forfudan in #60
Full Changelog: v0.7.0...v0.8.0