You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The dependency system's internal state types (DeferredParseState, DependencySourceState, PendingDependencySourceState) and helper functions have penetrated deeply into parser implementations beyond the construct layer where dependency resolution actually lives. An audit of dependency.ts imports across the codebase shows:
File
Imported symbols
Role
primitives.ts
16
State creation (DeferredParseState, DependencySourceState, PendingDependencySourceState), three-way state dispatch in complete(), registry queries in suggestions
modifiers.ts
9
Wrapped dependency source detection/propagation in optional() and withDefault(), DependencySourceState creation for default values, transformsDependencyValue marker handling
The coupling in constructs.ts is expected since constructs own the resolution pipeline. The coupling in primitives.ts and modifiers.ts is the concern.
In primitives.ts, option() and argument() are leaf parsers whose core job is token matching and value-parser delegation. Yet they import 16 dependency symbols to wrap parse results in the right state type, dispatch on three dependency state variants in complete(), and query a DependencyRegistry during suggestions. In modifiers.ts, optional() and withDefault() need to detect whether their inner parser is a wrapped dependency source, propagate wrappedDependencySourceMarker through the parser chain, and create DependencySourceState when supplying default values.
Violation of single responsibility. Wrapping results in DeferredParseState, creating PendingDependencySourceState for initial state, propagating wrappedDependencySourceMarker, and querying a DependencyRegistry during suggestions are all dependency-resolution concerns that belong in the construct layer, not in leaf parsers or modifiers.
Fragile state dispatch. The complete() methods in option() and argument() contain a chain of isDeferredParseState / isDependencySourceState / isPendingDependencySourceState / plain ValueParserResult checks. Modifiers like optional() and withDefault() have their own parallel dispatch for wrapped dependency sources. Adding a new dependency-related state type would require updating all of these sites.
Possible approaches
A. Let value parsers wrap their own results
DerivedValueParser and DependencySource would return their dependency-aware state types directly from parse(), removing the need for createOptionParseState() in primitives.ts. Similarly, modifiers would not need to detect or propagate dependency markers because the value parser's result would already carry the necessary information.
Pros:
Removes dependency-related branching from both primitives.ts and potentially modifiers.ts.
Each value parser is responsible for its own state representation.
Cons:
ValueParserResult<T> (or the parser's return type) would need to become a wider union, or a new wrapper type would be needed. This risks leaking dependency concepts into the value-parser interface, which is currently clean and simple.
complete() in primitives still needs to unwrap whatever the value parser returned, unless constructs take over completion entirely.
May not fully address the modifiers.ts coupling, since optional() and withDefault() need to propagate dependency information through the parser chain regardless of who creates the state.
B. Move wrapping/unwrapping into constructs
Primitives would always produce plain ValueParserResult<T>. Modifiers would pass through results without dependency-specific handling. The constructs (object(), tuple(), merge(), concat()) would inspect child parsers' value parsers to determine whether deferred resolution is needed and manage the raw-input bookkeeping themselves.
Pros:
Primitives and modifiers become truly dependency-agnostic.
Dependency resolution is fully centralized in the construct layer, which already owns the DependencyRegistry and the resolveDeferredParseStates() pipeline.
Cons:
Constructs would need access to the raw input string that produced each child's result, since deferred resolution requires re-parsing. This likely means extending the Parser interface (e.g., a rawInputs field on ParserContext, or a richer result type from parse()) so that constructs can retrieve the original tokens.
The wrappedDependencySourceMarker propagation in modifiers exists because withDefault() needs to supply a default value as a dependency source. Moving this to constructs may require constructs to understand the modifier chain, which could trade one form of coupling for another.
Other considerations
A hybrid approach may also be viable: primitives and modifiers could annotate their results with minimal metadata (e.g., the raw input string) without knowing why the metadata is needed, and constructs would use that metadata for dependency resolution. This would keep primitives and modifiers simple while avoiding a full Parser interface change.
Scope
This is a refactoring issue and should not change user-facing behavior. It should be addressed before 1.0 to reduce the risk of dependency-related bugs in leaf parsers and modifiers going forward.
Problem
The dependency system's internal state types (
DeferredParseState,DependencySourceState,PendingDependencySourceState) and helper functions have penetrated deeply into parser implementations beyond the construct layer where dependency resolution actually lives. An audit of dependency.ts imports across the codebase shows:DeferredParseState,DependencySourceState,PendingDependencySourceState), three-way state dispatch incomplete(), registry queries in suggestionsoptional()andwithDefault(),DependencySourceStatecreation for default values,transformsDependencyValuemarker handlingDependencyRegistrymanagement,resolveDeferredParseStates(), pre-completion, pending dependency detectionThe coupling in constructs.ts is expected since constructs own the resolution pipeline. The coupling in primitives.ts and modifiers.ts is the concern.
In primitives.ts,
option()andargument()are leaf parsers whose core job is token matching and value-parser delegation. Yet they import 16 dependency symbols to wrap parse results in the right state type, dispatch on three dependency state variants incomplete(), and query aDependencyRegistryduring suggestions. In modifiers.ts,optional()andwithDefault()need to detect whether their inner parser is a wrapped dependency source, propagatewrappedDependencySourceMarkerthrough the parser chain, and createDependencySourceStatewhen supplying default values.This coupling causes concrete problems:
option()andargument()do not resolve derived parser dependencies #238) requires verifying that each primitive and modifier correctly handles every dependency state type. The Top-leveloption()andargument()do not resolve derived parser dependencies #238 fix was ultimately a comment update in primitives.ts because the existingpreliminaryResultpath was already correct, but it took several review rounds to reach that conclusion precisely because the branching logic is spread across files.DeferredParseState, creatingPendingDependencySourceStatefor initial state, propagatingwrappedDependencySourceMarker, and querying aDependencyRegistryduring suggestions are all dependency-resolution concerns that belong in the construct layer, not in leaf parsers or modifiers.complete()methods inoption()andargument()contain a chain ofisDeferredParseState/isDependencySourceState/isPendingDependencySourceState/ plainValueParserResultchecks. Modifiers likeoptional()andwithDefault()have their own parallel dispatch for wrapped dependency sources. Adding a new dependency-related state type would require updating all of these sites.Possible approaches
A. Let value parsers wrap their own results
DerivedValueParserandDependencySourcewould return their dependency-aware state types directly fromparse(), removing the need forcreateOptionParseState()in primitives.ts. Similarly, modifiers would not need to detect or propagate dependency markers because the value parser's result would already carry the necessary information.Pros:
Cons:
ValueParserResult<T>(or the parser's return type) would need to become a wider union, or a new wrapper type would be needed. This risks leaking dependency concepts into the value-parser interface, which is currently clean and simple.complete()in primitives still needs to unwrap whatever the value parser returned, unless constructs take over completion entirely.optional()andwithDefault()need to propagate dependency information through the parser chain regardless of who creates the state.B. Move wrapping/unwrapping into constructs
Primitives would always produce plain
ValueParserResult<T>. Modifiers would pass through results without dependency-specific handling. The constructs (object(),tuple(),merge(),concat()) would inspect child parsers' value parsers to determine whether deferred resolution is needed and manage the raw-input bookkeeping themselves.Pros:
DependencyRegistryand theresolveDeferredParseStates()pipeline.Cons:
Parserinterface (e.g., arawInputsfield onParserContext, or a richer result type fromparse()) so that constructs can retrieve the original tokens.wrappedDependencySourceMarkerpropagation in modifiers exists becausewithDefault()needs to supply a default value as a dependency source. Moving this to constructs may require constructs to understand the modifier chain, which could trade one form of coupling for another.Other considerations
A hybrid approach may also be viable: primitives and modifiers could annotate their results with minimal metadata (e.g., the raw input string) without knowing why the metadata is needed, and constructs would use that metadata for dependency resolution. This would keep primitives and modifiers simple while avoiding a full
Parserinterface change.Scope
This is a refactoring issue and should not change user-facing behavior. It should be addressed before 1.0 to reduce the risk of dependency-related bugs in leaf parsers and modifiers going forward.