Repository navigation
Conversation
commit: |
ec340ef to
200cef1
Compare
Fix object() parser stalling on first unrecognized token, which left known options unparsed (#11) and made --help position-sensitive (#12). - object(): scopeInput skips unrecognized tokens and continues matching; -- terminates option processing; remainder preserves original order - command(): scan all args before -- for help flag instead of args[0]; pass contextual args to inner parser for help text - program(): post-parse remainder check catches --help/--version that no sub-parser handled - Spec amendments: interspersed parsing rule, remainder contract, position-insensitive help/version, phased extraction guarantee, Decision B provisional note - 44 new test steps across interspersed.test.ts and help-position.test.ts Fixes #11, partially addresses #12
The pre-scan in command() intercepted --help before descendant commands() parsers could dispatch, breaking outer inner --help routing. Fix: run the inner parser first with all args, then check if --help survived in the inner parser's remainder. Descendant parsers dispatch and handle --help at the correct level before the parent looks. Also rename cryptic locals in program.ts post-parse check (ddIdx → dashDashIndex, remArgs → remainderArgs, etc.) and add regression coverage for nested command help in both commands() and program().
200cef1 to
069f8bc
Compare
This is a circular justification. Given that it is the first statement of the PR, I'm going to truncate reading the rest. I am still not convinced that the two issues are indeed issues. I would want to see a theory of the parsing that was both hierarchical and compositional; that means that there are clearly defined rules about consumption precedence as pertains to depth of the combined parser and conflict resolution in the event that an arbitrarily nested parsers contain similar options. |
Baseline architectureThe following are facts about the current Configliere codebase. Parsers are compositionalConfigliere is a parser-combinator system. Small parsers ( Two distinct parsing concerns existThere are two separate jobs in parsing, handled by two separate parsers: Tokenization is Resolution is The boundary: The matcher system
Commands establish dispatch scope
Every parser returns remainder
Inspection derives from structure
Parsing is pure
Phased parsing exists via
|
|
The circularity was swept under the rug, but it is still there: The tell is in (2) where "correct" parsing requires "correct" scoping and "correct" claiming of matching tokens. This implies that there is a universal model of "correct scoping" and "correct matching" for any parser P so that the correctness of P can be determined. But no such models exist. But how a parser matches is up to the parser, and is externally opaque. How a parser narrows scope, and how it matches tokens are at the sole discretion of the parser. In other words, the circularity is "A correct parser must match and claim tokens in the way a correct parser would." The theory of input and remainder is not without merit, but to make it coherent, we would need some way to externally declare (and therefore validate) not only parsed values, but also matched tokens. This has me thinking how we can get around this within our current situation. Part of the problem is the gymnastics that argument() consume a single token. CLI sources only The key here is that app argument --foo barand([
argument(field(z.number()))),
object({ foo: field(z.string())),
])But this is a digression. A fundamental problem we have is no way to delimit scope inside arguments:
(1) the scope of each value is clear and independent What I really want to develop is a theory for (1) (2) and (3) that given the same parser all unambiguously generate the same tree. |
|
fixed by #20 |
Motivation
Configliere's phased parsing model depends on a phase-1 parser being able to extract its known flags from anywhere in argv, passing unrecognized tokens through as remainder. The
object()parser's tokenization loop stops when it encounters a token that no field matcher claims, breaking this contract: a bootstrap parser defining--configbut not<suite>fails to extract--configfromrun ./suite --config app.json. Consumers are forced to duplicate positional definitions between phases or manually re-parse remainder — workarounds that the architecture is designed to prevent.This causes two user-visible problems:
--helponly triggers reliably when it appears before any unrecognized token. Theprogram()architecture already treats help as a boolean flag alongside a populatedconfig: T— the tokenizer just doesn't reach--helpwhen an unrecognized token precedes it.Approach
Decision A (this PR)
object()parses known options interspersed with unrecognized tokens. Unrecognized tokens go to remainder in original order.--terminates option processing.--help/-hdetection becomes position-insensitive as a consequence of the general fix, with targeted changes tocommand()andprogram()to handle the token at the correct scope.Core change:
scopeInputinlib/object.tsWhen no matcher claims the current token, the loop now moves that token to a skipped list and continues rather than halting. Skipped tokens become remainder in original order. The loop terminates when args is empty. Existing matcher priority and positional field matching rules are preserved — only the scope over which they operate is extended.
Help routing:
command()inlib/commands.tscommand()runs the inner parser first with all args, then checks if--help/-hsurvived in the inner parser's result remainder. If a descendantcommands()parser dispatched a subcommand and itscommand()handled--help, the token is consumed before the parent ever looks — preserving nested help routing (outer inner --help→ help forinner).Program-level fallback:
program()inlib/program.tsprogram()retains its existingargs[0]early check for--help/--version. After delegating to the config parser, it also scans the config parser's remainder for help/version tokens that no sub-parser handled. Handled tokens are stripped fromresult.remainder.args.Decision B (provisional)
Whether help intent should suppress required-field validation is documented and tested in this PR but not resolved. The current behavior (validation proceeds normally; consumer checks
help: true) is preserved.command()returnsok: truewithhelp: trueregardless of whether the inner parse succeeded, sohelp: trueis reachable even when required fields are missing. A provisional spec note marks this as subject to revision.Spec amendments
object()tokenization rule (§8.2)--help/-hand--version/-vposition-insensitivity stated as normative (§8.4)Tests (150 steps across 10 suites)
--sentinel; full-consume and no-consume cases; phased parsing; regression (equals syntax, negative switches, array fields, remainder order); spec-locking--helpin every position;-hshorthand;--helpafter--; command-scoped help; nested command help routing (outer inner --helpon bothcommands()andprogram(commands(...))); path comparison; Decision B observation; version position-insensitivity; spec-lockingRisks
This is a correctness fix that changes externally visible argv behavior. Remainder will contain fewer tokens (recognized flags are now extracted instead of left in). No previously parsed token stops being parsed. Existing consumers should be checked for assumptions about early termination.
Fixes #11, partially addresses #12