Browse Source

G-084 inc 1: value -parsekey support + leader -parsekey define-time rejection + defaults precedence

G-084 (punk::args 0.23.0) increment 1 - parsekey completeness part 1:

Value -parsekey is now supported (was: accepted at define time but aborted
parse). A value record declaring -parsekey renames its result `values` slot
and its `received` key to the parsekey (e.g `@values {v1 -parsekey renamed
-type string}` parses `hello` to `values={renamed hello}`, `received` keyed
by `renamed`), and -multiple value members collect under the parsekey (the
`variable` setvalues form `?name value...?` pairs collect under `name_value`
- the motivating caller that carried the `#todo - fix -parsekey for leaders
and values` moduledoc comment).

Implementation: VAL_DEFAULTS and the values_dict init/storage are keyed by
parsekey (matching the already-parsekey-keyed VAL_REQUIRED); a new
api_valnames_received list tracks the parsekey (the internal valnames_received
stays argname-keyed for the -multiple first/continuation gate and G-053
occurrence enforcement); api_valnames_received drives the
required-satisfaction check and the received dict; a per-form
val_pk2name/leader_pk2name reverse map lets the valmin clause-length fallback
and the post-parse validation loop resolve a parsekey back to its argname
for ARG_INFO/arg_checks lookups.

-parsekey on a leader is now REJECTED at define time with a clear message
(was: silently ignored) - no live caller uses a leader -parsekey, so the
goal's no-silent-ignore contract is met by rejection rather than parallel
hot-path surgery; the dead req_name derivation in the leaders resolve branch
is removed. The @values directive line still rejects -parsekey (a
group-default parsekey is not a feature; per-arg value -parsekey is).

The defaulted-members precedence rule for a shared-parsekey group (none
received) is now documented as last-defined-member-wins (deterministic in
definition order) and the in-code `? review` is removed.

parsekey.test: the value GAP flipped to parsekey_value_result_key_settled,
the leader GAP flipped to parsekey_leader_parsekey_rejected, the @values-line
GAP split to parsekey_values_line_rejects_parsekey; the defaults GAP flipped
to a settled pin (parsekey_shared_key_member_defaults).
dev/parsekey-knownbugs.test: the value and leader disabled pins retired
(settled in parsekey.test); the cross-member -multiple accumulation pin
remains (increment 3, pending the design decision).

Full punk/args suite 399/0 (1 skipped), punk/ns 125/125, broader punk sweep
989/0. Cross-member -multiple collection (inc 3) and the parse_report/
parse_status received-attribution display gap (inc 2) remain.

Assisted-by: harness=pi; primary-model=huggingface/zai-org/GLM-5.2; api-location=huggingface.co
master
Julian Noble 4 days ago
parent
commit
abd8616f3c
  1. 2
      GOALS.md
  2. 2
      goals/G-084-punkargs-parsekey-completeness.md
  3. 101
      src/modules/punk/args-999999.0a1.0.tm
  4. 3
      src/modules/punk/args-buildversion.txt
  5. 3
      src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm
  6. 45
      src/tests/modules/punk/args/testsuites/args/parsekey.test
  7. 48
      src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test

2
GOALS.md

@ -285,7 +285,7 @@ Scope: src/modules/punk/mix/commandset/doc-999999.0a1.0.tm (doc.* commandset - n
Detail: goals/G-081-argdoc-build-pipeline.md Detail: goals/G-081-argdoc-build-pipeline.md
### G-084 [proposed] punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence ### G-084 [active] punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence
Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips) Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips)
Detail: goals/G-084-punkargs-parsekey-completeness.md Detail: goals/G-084-punkargs-parsekey-completeness.md

2
goals/G-084-punkargs-parsekey-completeness.md

@ -1,6 +1,6 @@
# G-084 punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence # G-084 punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence
Status: proposed Status: active
Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips) Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips)
Goal: -parsekey behaves as a complete, position-independent result-keying mechanism: distinct -multiple members sharing a parsekey accumulate under the shared key in received order (no silent value loss), -parsekey on leaders and values renames their result/received keys the same way it does for options (or is rejected at define time everywhere it is unsupported - no silent ignore, no parse-time abort), and the precedence rule for multiple defaulted members of one group is documented behaviour rather than an in-code '? review'. Goal: -parsekey behaves as a complete, position-independent result-keying mechanism: distinct -multiple members sharing a parsekey accumulate under the shared key in received order (no silent value loss), -parsekey on leaders and values renames their result/received keys the same way it does for options (or is rejected at define time everywhere it is unsupported - no silent ignore, no parse-time abort), and the precedence rule for multiple defaulted members of one group is documented behaviour rather than an in-code '? review'.
Acceptance: the three punkargsKnownBug tests in testsuites/dev/parsekey-knownbugs.test are enabled and pass (or are amended to the settled design and pass); the corresponding _GAP pins in parsekey.test are flipped or retired; the '#todo - fix -parsekey for leaders and values' moduledoc comment is resolved; the full existing punk::args suite passes untouched. Acceptance: the three punkargsKnownBug tests in testsuites/dev/parsekey-knownbugs.test are enabled and pass (or are amended to the settled design and pass); the corresponding _GAP pins in parsekey.test are flipped or retired; the '#todo - fix -parsekey for leaders and values' moduledoc comment is resolved; the full existing punk::args suite passes untouched.

101
src/modules/punk/args-999999.0a1.0.tm

@ -3697,12 +3697,13 @@ tcl::namespace::eval punk::args {
} }
} else { } else {
if {[dict get $FDICT argspace] eq "leaders"} { if {[dict get $FDICT argspace] eq "leaders"} {
if {[dict exists $spec_merged -parsekey]} { #G-084: -parsekey on a leader is not supported (no live
#if parsekey exists, we use that in the required list instead of argname, as that's what the parser will be looking for when it checks for required args #caller; the leaders parse loop has no parsekey keying).
set req_name [dict get $spec_merged -parsekey] #Reject at define time per the goal's no-silent-ignore contract.
} else { if {[dict exists $spec_merged -parsekey] && [dict get $spec_merged -parsekey] ne ""} {
set req_name $argname error "punk::args::resolve - -parsekey is not supported on a leader argument ('$argname'). Use -parsekey on an option or a value, or rename the leader argument itself. @id:$DEF_definition_id"
} }
set req_name $argname
if {$req_name ni $upd_LEADER_REQUIRED} { if {$req_name ni $upd_LEADER_REQUIRED} {
lappend upd_LEADER_REQUIRED $argname lappend upd_LEADER_REQUIRED $argname
} }
@ -3736,7 +3737,16 @@ tcl::namespace::eval punk::args {
dict set upd_LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] dict set upd_LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default]
#tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] #tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default]
} else { } else {
dict set upd_VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] #G-084: key VAL_DEFAULTS by parsekey (when present) so the
#default lands on the same parsekey-keyed values_dict slot the
#parse loop stores received values under. Matches VAL_REQUIRED
#which is already parsekey-keyed.
set _valdef_key $argname
if {[dict exists $spec_merged -parsekey]} {
set _pk [dict get $spec_merged -parsekey]
if {$_pk ne ""} {set _valdef_key $_pk}
}
dict set upd_VAL_DEFAULTS $_valdef_key [tcl::dict::get $spec_merged -default]
#tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] #tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default]
} }
} }
@ -10706,6 +10716,29 @@ tcl::namespace::eval punk::args {
set FORMDISPLAY [dict get $formdict FORMDISPLAY] set FORMDISPLAY [dict get $formdict FORMDISPLAY]
#G-084: parsekey->argname reverse maps. VAL_REQUIRED/LEADER_REQUIRED and the
#VAL_DEFAULTS/LEADER_DEFAULTS dicts are keyed by parsekey (so they match the
#parsekey-keyed result/received dicts), but ARG_INFO is keyed by argname - so
#any ARG_INFO lookup by a required/default key (e.g the valmin clause-length
#fallback below) must resolve the parsekey back to an argname. Built once per
#form from ARG_INFO; for an arg with no -parsekey the parsekey IS the argname.
set val_pk2name [dict create]
foreach {vn vspec} $ARG_INFO {
if {$vn in $VAL_NAMES} {
set pk [Dict_getdef $vspec -parsekey ""]
if {$pk eq ""} {set pk $vn}
if {![dict exists $val_pk2name $pk]} {dict set val_pk2name $pk $vn}
}
}
set leader_pk2name [dict create]
foreach {vn vspec} $ARG_INFO {
if {$vn in $LEADER_NAMES} {
set pk [Dict_getdef $vspec -parsekey ""]
if {$pk eq ""} {set pk $vn}
if {![dict exists $leader_pk2name $pk]} {dict set leader_pk2name $pk $vn}
}
}
#G-082 single-form error selection: words the allocation screens rejected for an #G-082 single-form error selection: words the allocation screens rejected for an
#OPTIONAL argument (restricted choices, basic type), keyed by the word itself. #OPTIONAL argument (restricted choices, basic type), keyed by the word itself.
#First rejection per word wins (nearest/earliest defined argument - the one the #First rejection per word wins (nearest/earliest defined argument - the one the
@ -10721,12 +10754,15 @@ tcl::namespace::eval punk::args {
set valmin 0 set valmin 0
#set VAL_MIN 0 #set VAL_MIN 0
foreach v $VAL_REQUIRED { foreach v $VAL_REQUIRED {
#G-084: VAL_REQUIRED is keyed by parsekey; resolve to argname for ARG_INFO.
set vname $v
if {[dict exists $val_pk2name $v]} {set vname [dict get $val_pk2name $v]}
# todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks)
# e.g -types {a ?xxx?} # e.g -types {a ?xxx?}
#this has one required and one optional #this has one required and one optional
set clause_length 0 set clause_length 0
#for each t in typelist #for each t in typelist
foreach t [dict get $ARG_INFO $v -type] { foreach t [dict get $ARG_INFO $vname -type] {
if {![string match {\?*\?} $t]} { if {![string match {\?*\?} $t]} {
incr clause_length incr clause_length
} }
@ -11803,7 +11839,13 @@ tcl::namespace::eval punk::args {
if {$parsekey ne $optset} { if {$parsekey ne $optset} {
set tailopt [string trimright [lindex [split $optset |] end] =] set tailopt [string trimright [lindex [split $optset |] end] =]
if {$tailopt ne $parsekey} { if {$tailopt ne $parsekey} {
#defaults for multiple options sharing a -parsekey value ? review #G-084: settled precedence rule for multiple defaulted members of
#one shared-parsekey group (none received): the LAST-defined member's
#-default wins. This loop visits optsets in definition order and applies
#each unreceived member's default via dict set (last wins), so the
#outcome is deterministic in definition order. (No live caller declares
#multiple defaults on one shared-parsekey group - e.g lsearch's
#MATCHSTYLE group defaults only -glob - so this rules the synthetic case.)
dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset] dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset]
} else { } else {
dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset]
@ -12090,10 +12132,15 @@ tcl::namespace::eval punk::args {
## set values_dict $val_defaults ## set values_dict $val_defaults
set values_dict [dict create] set values_dict [dict create]
foreach valname [lrange $VAL_NAMES 0 $num_values-1] { foreach valname [lrange $VAL_NAMES 0 $num_values-1] {
#set ALL valnames to lock in positioning #set ALL valnames to lock in positioning. G-084: key by parsekey so received
#note - later we need to unset any optional that had no default and was not received (no phantom default) #values (stored under val_ident=parsekey) land on the same slot, and the
dict set values_dict $valname {} #VAL_DEFAULTS merge (also parsekey-keyed) overlays the default here.
} set _vkey $valname
set _vpk [Dict_getdef [dict get $ARG_INFO $valname] -parsekey ""]
if {$_vpk ne ""} {set _vkey $_vpk}
dict set values_dict $_vkey {}
}
unset -nocomplain _vkey _vpk
set values_dict [dict merge $values_dict $VAL_DEFAULTS] set values_dict [dict merge $values_dict $VAL_DEFAULTS]
#------------------------------------------ #------------------------------------------
set nameidx 0 set nameidx 0
@ -12217,15 +12264,15 @@ tcl::namespace::eval punk::args {
# tcl::dict::lappend values_dict $valname $clauseval # tcl::dict::lappend values_dict $valname $clauseval
#} #}
if {$valname in $valnames_received} { if {$valname in $valnames_received} {
tcl::dict::lappend values_dict $valname $clauseval tcl::dict::lappend values_dict $api_valname $clauseval
tcl::dict::lappend argument_clause_typestate $valname $newtypelist tcl::dict::lappend argument_clause_typestate $valname $newtypelist
} else { } else {
tcl::dict::set values_dict $valname [list $clauseval] tcl::dict::set values_dict $api_valname [list $clauseval]
tcl::dict::set argument_clause_typestate $valname [list $newtypelist] tcl::dict::set argument_clause_typestate $valname [list $newtypelist]
} }
set valname_multiple $valname set valname_multiple $valname
} else { } else {
tcl::dict::set values_dict $valname $clauseval tcl::dict::set values_dict $api_valname $clauseval
tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect
set valname_multiple "" set valname_multiple ""
incr nameidx incr nameidx
@ -12250,9 +12297,16 @@ tcl::namespace::eval punk::args {
lappend clauseval [lindex $values $validx] lappend clauseval [lindex $values $validx]
} }
} }
tcl::dict::lappend values_dict $valname_multiple $clauseval set _vmkey $valname_multiple
set _vmpk [Dict_getdef [tcl::dict::get $argstate $valname_multiple] -parsekey ""]
if {$_vmpk ne ""} {set _vmkey $_vmpk}
tcl::dict::lappend values_dict $_vmkey $clauseval
#name already seen - but must add to valnames_received anyway (as with opts and leaders) #name already seen - but must add to valnames_received anyway (as with opts and leaders)
lappend valnames_received $valname_multiple lappend valnames_received $valname_multiple
#G-084: continuation occurrences of a -multiple value must also land
#under the parsekey and register in api_valnames_received (the
#parsekey-keyed received/satisfaction tracker) - not just the first.
lappend api_valnames_received $_vmkey
} else { } else {
if {$VAL_UNNAMED} { if {$VAL_UNNAMED} {
tcl::dict::set values_dict $positionalidx $val tcl::dict::set values_dict $positionalidx $val
@ -12314,7 +12368,7 @@ tcl::namespace::eval punk::args {
#review - always trailing - could break? #review - always trailing - could break?
continue continue
} }
if {![dict exists $VAL_DEFAULTS $vname] && $vname ni $valnames_received} { if {![dict exists $VAL_DEFAULTS $vname] && $vname ni $api_valnames_received} {
#remove the name with empty-string default we used to establish fixed order of names #remove the name with empty-string default we used to establish fixed order of names
#The 'values' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. #The 'values' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default.
dict unset values_dict $vname dict unset values_dict $vname
@ -12484,7 +12538,7 @@ tcl::namespace::eval punk::args {
} }
} }
if {[llength $VAL_REQUIRED] && !$skip_valuemissing} { if {[llength $VAL_REQUIRED] && !$skip_valuemissing} {
if {[llength [set missing [punk::args::system::punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { if {[llength [set missing [punk::args::system::punklib_ldiff $VAL_REQUIRED $api_valnames_received]]]} {
set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg
#arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs
@ -12565,7 +12619,7 @@ tcl::namespace::eval punk::args {
unset -nocomplain _g83_received_optsets _g83_n _g83_effpk _g83_pk_to_optsets _g83_optset _g83_p _g83_grp _g83_conflicts _g83_t _g83_t_optsets _g83_t_optset _g83_g _g83_ginfo _g83_g_received _g83_a _g83_b unset -nocomplain _g83_received_optsets _g83_n _g83_effpk _g83_pk_to_optsets _g83_optset _g83_p _g83_grp _g83_conflicts _g83_t _g83_t_optsets _g83_t_optset _g83_g _g83_ginfo _g83_g_received _g83_a _g83_b
#--------------------------------------------------------------------------------------------- #---------------------------------------------------------------------------------------------
#maintain order of opts $opts values $values as caller may use lassign. #maintain order of opts $opts values $values as caller may use lassign.
set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$api_valnames_received]
if {[llength $receivednames]} { if {[llength $receivednames]} {
#flat zip of names with overall posn, including opts #flat zip of names with overall posn, including opts
#set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]]
@ -12644,6 +12698,15 @@ tcl::namespace::eval punk::args {
} else { } else {
#leader or value. #leader or value.
set argname $argname_or_ident set argname $argname_or_ident
#G-084: values_dict/leaders_dict are keyed by parsekey, but arg_checks/argstate
#are argname-keyed - resolve the parsekey back to its argname. (For a value
#or leader without -parsekey the parsekey IS the argname, so the maps hold a
#self-entry and this is a no-op.)
if {[dict exists $val_pk2name $argname]} {
set argname [dict get $val_pk2name $argname]
} elseif {[dict exists $leader_pk2name $argname]} {
set argname [dict get $leader_pk2name $argname]
}
#set pkoverride [Dict_getdef $argstate $argname -parsekey ""] #set pkoverride [Dict_getdef $argstate $argname -parsekey ""]
#TODO? #TODO?
if {$pkoverride ne ""} { if {$pkoverride ne ""} {

3
src/modules/punk/args-buildversion.txt

@ -1,6 +1,7 @@
0.22.0 0.23.0
#First line must be a semantic version number #First line must be a semantic version number
#all other lines are ignored. #all other lines are ignored.
#0.23.0 - G-084 increment 1 (parsekey completeness, part 1): value -parsekey is now supported (was: accepted at define time but aborted parse). A value record declaring -parsekey now renames its result `values` slot and its `received` key to the parsekey (e.g `@values {v1 -parsekey renamed -type string}` parses `hello` to `values={renamed hello}`, `received` keyed by `renamed`), and -multiple value members collect under the parsekey (e.g the `variable` setvalues form `?name value...?` pairs collect under `name_value`). The fix keys VAL_DEFAULTS and the values_dict init/storage by parsekey (matching the already-parsekey-keyed VAL_REQUIRED), tracks the parsekey in a new api_valnames_received list (the internal valnames_received stays argname-keyed for the -multiple first/continuation gate and G-053 occurrence enforcement), uses api_valnames_received for the required-satisfaction check and the received dict, and adds a per-form val_pk2name/leader_pk2name reverse map so the valmin clause-length fallback and the post-parse validation loop can resolve a parsekey back to its argname for ARG_INFO/arg_checks lookups. -parsekey on a leader is now REJECTED at define time with a clear message (was: silently ignored) - no live caller uses a leader -parsekey, so the goal's no-silent-ignore contract is met by rejection rather than parallel hot-path surgery; the dead req_name derivation in the leaders resolve branch is removed. The @values directive line still rejects -parsekey (a group-default parsekey is not a feature; per-arg value -parsekey is). The defaulted-members precedence rule for a shared-parsekey group (none received) is now documented as last-defined-member-wins (deterministic in definition order) and the in-code `? review` is removed. The tclcore moduledoc `#todo - fix -parsekey for leaders and values` is resolved (value supported, leader rejected). parsekey.test: the value GAP flipped to parsekey_value_result_key_settled, the leader GAP flipped to parsekey_leader_parsekey_rejected, the @values-line GAP split to parsekey_values_line_rejects_parsekey; the defaults GAP flipped to a settled pin. dev/parsekey-knownbugs.test: the value and leader disabled pins retired (settled in parsekey.test); the cross-member -multiple accumulation pin remains (increment 3). Full punk/args suite 399/0 (1 skipped), punk/ns 125/125, broader punk sweep 989/0.
#0.22.0 - G-053 range-valued -multiple (occurrence arity): -multiple now accepts a {min max} range (max -1 = unbounded) alongside the legacy booleans 0/1, so a definition can declare at most once ({0 1}, a second occurrence is a parse error), bounded repetition ({2 4}), or one-or-more ({1 -1}) instead of choosing between silent last-wins (0) and unbounded collection (1). The spec compiler canonicalises once at resolve: the stored -multiple becomes the computed boolean (list-shape collect: true for max>1 or -1, false for legacy 0 and max==1) so every existing collect-vs-replace / scalar-vs-list / leader-value-single-multiple hot-path truth-test stays correct, and the range companions (min/max/maxbounded) live in a separate per-form MULTIPLE_RANGES dict (NOT in ARG_INFO, so they do not ride along when ARG_INFO is round-tripped as a spec via resolved_def copyfrom). Legacy 0/1 (and boolean strings true/false/yes/no) are coerced to the boolean and stay byte-unchanged (no MULTIPLE_RANGES entry for unlimited cases). Resolve validation: max must be positive or -1, min <= max, and the -optional/range-min reconciliation - a non-zero min forces presence so -optional set is a contradiction (reject with a clear message; declare -optional 0). Parse enforcement: a new PUNKARGS VALIDATION occurrencecount failure class (payload <argname> count <n> min <m> | max <m>) fires in a single post-loop pass per section (opts/leaders/values); over-max is a hard contradiction (fires in both normal and viability-probe modes, parse_status_classify maps it to invalid), under-min is pure end-of-input exhaustion (SUPPRESSED in the G-152 viability probe and classified incomplete so a viable form reports incomplete, not invalid). The usage-table Multi column reflects the range (0-1 / 2-4 / 1+ for unbounded-with-floor; the greencheck stays for legacy 1), the string renderer emits MULTI:0-1 etc., and the synopsis distinguishes at-most-once (?arg?, no ellipsis) from repeating (arg...). -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged. define -help documents the range form and the -optional/range-min rule. New testsuite multipleranges.test (28 tests: 13 define-time canonicalisation+validation, 12 parse-time enforcement incl parse_status verdicts and legacy-required-still-trailingvaluecount guard, 3 rendering); full punk/args suite 399/0, punk/ns 125/125. #0.22.0 - G-053 range-valued -multiple (occurrence arity): -multiple now accepts a {min max} range (max -1 = unbounded) alongside the legacy booleans 0/1, so a definition can declare at most once ({0 1}, a second occurrence is a parse error), bounded repetition ({2 4}), or one-or-more ({1 -1}) instead of choosing between silent last-wins (0) and unbounded collection (1). The spec compiler canonicalises once at resolve: the stored -multiple becomes the computed boolean (list-shape collect: true for max>1 or -1, false for legacy 0 and max==1) so every existing collect-vs-replace / scalar-vs-list / leader-value-single-multiple hot-path truth-test stays correct, and the range companions (min/max/maxbounded) live in a separate per-form MULTIPLE_RANGES dict (NOT in ARG_INFO, so they do not ride along when ARG_INFO is round-tripped as a spec via resolved_def copyfrom). Legacy 0/1 (and boolean strings true/false/yes/no) are coerced to the boolean and stay byte-unchanged (no MULTIPLE_RANGES entry for unlimited cases). Resolve validation: max must be positive or -1, min <= max, and the -optional/range-min reconciliation - a non-zero min forces presence so -optional set is a contradiction (reject with a clear message; declare -optional 0). Parse enforcement: a new PUNKARGS VALIDATION occurrencecount failure class (payload <argname> count <n> min <m> | max <m>) fires in a single post-loop pass per section (opts/leaders/values); over-max is a hard contradiction (fires in both normal and viability-probe modes, parse_status_classify maps it to invalid), under-min is pure end-of-input exhaustion (SUPPRESSED in the G-152 viability probe and classified incomplete so a viable form reports incomplete, not invalid). The usage-table Multi column reflects the range (0-1 / 2-4 / 1+ for unbounded-with-floor; the greencheck stays for legacy 1), the string renderer emits MULTI:0-1 etc., and the synopsis distinguishes at-most-once (?arg?, no ellipsis) from repeating (arg...). -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged. define -help documents the range form and the -optional/range-min rule. New testsuite multipleranges.test (28 tests: 13 define-time canonicalisation+validation, 12 parse-time enforcement incl parse_status verdicts and legacy-required-still-trailingvaluecount guard, 3 rendering); full punk/args suite 399/0, punk/ns 125/125.
#0.21.0 - G-083 increment 3 (argument-relations usage rendering + lsearch moduledoc adoption): the -conflicts and -parsekeymode error vocabulary now surfaces in usage/arg_error and synopsis output, and the lsearch tclcore moduledoc models its documented option incompatibilities with the new vocabulary. Rendering: (a) a per-arg -conflicts list appends a 'conflicts with: <targets>' hint to the argument's help text in the usage table (targets resolved to display names via lookup_optset, so a parsekey target shows its member flag, not the raw parsekey); (b) a named @opts group marked -parsekeymode error is annotated 'mutually exclusive (distinct members may not be combined)' in its group header, while override (default) groups carry no such annotation; (c) the synopsis one-line form carries no conflict detail (conflicts are a usage-table concern, not a synopsis-line one). Characterized in relations.test (conflicts_usage_hint, parsekeymode_error_usage_group_header, parsekeymode_override_no_header_annotation, conflicts_synopsis_no_hint). lsearch moduledoc: -sorted gains -conflicts {-glob -regexp} and -bisect gains -conflicts {-all -not} (per-arg conflicts, the whole group stays -parsekeymode override so -glob/-regexp remain last-wins); the '(documentation incomplete - punk::args fixes required for grouped mutually exclusive options and prefix calculation)' caveat is dropped from the @cmd -help. Pinned in relations.test (lsearch_sorted_conflicts_glob + lsearch_bisect_conflicts_all raise optionconflict, lsearch_glob_regexp_last_wins stays last-wins, lsearch_caveat_dropped). clock clicks unchanged - its active positional-choice definition already models exclusivity (exactly-one-of) and the acceptance clause names lsearch only. Legacy untouched by default (no -conflicts/parsekeymode-error definition carries no new rendering). Full punk/args suite green; tclcoreparity 10/10. #0.21.0 - G-083 increment 3 (argument-relations usage rendering + lsearch moduledoc adoption): the -conflicts and -parsekeymode error vocabulary now surfaces in usage/arg_error and synopsis output, and the lsearch tclcore moduledoc models its documented option incompatibilities with the new vocabulary. Rendering: (a) a per-arg -conflicts list appends a 'conflicts with: <targets>' hint to the argument's help text in the usage table (targets resolved to display names via lookup_optset, so a parsekey target shows its member flag, not the raw parsekey); (b) a named @opts group marked -parsekeymode error is annotated 'mutually exclusive (distinct members may not be combined)' in its group header, while override (default) groups carry no such annotation; (c) the synopsis one-line form carries no conflict detail (conflicts are a usage-table concern, not a synopsis-line one). Characterized in relations.test (conflicts_usage_hint, parsekeymode_error_usage_group_header, parsekeymode_override_no_header_annotation, conflicts_synopsis_no_hint). lsearch moduledoc: -sorted gains -conflicts {-glob -regexp} and -bisect gains -conflicts {-all -not} (per-arg conflicts, the whole group stays -parsekeymode override so -glob/-regexp remain last-wins); the '(documentation incomplete - punk::args fixes required for grouped mutually exclusive options and prefix calculation)' caveat is dropped from the @cmd -help. Pinned in relations.test (lsearch_sorted_conflicts_glob + lsearch_bisect_conflicts_all raise optionconflict, lsearch_glob_regexp_last_wins stays last-wins, lsearch_caveat_dropped). clock clicks unchanged - its active positional-choice definition already models exclusivity (exactly-one-of) and the acceptance clause names lsearch only. Legacy untouched by default (no -conflicts/parsekeymode-error definition carries no new rendering). Full punk/args suite green; tclcoreparity 10/10.
#0.20.0 - G-083 increment 2 (argument-relations parse-time enforcement): the -conflicts and -parsekeymode error vocabulary declared at define time (0.19.0) is now enforced at parse. A new optionconflict failure class joins the PUNKARGS VALIDATION errorcode vocabulary, mirroring optionmissing's shape: {optionconflict <arg_a> <arg_b> received <receivednames>} - it names both offending received arguments, for both per-arg -conflicts violations (any pair, cross-group) and -parsekeymode error group co-occurrence (distinct members of a shared-parsekey group). The check runs in a single post-resolution site in get_dict_form (after the optionmissing/valuemissing block), so the ordinary option path and the mash (short-flag bundling) path share it - both raise identically. Checked against RECEIVED arguments only (defaults never conflict), after prefix/abbreviation resolution, on optset identity (a new optsets_received tracker for options, since flagsreceived collapses shared-parsekey members onto one api_opt). Runs unconditionally - a received conflict is a hard contradiction in any mode, not end-of-input exhaustion, so the candidacy/viability probe reports it as status invalid (parse_status_classify maps optionconflict to invalid, not incomplete). Define-time check added: a group marked -parsekeymode error must also declare a non-empty -parsekey (the strict mode only applies to a shared-parsekey group). Legacy untouched by default: -parsekeymode override is the default and absent -conflicts means no check runs, so the full existing suite (including the pinned parsekey_repeat_ordering last-wins / prepend-defaults idiom) passes unchanged. relations.test extended with 7 parse-time enforcement pins (raise cases, defaults-never-conflict, cross-group, parsekeymode error raise + one-received-ok + override-legacy, parse_status invalid); full punk/args suite green. #0.20.0 - G-083 increment 2 (argument-relations parse-time enforcement): the -conflicts and -parsekeymode error vocabulary declared at define time (0.19.0) is now enforced at parse. A new optionconflict failure class joins the PUNKARGS VALIDATION errorcode vocabulary, mirroring optionmissing's shape: {optionconflict <arg_a> <arg_b> received <receivednames>} - it names both offending received arguments, for both per-arg -conflicts violations (any pair, cross-group) and -parsekeymode error group co-occurrence (distinct members of a shared-parsekey group). The check runs in a single post-resolution site in get_dict_form (after the optionmissing/valuemissing block), so the ordinary option path and the mash (short-flag bundling) path share it - both raise identically. Checked against RECEIVED arguments only (defaults never conflict), after prefix/abbreviation resolution, on optset identity (a new optsets_received tracker for options, since flagsreceived collapses shared-parsekey members onto one api_opt). Runs unconditionally - a received conflict is a hard contradiction in any mode, not end-of-input exhaustion, so the candidacy/viability probe reports it as status invalid (parse_status_classify maps optionconflict to invalid, not incomplete). Define-time check added: a group marked -parsekeymode error must also declare a non-empty -parsekey (the strict mode only applies to a shared-parsekey group). Legacy untouched by default: -parsekeymode override is the default and absent -conflicts means no check runs, so the full existing suite (including the pinned parsekey_repeat_ordering last-wins / prepend-defaults idiom) passes unchanged. relations.test extended with 7 parse-time enforcement pins (raise cases, defaults-never-conflict, cross-group, parsekeymode error raise + one-received-ok + override-legacy, parse_status invalid); full punk/args suite green.

3
src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm

@ -11028,7 +11028,8 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
#@form -form "setvalues" -synopsis "variable ?name value...? ?name?" #@form -form "setvalues" -synopsis "variable ?name value...? ?name?"
@form -form "setvalues" @form -form "setvalues"
@values -min 0 -max -1 @values -min 0 -max -1
#todo - fix -parsekey for leaders and values #G-084 (2026-08-08): value -parsekey now supported (name value pairs
#collect under the parsekey name_value). Leaders -parsekey remains pending.
"name value" -parsekey name_value -type {string any} -optional 1 -multiple 1 "name value" -parsekey name_value -type {string any} -optional 1 -multiple 1
name -type string -optional 1 -multiple 0 name -type string -optional 1 -multiple 0

45
src/tests/modules/punk/args/testsuites/args/parsekey.test

@ -293,7 +293,7 @@ namespace eval ::testspace {
#GAP: behaviour unsettled in source (args tm ordered_opts default handling carries a '? review'). #GAP: behaviour unsettled in source (args tm ordered_opts default handling carries a '? review').
#Current behaviour: when more than one member of a shared-parsekey group declares -default, #Current behaviour: when more than one member of a shared-parsekey group declares -default,
#the default of the LAST-DEFINED defaulted member is returned under the shared key. #the default of the LAST-DEFINED defaulted member is returned under the shared key.
test parsekey_shared_key_member_defaults_GAP {GAP: last-defined member default wins when multiple members of a shared-parsekey group have -default}\ test parsekey_shared_key_member_defaults {settled (G-084): last-defined member default wins when multiple members of a shared-parsekey group have -default and none is received}\
-setup $common -body { -setup $common -body {
set docids [list] set docids [list]
@ -403,34 +403,37 @@ namespace eval ::testspace {
# - the @values directive line rejects -parsekey outright (unrecognised key) # - the @values directive line rejects -parsekey outright (unrecognised key)
# - per-arg -parsekey on a leader is silently ignored (result keyed by the leader name) # - per-arg -parsekey on a leader is silently ignored (result keyed by the leader name)
#Desired rename semantics pinned (disabled) in dev/parsekey-knownbugs.test. #Desired rename semantics pinned (disabled) in dev/parsekey-knownbugs.test.
test parsekey_values_leaders_GAP {GAP: -parsekey on a value aborts parse, on a leader is ignored, on @values line is rejected}\ test parsekey_value_result_key_settled {settled (G-084): -parsekey on a value renames the values result key and the received key (no longer aborts parse)}\
-setup $common -body {
set argd [punk::args::parse {hello} withdef @values {v1 -parsekey renamed -type string}]
lappend result [dict get $argd values]
lappend result [dict exists [dict get $argd received] renamed]
lappend result [dict exists [dict get $argd received] v1]
}\
-cleanup {
punk::args::undefine [dict get $argd id] 1
}\
-result [list\
{renamed hello} 1 0\
]
test parsekey_values_line_rejects_parsekey {settled (G-084): @values directive line rejects -parsekey (group-default parsekey is not a feature)}\
-setup $common -body { -setup $common -body {
set docid ::testspace::parsekey_values_leaders_GAP
set docids [list $docid]
punk::args::define [list @id -id $docid] @values {v1 -parsekey renamed -type string}
set err [catch {punk::args::parse {hello} withid $docid} msg]
lappend result $err
set err [catch {punk::args::parse {hello} withdef {@values -parsekey renamed} {v1 -type string}} msg] set err [catch {punk::args::parse {hello} withdef {@values -parsekey renamed} {v1 -type string}} msg]
lappend result $err lappend result $err
lappend result [string match "*unrecognised key '-parsekey' in @values line*" $msg] lappend result [string match "*unrecognised key '-parsekey' in @values line*" $msg]
set argd [punk::args::parse {hello there} withdef {@leaders -min 1 -max 1} {l1 -parsekey renamed -type string} @values {v1 -type string}]
lappend docids [dict get $argd id]
lappend result [dict get $argd leaders]
lappend result [dict get $argd received]
}\ }\
-cleanup { -result [list\
foreach id $docids { 1 1\
punk::args::undefine $id 1 ]
}
test parsekey_leader_parsekey_rejected {settled (G-084): -parsekey on a leader is rejected at define time (no silent ignore, no parse-time abort)}\
-setup $common -body {
set err [catch {punk::args::parse {hello there} withdef {@leaders -min 1 -max 1} {l1 -parsekey renamed -type string} @values {v1 -type string}} msg]
lappend result $err
lappend result [string match "*-parsekey is not supported on a leader argument*" $msg]
}\ }\
-result [list\ -result [list\
1\
1 1\ 1 1\
{l1 hello}\
{l1 0 v1 1}
] ]
} }
tcltest::cleanupTests ;#needed to produce test summary line. tcltest::cleanupTests ;#needed to produce test summary line.

48
src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test

@ -4,6 +4,13 @@ package require tcltest
#Desired-behaviour pins for known parsekey gaps. Each has a corresponding _GAP test in #Desired-behaviour pins for known parsekey gaps. Each has a corresponding _GAP test in
#testsuites/args/parsekey.test pinning today's behaviour; when a fix lands, enable the #testsuites/args/parsekey.test pinning today's behaviour; when a fix lands, enable the
#test here and flip/remove the GAP pin there. #test here and flip/remove the GAP pin there.
#G-084 (2026-08-08): value -parsekey support landed - the parsekey_value_result_key
#pin moved to testsuites/args/parsekey.test as parsekey_value_result_key_settled
#(now an enabled assertion). The leader case is settled as a define-time rejection
#(parsekey_leader_parsekey_rejected in parsekey.test) - no live caller, so -parsekey
#on a leader is rejected rather than silently ignored. The @values directive line
#still rejects -parsekey (parsekey_values_line_rejects_parsekey). The remaining open
#pin here is the cross-member -multiple accumulation design decision.
namespace eval ::testspace { namespace eval ::testspace {
namespace import ::tcltest::* namespace import ::tcltest::*
tcltest::testConstraint punkargsKnownBug 0 tcltest::testConstraint punkargsKnownBug 0
@ -12,45 +19,6 @@ namespace eval ::testspace {
set result "" set result ""
} }
test parsekey_value_result_key {Known bug: -parsekey on a value should rename the values result key (currently aborts parse)}\
-constraints punkargsKnownBug\
-setup $common -body {
set docids [list]
set argd [punk::args::parse {hello} withdef @values {v1 -parsekey renamed -type string}]
lappend docids [dict get $argd id]
lappend result [dict get $argd values]
lappend result [dict exists [dict get $argd received] renamed]
lappend result [dict exists [dict get $argd received] v1]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list\
{renamed hello} 1 0
]
test parsekey_leader_result_key {Known bug: -parsekey on a leader should rename the leaders result key (currently ignored)}\
-constraints punkargsKnownBug\
-setup $common -body {
set docids [list]
set argd [punk::args::parse {hello there} withdef {@leaders -min 1 -max 1} {l1 -parsekey renamed -type string} @values {v1 -type string}]
lappend docids [dict get $argd id]
lappend result [dict get $argd leaders]
lappend result [dict exists [dict get $argd received] renamed]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list\
{renamed hello} 1
]
#Design-pending rather than settled: the lean is that -multiple 1 members sharing a #Design-pending rather than settled: the lean is that -multiple 1 members sharing a
#-parsekey accumulate under the shared key in received order (today only one member's #-parsekey accumulate under the shared key in received order (today only one member's
#collected list survives - see parsekey_shared_key_multiple_collection_GAP). #collected list survives - see parsekey_shared_key_multiple_collection_GAP).
@ -63,7 +31,7 @@ namespace eval ::testspace {
lappend docids [dict get $argd id] lappend docids [dict get $argd id]
lappend result [dict get $argd opts] lappend result [dict get $argd opts]
set argd [punk::args::parse {-add a -sub b -add c} withdef {@opts -type string -parsekey -op -multiple 1} -add -sub] set argd [punk::args::parse {-add a -sub b -add c} withdef @opts {-add -type string -parsekey -op -multiple 1} {-sub -type string -parsekey -op -multiple 1}]
lappend docids [dict get $argd id] lappend docids [dict get $argd id]
lappend result [dict get $argd opts] lappend result [dict get $argd opts]
}\ }\

Loading…
Cancel
Save