@optique/core 1.0.0-dev.1711 → 1.0.0-dev.1712

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -712,9 +712,23 @@ function or(...args) {
712
712
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
713
713
  let zeroConsumedBranch = null;
714
714
  let zeroConsumedCount = 0;
715
+ let provisionalConsuming = null;
716
+ let provisionalAmbiguous = false;
715
717
  for (const [parser, i] of orderedParsers) {
716
718
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
717
719
  if (result.success && result.consumed.length > 0) {
720
+ if (result.provisional) {
721
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
722
+ index: i,
723
+ parser,
724
+ result
725
+ };
726
+ else {
727
+ provisionalConsuming = null;
728
+ provisionalAmbiguous = true;
729
+ }
730
+ continue;
731
+ }
718
732
  if (activeState?.[0] !== i && activeState?.[1].success) {
719
733
  if (activeState[1].consumed.length === 0) {
720
734
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -812,6 +826,24 @@ function or(...args) {
812
826
  consumed: []
813
827
  };
814
828
  }
829
+ if (provisionalConsuming !== null && !(activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index)) {
830
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
831
+ return {
832
+ success: true,
833
+ provisional: true,
834
+ next: {
835
+ ...context,
836
+ buffer: provisionalConsuming.result.next.buffer,
837
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
838
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
839
+ ...mergedExec != null ? {
840
+ exec: mergedExec,
841
+ dependencyRegistry: mergedExec.dependencyRegistry
842
+ } : {}
843
+ },
844
+ consumed: provisionalConsuming.result.consumed
845
+ };
846
+ }
815
847
  return {
816
848
  ...error,
817
849
  success: false
@@ -824,10 +856,24 @@ function or(...args) {
824
856
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
825
857
  let zeroConsumedBranch = null;
826
858
  let zeroConsumedCount = 0;
859
+ let provisionalConsuming = null;
860
+ let provisionalAmbiguous = false;
827
861
  for (const [parser, i] of orderedParsers) {
828
862
  const resultOrPromise = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
829
863
  const result = await resultOrPromise;
830
864
  if (result.success && result.consumed.length > 0) {
865
+ if (result.provisional) {
866
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
867
+ index: i,
868
+ parser,
869
+ result
870
+ };
871
+ else {
872
+ provisionalConsuming = null;
873
+ provisionalAmbiguous = true;
874
+ }
875
+ continue;
876
+ }
831
877
  if (activeState?.[0] !== i && activeState?.[1].success) {
832
878
  if (activeState[1].consumed.length === 0) {
833
879
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -927,6 +973,24 @@ function or(...args) {
927
973
  consumed: []
928
974
  };
929
975
  }
976
+ if (provisionalConsuming !== null && !(activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index)) {
977
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
978
+ return {
979
+ success: true,
980
+ provisional: true,
981
+ next: {
982
+ ...context,
983
+ buffer: provisionalConsuming.result.next.buffer,
984
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
985
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
986
+ ...mergedExec != null ? {
987
+ exec: mergedExec,
988
+ dependencyRegistry: mergedExec.dependencyRegistry
989
+ } : {}
990
+ },
991
+ consumed: provisionalConsuming.result.consumed
992
+ };
993
+ }
930
994
  return {
931
995
  ...error,
932
996
  success: false
@@ -1020,7 +1084,8 @@ function longestMatch(...args) {
1020
1084
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1021
1085
  if (result.success) {
1022
1086
  const consumed = context.buffer.length - result.next.buffer.length;
1023
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1087
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1088
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1024
1089
  index: i,
1025
1090
  parser,
1026
1091
  result,
@@ -1060,7 +1125,8 @@ function longestMatch(...args) {
1060
1125
  const result = await resultOrPromise;
1061
1126
  if (result.success) {
1062
1127
  const consumed = context.buffer.length - result.next.buffer.length;
1063
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1128
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1129
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1064
1130
  index: i,
1065
1131
  parser,
1066
1132
  result,
@@ -3656,18 +3722,35 @@ function group(label, parser, options = {}) {
3656
3722
  * // defaultResult.value = [undefined, {}]
3657
3723
  * ```
3658
3724
  *
3659
- * ### Async discriminator limitation
3725
+ * ### Speculative branch parsing
3660
3726
  *
3661
3727
  * When the discriminator is an async parser that succeeds without consuming
3662
3728
  * input (e.g., `prompt(option(...))` with no CLI input), branch selection
3663
- * is deferred to the complete phase. If the selected branch needs to
3664
- * consume remaining tokens, those tokens cannot be consumed because the
3665
- * branch is not known during parse. In practice, this means
3666
- * `conditional(prompt(option(...)), { key: option(...) })` cannot parse
3667
- * branch-specific tokens when the discriminator requires interactive
3668
- * resolution. Provide a default branch or ensure the discriminator
3669
- * can resolve synchronously (e.g., via `bindEnv()` or `withDefault()`)
3670
- * to avoid this limitation.
3729
+ * is normally deferred to the complete phase. To allow branch-specific
3730
+ * tokens to be consumed, `conditional()` speculatively tries all named
3731
+ * branches during parse. If exactly one branch can consume tokens, it is
3732
+ * tentatively selected and verified against the resolved discriminator
3733
+ * during the complete phase.
3734
+ *
3735
+ * If the discriminator resolves to a different branch than the one that
3736
+ * consumed tokens (contradictory input), the parse fails. When multiple
3737
+ * branches can consume the same tokens (ambiguous), speculation is skipped
3738
+ * entirely to keep branch selection order-independent.
3739
+ *
3740
+ * #### Known limitations
3741
+ *
3742
+ * - When a default branch accepts the same tokens as a named branch,
3743
+ * speculation prefers the named branch. If the discriminator later
3744
+ * resolves to a value not in the named branches, the parse fails
3745
+ * instead of falling back to the default branch. To avoid this,
3746
+ * ensure named branch options are distinct from the default branch.
3747
+ * - Within `longestMatch()`, a longer speculative match can beat a
3748
+ * shorter definitive one. If the speculative match fails during
3749
+ * completion, the tokens consumed by it are not recoverable.
3750
+ * - The dependency runtime seeds both discriminator and branch sources
3751
+ * before verifying the speculative selection. A discriminator that
3752
+ * depends on branch-local dependency sources could be circularly
3753
+ * confirmed by the speculative branch.
3671
3754
  *
3672
3755
  * @since 0.8.0
3673
3756
  */
@@ -3880,6 +3963,76 @@ function conditional(discriminator, branches, defaultBranch, options) {
3880
3963
  const discriminatorResult = await discriminator.parse({ ...withChildContext(context, "_discriminator", state.discriminatorState, discriminator) });
3881
3964
  if (discriminatorResult.success) {
3882
3965
  if (discriminatorResult.consumed.length === 0 && discriminator.$mode === "async") {
3966
+ const discriminatorExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
3967
+ let speculativeHit;
3968
+ let provisionalHit;
3969
+ let provisionalAmbiguous = false;
3970
+ let speculativeError;
3971
+ let ambiguous = false;
3972
+ for (const [key, bp] of branchParsers) {
3973
+ const branchResult = await bp.parse(withChildContext({
3974
+ ...context,
3975
+ ...discriminatorExec != null ? {
3976
+ exec: discriminatorExec,
3977
+ dependencyRegistry: discriminatorExec.dependencyRegistry
3978
+ } : {}
3979
+ }, "_branch", bp.initialState, bp, bp.usage));
3980
+ if (branchResult.success && branchResult.consumed.length > 0) {
3981
+ if (branchResult.provisional) {
3982
+ if (provisionalHit == null && !provisionalAmbiguous) provisionalHit = {
3983
+ key,
3984
+ bp,
3985
+ result: branchResult
3986
+ };
3987
+ else {
3988
+ provisionalHit = void 0;
3989
+ provisionalAmbiguous = true;
3990
+ }
3991
+ continue;
3992
+ }
3993
+ if (speculativeHit != null) {
3994
+ ambiguous = true;
3995
+ break;
3996
+ }
3997
+ speculativeHit = {
3998
+ key,
3999
+ bp,
4000
+ result: branchResult
4001
+ };
4002
+ }
4003
+ if (!branchResult.success && branchResult.consumed > 0 && (speculativeError == null || speculativeError.consumed < branchResult.consumed)) speculativeError = branchResult;
4004
+ }
4005
+ if (speculativeHit != null && (provisionalHit != null || provisionalAmbiguous)) ambiguous = true;
4006
+ if (speculativeHit == null && !ambiguous && !provisionalAmbiguous && provisionalHit != null) speculativeHit = provisionalHit;
4007
+ if (speculativeHit != null && !ambiguous) {
4008
+ const { key, bp, result: branchResult } = speculativeHit;
4009
+ if (branchResult.success) {
4010
+ const annotatedDiscriminatorState$2 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
4011
+ const mergedExec = mergeChildExec(discriminatorExec, branchResult.next.exec);
4012
+ return {
4013
+ success: true,
4014
+ provisional: true,
4015
+ next: {
4016
+ ...branchResult.next,
4017
+ state: {
4018
+ ...state,
4019
+ discriminatorState: annotatedDiscriminatorState$2,
4020
+ selectedBranch: {
4021
+ kind: "branch",
4022
+ key
4023
+ },
4024
+ branchState: getAnnotatedChildState(state, branchResult.next.state, bp),
4025
+ speculative: true
4026
+ },
4027
+ ...mergedExec != null ? {
4028
+ exec: mergedExec,
4029
+ dependencyRegistry: mergedExec.dependencyRegistry
4030
+ } : {}
4031
+ },
4032
+ consumed: branchResult.consumed
4033
+ };
4034
+ }
4035
+ }
3883
4036
  let deferredBranchState = state.branchState;
3884
4037
  if (defaultBranch !== void 0) {
3885
4038
  const defaultResult = await defaultBranch.parse(withChildContext(context, "_branch", state.branchState ?? defaultBranch.initialState, defaultBranch, defaultBranch.usage));
@@ -3887,6 +4040,7 @@ function conditional(discriminator, branches, defaultBranch, options) {
3887
4040
  const defaultExec = mergeChildExec(context.exec, defaultResult.next.exec);
3888
4041
  return {
3889
4042
  success: true,
4043
+ ...defaultResult.provisional ? { provisional: true } : {},
3890
4044
  next: {
3891
4045
  ...defaultResult.next,
3892
4046
  state: {
@@ -3905,8 +4059,8 @@ function conditional(discriminator, branches, defaultBranch, options) {
3905
4059
  if (!defaultResult.success && defaultResult.consumed > 0) return defaultResult;
3906
4060
  if (defaultResult.success && defaultResult.consumed.length === 0 && context.buffer.length === 0) deferredBranchState = getAnnotatedChildState(state, defaultResult.next.state, defaultBranch);
3907
4061
  }
4062
+ if (speculativeError != null && !ambiguous) return speculativeError;
3908
4063
  const annotatedDiscriminatorState$1 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
3909
- const mergedExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
3910
4064
  return {
3911
4065
  success: true,
3912
4066
  provisional: true,
@@ -3917,9 +4071,9 @@ function conditional(discriminator, branches, defaultBranch, options) {
3917
4071
  discriminatorState: annotatedDiscriminatorState$1,
3918
4072
  branchState: deferredBranchState
3919
4073
  },
3920
- ...mergedExec != null ? {
3921
- exec: mergedExec,
3922
- dependencyRegistry: mergedExec.dependencyRegistry
4074
+ ...discriminatorExec != null ? {
4075
+ exec: discriminatorExec,
4076
+ dependencyRegistry: discriminatorExec.dependencyRegistry
3923
4077
  } : {}
3924
4078
  },
3925
4079
  consumed: []
@@ -4141,6 +4295,18 @@ function conditional(discriminator, branches, defaultBranch, options) {
4141
4295
  };
4142
4296
  };
4143
4297
  const completeAsync = async (state, exec) => {
4298
+ let wasSpeculative = false;
4299
+ if (state.speculative && state.selectedBranch?.kind === "branch") if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4300
+ wasSpeculative = true;
4301
+ state = {
4302
+ ...state,
4303
+ speculative: void 0
4304
+ };
4305
+ } else state = {
4306
+ ...state,
4307
+ discriminatorValue: state.selectedBranch.key,
4308
+ speculative: void 0
4309
+ };
4144
4310
  if (state.selectedBranch === void 0) {
4145
4311
  if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4146
4312
  const annotatedDiscriminatorStateForDeferred = getAnnotatedChildState(state, state.discriminatorState, discriminator);
@@ -4225,20 +4391,26 @@ function conditional(discriminator, branches, defaultBranch, options) {
4225
4391
  };
4226
4392
  const needsDiscriminatorCompletion = state.selectedBranch.kind !== "default" && !(state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key);
4227
4393
  const discriminatorCompleteResult = needsDiscriminatorCompletion ? await discriminator.complete(annotatedDiscriminatorState, withChildExecPath(completionExec, "_discriminator")) : void 0;
4228
- const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4229
- if (!branchResult.success) {
4230
- if (state.discriminatorValue !== void 0 && options?.errors?.branchError) return {
4231
- success: false,
4232
- error: options.errors.branchError(state.discriminatorValue, branchResult.error)
4233
- };
4234
- return branchResult;
4235
- }
4236
4394
  let discriminatorValue;
4237
4395
  if (state.selectedBranch.kind === "default") discriminatorValue = void 0;
4238
4396
  else if (state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key) discriminatorValue = state.discriminatorValue;
4239
4397
  else {
4240
4398
  const completedDiscriminator = unwrapCompleteResult(discriminatorCompleteResult);
4241
- discriminatorValue = completedDiscriminator.success ? completedDiscriminator.value : state.selectedBranch.key;
4399
+ if (completedDiscriminator.success) discriminatorValue = completedDiscriminator.value;
4400
+ else if (wasSpeculative) return completedDiscriminator;
4401
+ else discriminatorValue = state.selectedBranch.key;
4402
+ }
4403
+ if (wasSpeculative && state.selectedBranch.kind === "branch" && discriminatorValue !== state.selectedBranch.key) return {
4404
+ success: false,
4405
+ error: getNoMatchError$1()
4406
+ };
4407
+ const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4408
+ if (!branchResult.success) {
4409
+ if (discriminatorValue !== void 0 && options?.errors?.branchError) return {
4410
+ success: false,
4411
+ error: options.errors.branchError(discriminatorValue, branchResult.error)
4412
+ };
4413
+ return branchResult;
4242
4414
  }
4243
4415
  return {
4244
4416
  success: true,
@@ -1316,6 +1316,7 @@ interface ConditionalState<TDiscriminator extends string> {
1316
1316
  readonly discriminatorValue: TDiscriminator | undefined;
1317
1317
  readonly selectedBranch: SelectedBranch<TDiscriminator> | undefined;
1318
1318
  readonly branchState: unknown;
1319
+ readonly speculative?: true;
1319
1320
  }
1320
1321
  /**
1321
1322
  * Options for customizing error messages in the {@link conditional} combinator.
@@ -1316,6 +1316,7 @@ interface ConditionalState<TDiscriminator extends string> {
1316
1316
  readonly discriminatorValue: TDiscriminator | undefined;
1317
1317
  readonly selectedBranch: SelectedBranch<TDiscriminator> | undefined;
1318
1318
  readonly branchState: unknown;
1319
+ readonly speculative?: true;
1319
1320
  }
1320
1321
  /**
1321
1322
  * Options for customizing error messages in the {@link conditional} combinator.
@@ -712,9 +712,23 @@ function or(...args) {
712
712
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
713
713
  let zeroConsumedBranch = null;
714
714
  let zeroConsumedCount = 0;
715
+ let provisionalConsuming = null;
716
+ let provisionalAmbiguous = false;
715
717
  for (const [parser, i] of orderedParsers) {
716
718
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
717
719
  if (result.success && result.consumed.length > 0) {
720
+ if (result.provisional) {
721
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
722
+ index: i,
723
+ parser,
724
+ result
725
+ };
726
+ else {
727
+ provisionalConsuming = null;
728
+ provisionalAmbiguous = true;
729
+ }
730
+ continue;
731
+ }
718
732
  if (activeState?.[0] !== i && activeState?.[1].success) {
719
733
  if (activeState[1].consumed.length === 0) {
720
734
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -812,6 +826,24 @@ function or(...args) {
812
826
  consumed: []
813
827
  };
814
828
  }
829
+ if (provisionalConsuming !== null && !(activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index)) {
830
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
831
+ return {
832
+ success: true,
833
+ provisional: true,
834
+ next: {
835
+ ...context,
836
+ buffer: provisionalConsuming.result.next.buffer,
837
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
838
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
839
+ ...mergedExec != null ? {
840
+ exec: mergedExec,
841
+ dependencyRegistry: mergedExec.dependencyRegistry
842
+ } : {}
843
+ },
844
+ consumed: provisionalConsuming.result.consumed
845
+ };
846
+ }
815
847
  return {
816
848
  ...error,
817
849
  success: false
@@ -824,10 +856,24 @@ function or(...args) {
824
856
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
825
857
  let zeroConsumedBranch = null;
826
858
  let zeroConsumedCount = 0;
859
+ let provisionalConsuming = null;
860
+ let provisionalAmbiguous = false;
827
861
  for (const [parser, i] of orderedParsers) {
828
862
  const resultOrPromise = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
829
863
  const result = await resultOrPromise;
830
864
  if (result.success && result.consumed.length > 0) {
865
+ if (result.provisional) {
866
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
867
+ index: i,
868
+ parser,
869
+ result
870
+ };
871
+ else {
872
+ provisionalConsuming = null;
873
+ provisionalAmbiguous = true;
874
+ }
875
+ continue;
876
+ }
831
877
  if (activeState?.[0] !== i && activeState?.[1].success) {
832
878
  if (activeState[1].consumed.length === 0) {
833
879
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -927,6 +973,24 @@ function or(...args) {
927
973
  consumed: []
928
974
  };
929
975
  }
976
+ if (provisionalConsuming !== null && !(activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index)) {
977
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
978
+ return {
979
+ success: true,
980
+ provisional: true,
981
+ next: {
982
+ ...context,
983
+ buffer: provisionalConsuming.result.next.buffer,
984
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
985
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
986
+ ...mergedExec != null ? {
987
+ exec: mergedExec,
988
+ dependencyRegistry: mergedExec.dependencyRegistry
989
+ } : {}
990
+ },
991
+ consumed: provisionalConsuming.result.consumed
992
+ };
993
+ }
930
994
  return {
931
995
  ...error,
932
996
  success: false
@@ -1020,7 +1084,8 @@ function longestMatch(...args) {
1020
1084
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1021
1085
  if (result.success) {
1022
1086
  const consumed = context.buffer.length - result.next.buffer.length;
1023
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1087
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1088
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1024
1089
  index: i,
1025
1090
  parser,
1026
1091
  result,
@@ -1060,7 +1125,8 @@ function longestMatch(...args) {
1060
1125
  const result = await resultOrPromise;
1061
1126
  if (result.success) {
1062
1127
  const consumed = context.buffer.length - result.next.buffer.length;
1063
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1128
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1129
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1064
1130
  index: i,
1065
1131
  parser,
1066
1132
  result,
@@ -3656,18 +3722,35 @@ function group(label, parser, options = {}) {
3656
3722
  * // defaultResult.value = [undefined, {}]
3657
3723
  * ```
3658
3724
  *
3659
- * ### Async discriminator limitation
3725
+ * ### Speculative branch parsing
3660
3726
  *
3661
3727
  * When the discriminator is an async parser that succeeds without consuming
3662
3728
  * input (e.g., `prompt(option(...))` with no CLI input), branch selection
3663
- * is deferred to the complete phase. If the selected branch needs to
3664
- * consume remaining tokens, those tokens cannot be consumed because the
3665
- * branch is not known during parse. In practice, this means
3666
- * `conditional(prompt(option(...)), { key: option(...) })` cannot parse
3667
- * branch-specific tokens when the discriminator requires interactive
3668
- * resolution. Provide a default branch or ensure the discriminator
3669
- * can resolve synchronously (e.g., via `bindEnv()` or `withDefault()`)
3670
- * to avoid this limitation.
3729
+ * is normally deferred to the complete phase. To allow branch-specific
3730
+ * tokens to be consumed, `conditional()` speculatively tries all named
3731
+ * branches during parse. If exactly one branch can consume tokens, it is
3732
+ * tentatively selected and verified against the resolved discriminator
3733
+ * during the complete phase.
3734
+ *
3735
+ * If the discriminator resolves to a different branch than the one that
3736
+ * consumed tokens (contradictory input), the parse fails. When multiple
3737
+ * branches can consume the same tokens (ambiguous), speculation is skipped
3738
+ * entirely to keep branch selection order-independent.
3739
+ *
3740
+ * #### Known limitations
3741
+ *
3742
+ * - When a default branch accepts the same tokens as a named branch,
3743
+ * speculation prefers the named branch. If the discriminator later
3744
+ * resolves to a value not in the named branches, the parse fails
3745
+ * instead of falling back to the default branch. To avoid this,
3746
+ * ensure named branch options are distinct from the default branch.
3747
+ * - Within `longestMatch()`, a longer speculative match can beat a
3748
+ * shorter definitive one. If the speculative match fails during
3749
+ * completion, the tokens consumed by it are not recoverable.
3750
+ * - The dependency runtime seeds both discriminator and branch sources
3751
+ * before verifying the speculative selection. A discriminator that
3752
+ * depends on branch-local dependency sources could be circularly
3753
+ * confirmed by the speculative branch.
3671
3754
  *
3672
3755
  * @since 0.8.0
3673
3756
  */
@@ -3880,6 +3963,76 @@ function conditional(discriminator, branches, defaultBranch, options) {
3880
3963
  const discriminatorResult = await discriminator.parse({ ...withChildContext(context, "_discriminator", state.discriminatorState, discriminator) });
3881
3964
  if (discriminatorResult.success) {
3882
3965
  if (discriminatorResult.consumed.length === 0 && discriminator.$mode === "async") {
3966
+ const discriminatorExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
3967
+ let speculativeHit;
3968
+ let provisionalHit;
3969
+ let provisionalAmbiguous = false;
3970
+ let speculativeError;
3971
+ let ambiguous = false;
3972
+ for (const [key, bp] of branchParsers) {
3973
+ const branchResult = await bp.parse(withChildContext({
3974
+ ...context,
3975
+ ...discriminatorExec != null ? {
3976
+ exec: discriminatorExec,
3977
+ dependencyRegistry: discriminatorExec.dependencyRegistry
3978
+ } : {}
3979
+ }, "_branch", bp.initialState, bp, bp.usage));
3980
+ if (branchResult.success && branchResult.consumed.length > 0) {
3981
+ if (branchResult.provisional) {
3982
+ if (provisionalHit == null && !provisionalAmbiguous) provisionalHit = {
3983
+ key,
3984
+ bp,
3985
+ result: branchResult
3986
+ };
3987
+ else {
3988
+ provisionalHit = void 0;
3989
+ provisionalAmbiguous = true;
3990
+ }
3991
+ continue;
3992
+ }
3993
+ if (speculativeHit != null) {
3994
+ ambiguous = true;
3995
+ break;
3996
+ }
3997
+ speculativeHit = {
3998
+ key,
3999
+ bp,
4000
+ result: branchResult
4001
+ };
4002
+ }
4003
+ if (!branchResult.success && branchResult.consumed > 0 && (speculativeError == null || speculativeError.consumed < branchResult.consumed)) speculativeError = branchResult;
4004
+ }
4005
+ if (speculativeHit != null && (provisionalHit != null || provisionalAmbiguous)) ambiguous = true;
4006
+ if (speculativeHit == null && !ambiguous && !provisionalAmbiguous && provisionalHit != null) speculativeHit = provisionalHit;
4007
+ if (speculativeHit != null && !ambiguous) {
4008
+ const { key, bp, result: branchResult } = speculativeHit;
4009
+ if (branchResult.success) {
4010
+ const annotatedDiscriminatorState$2 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
4011
+ const mergedExec = mergeChildExec(discriminatorExec, branchResult.next.exec);
4012
+ return {
4013
+ success: true,
4014
+ provisional: true,
4015
+ next: {
4016
+ ...branchResult.next,
4017
+ state: {
4018
+ ...state,
4019
+ discriminatorState: annotatedDiscriminatorState$2,
4020
+ selectedBranch: {
4021
+ kind: "branch",
4022
+ key
4023
+ },
4024
+ branchState: getAnnotatedChildState(state, branchResult.next.state, bp),
4025
+ speculative: true
4026
+ },
4027
+ ...mergedExec != null ? {
4028
+ exec: mergedExec,
4029
+ dependencyRegistry: mergedExec.dependencyRegistry
4030
+ } : {}
4031
+ },
4032
+ consumed: branchResult.consumed
4033
+ };
4034
+ }
4035
+ }
3883
4036
  let deferredBranchState = state.branchState;
3884
4037
  if (defaultBranch !== void 0) {
3885
4038
  const defaultResult = await defaultBranch.parse(withChildContext(context, "_branch", state.branchState ?? defaultBranch.initialState, defaultBranch, defaultBranch.usage));
@@ -3887,6 +4040,7 @@ function conditional(discriminator, branches, defaultBranch, options) {
3887
4040
  const defaultExec = mergeChildExec(context.exec, defaultResult.next.exec);
3888
4041
  return {
3889
4042
  success: true,
4043
+ ...defaultResult.provisional ? { provisional: true } : {},
3890
4044
  next: {
3891
4045
  ...defaultResult.next,
3892
4046
  state: {
@@ -3905,8 +4059,8 @@ function conditional(discriminator, branches, defaultBranch, options) {
3905
4059
  if (!defaultResult.success && defaultResult.consumed > 0) return defaultResult;
3906
4060
  if (defaultResult.success && defaultResult.consumed.length === 0 && context.buffer.length === 0) deferredBranchState = getAnnotatedChildState(state, defaultResult.next.state, defaultBranch);
3907
4061
  }
4062
+ if (speculativeError != null && !ambiguous) return speculativeError;
3908
4063
  const annotatedDiscriminatorState$1 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
3909
- const mergedExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
3910
4064
  return {
3911
4065
  success: true,
3912
4066
  provisional: true,
@@ -3917,9 +4071,9 @@ function conditional(discriminator, branches, defaultBranch, options) {
3917
4071
  discriminatorState: annotatedDiscriminatorState$1,
3918
4072
  branchState: deferredBranchState
3919
4073
  },
3920
- ...mergedExec != null ? {
3921
- exec: mergedExec,
3922
- dependencyRegistry: mergedExec.dependencyRegistry
4074
+ ...discriminatorExec != null ? {
4075
+ exec: discriminatorExec,
4076
+ dependencyRegistry: discriminatorExec.dependencyRegistry
3923
4077
  } : {}
3924
4078
  },
3925
4079
  consumed: []
@@ -4141,6 +4295,18 @@ function conditional(discriminator, branches, defaultBranch, options) {
4141
4295
  };
4142
4296
  };
4143
4297
  const completeAsync = async (state, exec) => {
4298
+ let wasSpeculative = false;
4299
+ if (state.speculative && state.selectedBranch?.kind === "branch") if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4300
+ wasSpeculative = true;
4301
+ state = {
4302
+ ...state,
4303
+ speculative: void 0
4304
+ };
4305
+ } else state = {
4306
+ ...state,
4307
+ discriminatorValue: state.selectedBranch.key,
4308
+ speculative: void 0
4309
+ };
4144
4310
  if (state.selectedBranch === void 0) {
4145
4311
  if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4146
4312
  const annotatedDiscriminatorStateForDeferred = getAnnotatedChildState(state, state.discriminatorState, discriminator);
@@ -4225,20 +4391,26 @@ function conditional(discriminator, branches, defaultBranch, options) {
4225
4391
  };
4226
4392
  const needsDiscriminatorCompletion = state.selectedBranch.kind !== "default" && !(state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key);
4227
4393
  const discriminatorCompleteResult = needsDiscriminatorCompletion ? await discriminator.complete(annotatedDiscriminatorState, withChildExecPath(completionExec, "_discriminator")) : void 0;
4228
- const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4229
- if (!branchResult.success) {
4230
- if (state.discriminatorValue !== void 0 && options?.errors?.branchError) return {
4231
- success: false,
4232
- error: options.errors.branchError(state.discriminatorValue, branchResult.error)
4233
- };
4234
- return branchResult;
4235
- }
4236
4394
  let discriminatorValue;
4237
4395
  if (state.selectedBranch.kind === "default") discriminatorValue = void 0;
4238
4396
  else if (state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key) discriminatorValue = state.discriminatorValue;
4239
4397
  else {
4240
4398
  const completedDiscriminator = unwrapCompleteResult(discriminatorCompleteResult);
4241
- discriminatorValue = completedDiscriminator.success ? completedDiscriminator.value : state.selectedBranch.key;
4399
+ if (completedDiscriminator.success) discriminatorValue = completedDiscriminator.value;
4400
+ else if (wasSpeculative) return completedDiscriminator;
4401
+ else discriminatorValue = state.selectedBranch.key;
4402
+ }
4403
+ if (wasSpeculative && state.selectedBranch.kind === "branch" && discriminatorValue !== state.selectedBranch.key) return {
4404
+ success: false,
4405
+ error: getNoMatchError$1()
4406
+ };
4407
+ const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4408
+ if (!branchResult.success) {
4409
+ if (discriminatorValue !== void 0 && options?.errors?.branchError) return {
4410
+ success: false,
4411
+ error: options.errors.branchError(discriminatorValue, branchResult.error)
4412
+ };
4413
+ return branchResult;
4242
4414
  }
4243
4415
  return {
4244
4416
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1711+3514d59b",
3
+ "version": "1.0.0-dev.1712+ea879e28",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",