Skip to content

Let parsers outside @optique/core declare their empty-input behavior #1014

Description

@dahlia

#1015, which resolves #1013, lets usage groups reflect how their parsers handle an empty argument list. or(), longestMatch(), and multiple() record acceptsEmpty on the exclusive and multiple terms they produce, and both usage formatters draw those groups as optional exactly when the parser accepts empty input. The combinators decide this from internal facts that each built-in parser carries about its empty parse step and completion. Those facts are not public, so only parsers defined in @optique/core can provide them.

Any other parser counts as unknown, and so does any group whose outcome depends on it. For example, bindConfig(fail(), { ... }) has no leading names, so or() would consider it when choosing a branch for an empty argument list. In or(bindConfig(fail(), { ... }), argument(string())) its facts are unknown, the group gets no acceptsEmpty record, and the formatters fall back to the notation the branches declare.

This affects the integration packages in this repository as much as third-party parsers. bindEnv(), bindConfig(), prompt(), bindKeyring(), bindDerivedDefault(), and the discover wrappers cannot reach the internal facts, so groups that contain them are always unknown.

Proposal

Add a public way for a parser to declare its empty-input behavior, and let wrappers pass on the facts of the parser they wrap. Combinators would treat declared facts the same way they treat built-in ones: a missing fact stays unknown, and a declared fact is trusted.

Open questions

  • Which facts to expose. The internal model has four: the outcome of the empty parse step (success, provisional, or failure), whether completion succeeds after that step, whether completion succeeds from the initial state, and the shape of the state the step produces. The last one exists only so that multiple() can tell whether a zero-token step leaves an item behind. It depends on runtime details such as state identity and singleton-array unwrapping, so it probably should stay internal, and repetition of a parser with declared facts would stay unknown.
  • How to keep declared facts valid. Built-in facts are bound to the parser's parse(), complete(), and initialState, so spreading a parser and replacing a method drops them. A public API needs the same protection, or a clear rule that whoever replaces a method must declare the facts again.
  • How to describe parsers whose outcome depends on context. Whether bindEnv() accepts empty input depends on the environment at run time. Declaring it unknown is safe, but a separate “depends on context” value might let formatters keep these parsers drawn as optional on purpose instead of by fallback.
  • Whether withDefault() with a function default, conditional(), merge(), concat(), and seq() should gain built-in facts at the same time, or in separate changes.

Non-goals

Activity

  1. self-assigned this
    on Oct 7, 2026
  2. added this to the Optique 1.4 milestone on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions