Skip to content

Commit ba87647

Browse files
Smaug123claude
andauthored
Scan positional tokens into candidate-bearing events (#577)
A positional token's spelling now determines the set of sinks which could consume it — every sink for a bare token (including everything after `--` and flag-like tokens collected in Collect mode), the claimants of the key for a keyed one — and the scan event carries that candidate set, leaving the actual consumer unresolved until after case selection. Scanning remains independent of the eventual case: the claimant computation reads only the schema's lexical surface. For the schemas the generator emits today (at most one sink) the metadata is fully determined, pinned by a property; scan-losslessness is unchanged. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1cdf0f8 commit ba87647

7 files changed

Lines changed: 592 additions & 248 deletions

File tree

ConsumePlugin/GeneratedArgParserNegationTests.fs

Lines changed: 80 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,32 @@ module private ArgParserRuntime_BoolNegation =
146146
/// `--rest value`; the key text is recorded as spelled.
147147
| KeySpaced of key : string
148148

149+
/// One value belonging to the positional stream. Scanning does not decide which sink
150+
/// receives it: the token's spelling determines only the *candidate* sinks, and the actual
151+
/// consumer is resolved after case selection. (For today's schemas, with at most one sink,
152+
/// the candidate set is that sink alone.)
153+
type PositionalEvent =
154+
{
155+
Value : string
156+
/// True for tokens which appeared after the `--` separator.
157+
AfterSeparator : bool
158+
/// How the value was spelled; preserved exactly, for diagnostics and lossless
159+
/// reconstruction of argv.
160+
Form : PositionalForm
161+
/// The sinks which could consume this token: every sink for a bare token
162+
/// (including everything after `--`, and flag-like tokens collected in Collect
163+
/// mode), the claimants of the key for a keyed one.
164+
Candidates : Set<int>
165+
}
166+
149167
/// The scan phase emits an ordered log of these; the typed layer folds over the log in order,
150168
/// so that e.g. conversion errors interleave with structural errors in argv order.
151169
[<RequireQualifiedAccess>]
152170
type ScanEvent =
153171
| Occurrence of ErasedOccurrence
154-
/// A value routed to the positional sink (or to the leftover-args accumulator, for a
155-
/// schema with no sink). `afterSeparator` is true for tokens which appeared after `--`.
156-
| Positional of value : string * afterSeparator : bool * form : PositionalForm
172+
/// A value belonging to the positional stream (or to the leftover-args accumulator,
173+
/// for a schema with no sink).
174+
| Positional of PositionalEvent
157175
| Error of ScanError
158176
/// A `--help`-shaped token was seen in key position; the token itself is recorded (the
159177
/// match is case-insensitive, so it may be e.g. "--HELP").
@@ -178,9 +196,10 @@ module private ArgParserRuntime_BoolNegation =
178196
type private ScanState =
179197
| AwaitingKey
180198
| AwaitingValue of leaf : (ErasedLeaf * bool) option * source : string
181-
/// The positional sink's own key (`--rest`) was seen; the next token is its value,
182-
/// consumed greedily (keyed positionals always take exactly one value).
183-
| AwaitingPositionalValue of source : string
199+
/// A positional sink's own key (`--rest`) was seen; the next token is its value,
200+
/// consumed greedily (keyed positionals always take exactly one value). `candidates`
201+
/// are the sinks claiming the key.
202+
| AwaitingPositionalValue of candidates : Set<int> * source : string
184203

185204
/// Match a full `--key` token (value part already split off) against the leaf table.
186205
/// Returns the leaf and whether the match was via the negated `--no-` form.
@@ -236,13 +255,29 @@ module private ArgParserRuntime_BoolNegation =
236255
| ErasedFlagLikeBehaviour.Reject -> false
237256
)
238257

239-
/// Does this full `--key` token (value part already split off) name a positional sink?
240-
let isPositionalKey (key : string) : bool =
258+
/// Every sink: the candidate set of a bare positional token.
259+
let allSinks : Set<int> =
260+
schema.Positionals |> List.map (fun p -> p.Id) |> Set.ofList
261+
262+
/// The sinks claiming this full `--key` token (value part already split off); empty
263+
/// when the token does not name a positional sink at all.
264+
let positionalClaimants (key : string) : Set<int> =
241265
schema.Positionals
242-
|> List.exists (fun p ->
266+
|> List.filter (fun p ->
243267
p.Forms
244268
|> List.exists (fun form -> String.Equals (key, "--" + form, StringComparison.OrdinalIgnoreCase))
245269
)
270+
|> List.map (fun p -> p.Id)
271+
|> Set.ofList
272+
273+
let bareToken (value : string) (afterSeparator : bool) : ScanEvent =
274+
ScanEvent.Positional
275+
{
276+
Value = value
277+
AfterSeparator = afterSeparator
278+
Form = PositionalForm.Bare
279+
Candidates = allSinks
280+
}
246281

247282
/// Resolve a pending key which will receive no value (end of input, or `--` next).
248283
let resolvePending (state : ScanState) : ScanEvent list =
@@ -262,22 +297,19 @@ module private ArgParserRuntime_BoolNegation =
262297
]
263298
| ErasedArity.One -> [ ScanEvent.Error (ScanError.TrailingKeyNoValue source) ]
264299
| ScanState.AwaitingValue (None, source) -> [ ScanEvent.Error (ScanError.TrailingKeyNoValue source) ]
265-
| ScanState.AwaitingPositionalValue source -> [ ScanEvent.Error (ScanError.TrailingKeyNoValue source) ]
300+
| ScanState.AwaitingPositionalValue (_, source) -> [ ScanEvent.Error (ScanError.TrailingKeyNoValue source) ]
266301

267302
let rec go (state : ScanState) (acc : ScanEvent list) (args : string list) : ScanEvent list =
268303
match args with
269304
| [] -> List.rev acc @ resolvePending state
270305
| "--" :: rest ->
271-
let positionals =
272-
rest
273-
|> List.map (fun token -> ScanEvent.Positional (token, true, PositionalForm.Bare))
274-
306+
let positionals = rest |> List.map (fun token -> bareToken token true)
275307
List.rev acc @ resolvePending state @ (ScanEvent.Separator :: positionals)
276308
| arg :: rest ->
277309
match state with
278310
| ScanState.AwaitingKey ->
279311
if not (arg.StartsWith ("--", StringComparison.Ordinal)) then
280-
go ScanState.AwaitingKey (ScanEvent.Positional (arg, false, PositionalForm.Bare) :: acc) rest
312+
go ScanState.AwaitingKey (bareToken arg false :: acc) rest
281313
elif String.Equals (arg, "--help", StringComparison.OrdinalIgnoreCase) then
282314
go ScanState.AwaitingKey (ScanEvent.Help arg :: acc) rest
283315
else
@@ -299,16 +331,21 @@ module private ArgParserRuntime_BoolNegation =
299331

300332
go ScanState.AwaitingKey (ScanEvent.Occurrence occurrence :: acc) rest
301333
| None ->
302-
if isPositionalKey key then
303-
go
304-
ScanState.AwaitingKey
305-
(ScanEvent.Positional (value, false, PositionalForm.KeyEquals key) :: acc)
306-
rest
334+
let claimants = positionalClaimants key
335+
336+
if not (Set.isEmpty claimants) then
337+
let event =
338+
ScanEvent.Positional
339+
{
340+
Value = value
341+
AfterSeparator = false
342+
Form = PositionalForm.KeyEquals key
343+
Candidates = claimants
344+
}
345+
346+
go ScanState.AwaitingKey (event :: acc) rest
307347
elif collectFlagLike then
308-
go
309-
ScanState.AwaitingKey
310-
(ScanEvent.Positional (arg, false, PositionalForm.Bare) :: acc)
311-
rest
348+
go ScanState.AwaitingKey (bareToken arg false :: acc) rest
312349
else
313350
go
314351
ScanState.AwaitingKey
@@ -318,8 +355,10 @@ module private ArgParserRuntime_BoolNegation =
318355
match matchLeaf schema.Leaves arg with
319356
| Some matched -> go (ScanState.AwaitingValue (Some matched, arg)) acc rest
320357
| None ->
321-
if isPositionalKey arg then
322-
go (ScanState.AwaitingPositionalValue arg) acc rest
358+
let claimants = positionalClaimants arg
359+
360+
if not (Set.isEmpty claimants) then
361+
go (ScanState.AwaitingPositionalValue (claimants, arg)) acc rest
323362
else
324363
go (ScanState.AwaitingValue (None, arg)) acc rest
325364
| ScanState.AwaitingValue (Some (leaf, negated), source) ->
@@ -349,17 +388,20 @@ module private ArgParserRuntime_BoolNegation =
349388
go ScanState.AwaitingKey (ScanEvent.Occurrence occurrence :: acc) (arg :: rest)
350389
| ScanState.AwaitingValue (None, source) ->
351390
if collectFlagLike then
352-
go
353-
ScanState.AwaitingKey
354-
(ScanEvent.Positional (source, false, PositionalForm.Bare) :: acc)
355-
(arg :: rest)
391+
go ScanState.AwaitingKey (bareToken source false :: acc) (arg :: rest)
356392
else
357393
go ScanState.AwaitingKey (ScanEvent.Error (ScanError.UnknownKey source) :: acc) (arg :: rest)
358-
| ScanState.AwaitingPositionalValue source ->
359-
go
360-
ScanState.AwaitingKey
361-
(ScanEvent.Positional (arg, false, PositionalForm.KeySpaced source) :: acc)
362-
rest
394+
| ScanState.AwaitingPositionalValue (candidates, source) ->
395+
let event =
396+
ScanEvent.Positional
397+
{
398+
Value = arg
399+
AfterSeparator = false
400+
Form = PositionalForm.KeySpaced source
401+
Candidates = candidates
402+
}
403+
404+
go ScanState.AwaitingKey (event :: acc) rest
363405

364406
go ScanState.AwaitingKey [] args
365407

@@ -594,7 +636,7 @@ module private ArgParserRuntime_BoolNegation =
594636
events
595637
|> List.choose (fun event ->
596638
match event with
597-
| ScanEvent.Positional (value, _, _) -> Some value
639+
| ScanEvent.Positional positional -> Some positional.Value
598640
| _ -> None
599641
)
600642

@@ -1083,11 +1125,11 @@ module private ArgParserRuntime_BoolNegation =
10831125
| None -> stored.Add occurrence.LeafId |> ignore<bool>
10841126

10851127
consume rest
1086-
| ScanEvent.Positional (value, afterSeparator, _) ->
1128+
| ScanEvent.Positional positional ->
10871129
match schema.Positionals with
10881130
| [] -> consume rest
10891131
| _ :: _ ->
1090-
match callbacks.StorePositional value afterSeparator with
1132+
match callbacks.StorePositional positional.Value positional.AfterSeparator with
10911133
| Some error -> errors.Add error
10921134
| None -> ()
10931135

0 commit comments

Comments
 (0)