Browse Source

argparsingtest 1.2.0: parse_args/argparse timing wrappers, plus a sandwich calling style

compare's tables listed parse_args and argparse under "not loaded:" on the one runtime
carrying both - bin/runtime/win32-ix86/tclsh8.6.10-luck-test2.exe (argparse 0.61,
parse_args 0.5.1). Neither was missing. Both sat in optional_externals, the probe-only
list whose entries can never be better than not_wrappable, and compare bucketed
not_wrappable into the same footer line as not_loaded - so a package that was present and
working read as absent.

Timing wrappers now exist for both, and they move out of optional_externals into the
style rosters under a new {optional <pkg>} marker that generalises the former hardcoded
argp case: loaded when the package was present at module load, not_loaded otherwise.
getopt and cmdr stay probe-only. compare now reports not_wrappable separately - a
"no timing wrapper:" table line, and a not_wrappable key in the dict and json forms - so
the two conditions cannot be conflated again.

Availability is per-runtime and the two do not travel together (surveyed 2026-07-26): the
32-bit luck-test2 kit has both, the native Tcl 9.0.3 install has argparse only, punk902z
and punk91 have parse_args only, punk905 has neither. The tests constrain them separately
for that reason.

Style support was measured rather than assumed. argparse reaches tk style only via -mixed
(its default mode rejects leading positionals as excess arguments) and its switches need
an explicit -argument in the element spec or their values fall through as positionals.
parse_args cannot consume leading positionals at all, so tkstyle parse_args is
unsupported. Both wrappers call their library by fully qualified name: the procs
deliberately share the package name, so an unqualified call would resolve to the wrapper
itself - infinite recursion in argparse's case.

The fourth calling style, sandwich, is 2 mandatory leading positionals, THEN the 10 keyed
options, THEN 3 mandatory trailing positionals. The 2/3 split is asymmetric on purpose -
an even split could be satisfied by a parser that merely halves the positional block. All
three candidates support a valueless flag, so the argvec exercises -join as a solo flag
alongside the -flag value pairs. Wrappers: manual_switch, punkargs, punkargs_by_id,
punkargs_parsecache, opt and argparse, returning {{p1 p2} {p3 p4 p5} optsdict} - grouped
rather than the flat {p1 p2 opts} of the other positional styles, because positionals now
sit on both sides of the options.

punk::args and opt model that arrangement - @leaders/@opts/@values are three separate
sections, and tcl::OptProc fills positional slots in declaration order - and both reject
options placed ahead of the leaders, as does the hand-rolled baseline. argparse reaches
the style only through -mixed, which permits switches ANYWHERE, so it parses the argvec
correctly without being able to enforce it: the identical spec also accepts all five
positionals trailing. It is timed for the parse, not credited with expressing the
constraint, and both halves of that are pinned by tests (sandwich_strictness,
sandwich_argparse_is_permissive) rather than left as a silent difference. Its flag
convention differs too, opts being reported in each parser's natural form: a supplied
bare flag yields an empty-string value and an omitted one is absent from the dict, where
the others report -join as 1 or 0.

cmdline, tepam, argp and parse_args are unsupported N/A rows in this style, each verified
to fail on its argvec. cmdline is the one worth naming: cmdline::getoptions returns
WITHOUT error having parsed nothing at all - every option left at its default and the
whole argument list untouched - so a check that only looks for a raised error would
wrongly credit it.

Verification: 25/25 pass under the 32-bit runtime with no skips, which is the only
configuration that exercises every new test; 23 pass + 2 skip under native Tcl 9.0.3,
where parse_args is absent. first_call times the new parsers in fresh child interps,
confirming these binary extensions load a second time in-process.

Built artifacts are not in this commit - modules/ is git-ignored. modules/
argparsingtest-1.2.0.tm was deployed via 'make.tcl modules' so the dev shell can see it:
a plain 'package require argparsingtest' there resolves a deployed stable version in
preference to the 999999.0a1.0 source module, so source edits are invisible until built.

Claude-Session: https://claude.ai/code/session_01Q82VuBXzgeKsAwMYHrbpP1
Assisted-by: harness=claude; primary-model=claude-opus-5[1m]; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
4bd0ab1354
  1. 773
      src/modules/argparsingtest-999999.0a1.0.tm
  2. 9
      src/modules/argparsingtest-buildversion.txt
  3. 152
      src/tests/modules/argparsingtest/testsuites/argparsingtest/argparsingtest.test
  4. 14
      src/tests/modules/argparsingtest/testsuites/argparsingtest/argparsingtest_compare.test

773
src/modules/argparsingtest-999999.0a1.0.tm

@ -123,16 +123,25 @@ namespace eval argparsingtest {
# title - table section heading used by compare # title - table section heading used by compare
# argvec - default timing argument vector for the style # argvec - default timing argument vector for the style
# roster - parser proc name -> marker, where marker is one of: # roster - parser proc name -> marker, where marker is one of:
# always - proc unconditionally defined in argparsingtest::<style> # always - proc unconditionally defined in argparsingtest::<style>
# argp - proc defined unconditionally but callable only when the # {optional P} - proc defined unconditionally but callable only when the
# optional argp package loaded at module require time # optional package P loaded at module require time
# unsupported - the backing library cannot express this calling style # (see the optional-package requires below); reported as
# (no proc exists); reported as unsupported and shown as # loaded when P is present, not_loaded otherwise
# an N/A row by compare rather than being omitted # unsupported - the backing library cannot express this calling style
# (no proc exists); reported as unsupported and shown as
# an N/A row by compare rather than being omitted
# Styles: # Styles:
# opts - keyed options only # opts - keyed options only
# tkstyle - 2 mandatory positionals THEN options ('tk style', as in: .c create rect 1 2 3 4 -fill red) # tkstyle - 2 mandatory positionals THEN options ('tk style', as in: .c create rect 1 2 3 4 -fill red)
# tclstyle - options THEN 2 mandatory positionals ('tcl style', as in: lsearch -exact $list $val) # tclstyle - options THEN 2 mandatory positionals ('tcl style', as in: lsearch -exact $list $val)
# sandwich - 2 mandatory positionals, THEN options, THEN 3 more mandatory positionals.
# The asymmetric 2/3 split is deliberate: it catches a parser that
# merely splits the positional block evenly or matches it by count
# from one end. This is the STRICT sandwich - a parser is only
# credited with the style if it binds the leaders and the trailing
# values in that arrangement; see the sandwich namespace comment
# for what that does and does not say about permissive parsers.
# The manual_* roster membership differs deliberately: all manual option-loop # The manual_* roster membership differs deliberately: all manual option-loop
# variants are compared in the opts style; the positional styles carry a single # variants are compared in the opts style; the positional styles carry a single
# manual_switch baseline since the option-loop technique comparison is orthogonal # manual_switch baseline since the option-loop technique comparison is orthogonal
@ -157,7 +166,9 @@ namespace eval argparsingtest {
opt always opt always
cmdline_untyped always cmdline_untyped always
cmdline_typed always cmdline_typed always
argp argp argp {optional argp}
parse_args {optional parse_args}
argparse {optional argparse}
tepam always tepam always
} }
} }
@ -171,9 +182,11 @@ namespace eval argparsingtest {
punkargs_parsecache always punkargs_parsecache always
opt always opt always
tepam always tepam always
argparse {optional argparse}
cmdline_untyped unsupported cmdline_untyped unsupported
cmdline_typed unsupported cmdline_typed unsupported
argp unsupported argp unsupported
parse_args unsupported
} }
} }
tclstyle { tclstyle {
@ -188,18 +201,51 @@ namespace eval argparsingtest {
cmdline_untyped always cmdline_untyped always
cmdline_typed always cmdline_typed always
tepam always tepam always
parse_args {optional parse_args}
argparse {optional argparse}
argp unsupported argp unsupported
} }
} }
sandwich {
title "sandwich style (2 leaders, options, 3 trailing)"
argvec {alpha beta -return object -join -x hello -y world -1 1 -2 2 -3 3 gamma delta epsilon}
roster {
manual_switch always
punkargs always
punkargs_by_id always
punkargs_parsecache always
opt always
argparse {optional argparse}
cmdline_untyped unsupported
cmdline_typed unsupported
tepam unsupported
argp unsupported
parse_args unsupported
}
}
}] }]
# Optional external parser packages that DO have timing wrappers (see the
# {optional P} roster markers above). Required once here, guarded by catch,
# so discover_parsers can probe them with side-effect-free 'package present'
# - matching how the opts::argp registration site requires argp.
# Both are binary extensions and neither ships with a stock tclsh; which of
# them is present varies per runtime. Surveyed 2026-07-26:
# both bin/runtime/win32-ix86/tclsh8.6.10-luck-test2.exe
# (argparse 0.61, parse_args 0.5.1)
# argparse only the native Tcl 9.0.3 install (argparse0.61 in its lib dir)
# parse_args only punk902z and punk91 kits (lib_tcl9/parse_args0.5.1)
# neither punk905
# On runtimes without them the wrapper procs stay defined but discover_parsers
# reports not_loaded, so the harness never calls them.
catch {package require parse_args}
catch {package require argparse}
# Optional external parser packages probed by discover_parsers (parser name -> package name). # Optional external parser packages probed by discover_parsers (parser name -> package name).
# No timing wrappers are shipped for these: present -> not_wrappable, absent -> not_loaded. # No timing wrappers are shipped for these: present -> not_wrappable, absent -> not_loaded.
variable optional_externals variable optional_externals
set optional_externals { set optional_externals {
getopt getopt getopt getopt
parse_args parse_args
argparse argparse
cmdr cmdr cmdr cmdr
} }
@ -208,7 +254,7 @@ namespace eval argparsingtest {
@cmd -name argparsingtest::discover_parsers\ @cmd -name argparsingtest::discover_parsers\
-summary "Discover available argument parsers per parsing style"\ -summary "Discover available argument parsers per parsing style"\
-help\ -help\
"Returns a dict keyed by style (opts tkstyle tclstyle), each value "Returns a dict keyed by style (opts tkstyle tclstyle sandwich), each value
a dict of parser proc names to their status for that style. a dict of parser proc names to their status for that style.
Statuses: Statuses:
@ -218,7 +264,7 @@ namespace eval argparsingtest {
unsupported - backing library cannot express this parsing style unsupported - backing library cannot express this parsing style
(shown as an N/A row in compare tables)" (shown as an N/A row in compare tables)"
@opts -anyopts 0 @opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\ -style -default all -type string -choices {all opts tkstyle tclstyle sandwich} -help\
"Style set(s) to report. Default all reports every style. "Style set(s) to report. Default all reports every style.
The result is keyed by style in all cases." The result is keyed by style in all cases."
} }
@ -237,15 +283,16 @@ namespace eval argparsingtest {
foreach s $stylelist { foreach s $stylelist {
set sresult {} set sresult {}
foreach {pname marker} [dict get $style_info $s roster] { foreach {pname marker} [dict get $style_info $s roster] {
switch -- $marker { switch -- [lindex $marker 0] {
always { always {
dict set sresult $pname loaded dict set sresult $pname loaded
} }
argp { optional {
# argp: proc defined unconditionally but registration guarded by catch at # {optional P}: proc defined unconditionally but its backing package
# module load; use package present to check whether the load-time require # require (argp's registration, or the module-level requires for
# succeeded (no side effects) # parse_args/argparse) is guarded by catch at module load; use package
if {[catch {package present argp}]} { # present to check whether that require succeeded (no side effects)
if {[catch {package present [lindex $marker 1]}]} {
dict set sresult $pname not_loaded dict set sresult $pname not_loaded
} else { } else {
dict set sresult $pname loaded dict set sresult $pname loaded
@ -284,7 +331,7 @@ namespace eval argparsingtest {
warmup - warmup count used warmup - warmup count used
Parsers that error during timing return {error <message>} instead." Parsers that error during timing return {error <message>} instead."
@opts -anyopts 0 @opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\ -style -default all -type string -choices {all opts tkstyle tclstyle sandwich} -help\
"Style set(s) to time. The result is keyed by style in all cases." "Style set(s) to time. The result is keyed by style in all cases."
-iterations -default 1000 -type integer -help\ -iterations -default 1000 -type integer -help\
"Number of timed invocations per parser." "Number of timed invocations per parser."
@ -314,7 +361,7 @@ namespace eval argparsingtest {
set stylelist [list $style] set stylelist [list $style]
} }
if {[llength $argvec] && [llength $stylelist] > 1} { if {[llength $argvec] && [llength $stylelist] > 1} {
error "timeit: -args requires a single -style (opts, tkstyle or tclstyle)" error "timeit: -args requires a single -style (one of: opts tkstyle tclstyle sandwich)"
} }
set result {} set result {}
@ -490,6 +537,33 @@ namespace eval argparsingtest {
argparsingtest::libwarmup_argp -w 1 argparsingtest::libwarmup_argp -w 1
} }
# parse_args and argparse warm-ups exercise option, flag and positional
# handling through a sacrificial proc, so the library internals are compiled
# before the timed call. Both are single warm-ups covering every style: the
# library_warmups dict is keyed by parser name only, and the parse_args /
# argparse wrappers share their name across the styles they support.
dict set library_warmups parse_args {
proc ::argparsingtest::libwarmup_parse_args {args} {
::parse_args::parse_args $args {
-w {-default 0}
-wflag {-boolean}
wpos {-required}
} res
return $res
}
::argparsingtest::libwarmup_parse_args -w 1 -wflag pval
}
dict set library_warmups argparse {
proc ::argparsingtest::libwarmup_argparse {args} {
return [::argparse -inline -mixed {
{-w -argument -default 0}
{-wflag}
{wpos}
} $args]
}
::argparsingtest::libwarmup_argparse -w 1 -wflag pval
}
punk::args::define { punk::args::define {
@id -id ::argparsingtest::first_call @id -id ::argparsingtest::first_call
@cmd -name argparsingtest::first_call\ @cmd -name argparsingtest::first_call\
@ -522,7 +596,7 @@ namespace eval argparsingtest {
first-call microseconds (float), or {error <message>} if the run first-call microseconds (float), or {error <message>} if the run
fails in the child interp." fails in the child interp."
@opts -anyopts 0 @opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\ -style -default all -type string -choices {all opts tkstyle tclstyle sandwich} -help\
"Style set(s) to time. The result is keyed by style in all cases." "Style set(s) to time. The result is keyed by style in all cases."
-parsers -default {} -type list -help\ -parsers -default {} -type list -help\
"List of parser proc names to time. Empty (default) times "List of parser proc names to time. Empty (default) times
@ -642,14 +716,18 @@ namespace eval argparsingtest {
backing library cannot express a style are still listed for that backing library cannot express a style are still listed for that
style, as N/A rows below the timed rows. style, as N/A rows below the timed rows.
- table: per-style human-readable matrix - table: per-style human-readable matrix
(first_cold/first_warm columns are first_call timings) (first_cold/first_warm columns are first_call timings).
Parsers whose backing package is absent are listed on a
'not loaded:' line; optional packages that are present but
have no timing wrapper get a separate 'no timing wrapper:' line
- dict: {results {style {...}} first_call {style {...}} - dict: {results {style {...}} first_call {style {...}}
not_loaded {style {...}} unsupported {style {...}}} not_loaded {style {...}} not_wrappable {style {...}}
unsupported {style {...}}}
first_call values are {cold <us> warm <us>} sub-dicts (see first_call) first_call values are {cold <us> warm <us>} sub-dicts (see first_call)
- json: JSON object with results, first_call, not_loaded and - json: JSON object with results, first_call, not_loaded,
unsupported keys, each keyed by style" not_wrappable and unsupported keys, each keyed by style"
@opts -anyopts 0 @opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\ -style -default all -type string -choices {all opts tkstyle tclstyle sandwich} -help\
"Style set(s) to compare: all (default) or a single style." "Style set(s) to compare: all (default) or a single style."
-format -default table -type string -choices {table dict json} -help\ -format -default table -type string -choices {table dict json} -help\
"Output format: table (default), dict, or json." "Output format: table (default), dict, or json."
@ -682,24 +760,28 @@ namespace eval argparsingtest {
set stylelist [list $style] set stylelist [list $style]
} }
if {[llength $argvec] && [llength $stylelist] > 1} { if {[llength $argvec] && [llength $stylelist] > 1} {
error "compare: -args requires a single -style (opts, tkstyle or tclstyle)" error "compare: -args requires a single -style (one of: opts tkstyle tclstyle sandwich)"
} }
set discovered [discover_parsers] set discovered [discover_parsers]
# Per style: partition the roster (restricted to -parsers when given) into # Per style: partition the roster (restricted to -parsers when given) into
# timing candidates (loaded), N/A rows (unsupported) and the not-loaded # timing candidates (loaded), N/A rows (unsupported) and the two footer
# footer list. Requested names unknown to the roster are passed through to # lists - packages absent from this runtime (not_loaded) and packages
# timeit so they surface as error entries rather than vanishing silently. # present but shipped without a timing wrapper (not_wrappable). Requested
set timeit_dict {} ;# {style {pname timing}} # names unknown to the roster are passed through to timeit so they surface
set first_call_dict {} ;# {style {pname {cold <us> warm <us>}}} # as error entries rather than vanishing silently.
set na_dict {} ;# {style {pname ...}} - unsupported for the style set timeit_dict {} ;# {style {pname timing}}
set not_loaded_dict {} ;# {style {pname ...}} - not_loaded or not_wrappable set first_call_dict {} ;# {style {pname {cold <us> warm <us>}}}
set na_dict {} ;# {style {pname ...}} - unsupported for the style
set not_loaded_dict {} ;# {style {pname ...}} - backing package absent
set not_wrappable_dict {} ;# {style {pname ...}} - package present, no wrapper shipped
foreach s $stylelist { foreach s $stylelist {
set sdisco [dict get $discovered $s] set sdisco [dict get $discovered $s]
set candidates {} set candidates {}
set na {} set na {}
set notloaded {} set notloaded {}
set notwrappable {}
if {[llength $parsers]} { if {[llength $parsers]} {
set requested $parsers set requested $parsers
} else { } else {
@ -711,13 +793,15 @@ namespace eval argparsingtest {
continue continue
} }
switch -- [dict get $sdisco $pname] { switch -- [dict get $sdisco $pname] {
loaded { lappend candidates $pname } loaded { lappend candidates $pname }
unsupported { lappend na $pname } unsupported { lappend na $pname }
default { lappend notloaded $pname } not_wrappable { lappend notwrappable $pname }
default { lappend notloaded $pname }
} }
} }
dict set na_dict $s $na dict set na_dict $s $na
dict set not_loaded_dict $s $notloaded dict set not_loaded_dict $s $notloaded
dict set not_wrappable_dict $s $notwrappable
if {[llength $candidates]} { if {[llength $candidates]} {
set timeit_args [list -style $s -iterations $iterations -warmup $warmup -parsers $candidates] set timeit_args [list -style $s -iterations $iterations -warmup $warmup -parsers $candidates]
@ -740,6 +824,7 @@ namespace eval argparsingtest {
set sfc [dict get $first_call_dict $s] set sfc [dict get $first_call_dict $s]
set sna [dict get $na_dict $s] set sna [dict get $na_dict $s]
set snotloaded [dict get $not_loaded_dict $s] set snotloaded [dict get $not_loaded_dict $s]
set snotwrap [dict get $not_wrappable_dict $s]
set colwidth 6 set colwidth 6
foreach nm [concat [dict keys $stimeit] $sna] { foreach nm [concat [dict keys $stimeit] $sna] {
@ -787,6 +872,9 @@ namespace eval argparsingtest {
} else { } else {
lappend lines "not loaded: (none)" lappend lines "not loaded: (none)"
} }
if {[llength $snotwrap] > 0} {
lappend lines "no timing wrapper: [join $snotwrap {, }]"
}
if {[llength $error_names] > 0} { if {[llength $error_names] > 0} {
lappend lines "errors: [join $error_names {, }]" lappend lines "errors: [join $error_names {, }]"
} }
@ -795,13 +883,14 @@ namespace eval argparsingtest {
return [join $style_blocks \n\n] return [join $style_blocks \n\n]
} }
dict { dict {
return [dict create results $timeit_dict first_call $first_call_dict not_loaded $not_loaded_dict unsupported $na_dict] return [dict create results $timeit_dict first_call $first_call_dict not_loaded $not_loaded_dict not_wrappable $not_wrappable_dict unsupported $na_dict]
} }
json { json {
package require json::write package require json::write
set results_pairs {} set results_pairs {}
set fc_pairs {} set fc_pairs {}
set nl_pairs {} set nl_pairs {}
set nw_pairs {}
set na_pairs {} set na_pairs {}
foreach s $stylelist { foreach s $stylelist {
set timing_jsons [dict map {k v} [dict get $timeit_dict $s] { set timing_jsons [dict map {k v} [dict get $timeit_dict $s] {
@ -835,9 +924,10 @@ namespace eval argparsingtest {
lappend fc_pairs $s [json::write object {*}$fc_style_pairs] lappend fc_pairs $s [json::write object {*}$fc_style_pairs]
lappend nl_pairs $s [json::write array-strings {*}[dict get $not_loaded_dict $s]] lappend nl_pairs $s [json::write array-strings {*}[dict get $not_loaded_dict $s]]
lappend nw_pairs $s [json::write array-strings {*}[dict get $not_wrappable_dict $s]]
lappend na_pairs $s [json::write array-strings {*}[dict get $na_dict $s]] lappend na_pairs $s [json::write array-strings {*}[dict get $na_dict $s]]
} }
return [json::write object results [json::write object {*}$results_pairs] first_call [json::write object {*}$fc_pairs] not_loaded [json::write object {*}$nl_pairs] unsupported [json::write object {*}$na_pairs]] return [json::write object results [json::write object {*}$results_pairs] first_call [json::write object {*}$fc_pairs] not_loaded [json::write object {*}$nl_pairs] not_wrappable [json::write object {*}$nw_pairs] unsupported [json::write object {*}$na_pairs]]
} }
} }
} }
@ -1545,6 +1635,89 @@ namespace eval argparsingtest::opts {
return [array get opts] return [array get opts]
} }
# parse_args and argparse (both binary extensions - see the optional-package
# requires in the argparsingtest base namespace) are called by their fully
# qualified names throughout: the wrapper procs deliberately share the
# package/command name, so an unqualified call would resolve to the wrapper
# itself (infinite recursion for argparse) rather than to the library.
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::opts::parse_args
@cmd -name argparsingtest::opts::parse_args -summary "parse_args package arg parsing" -help\
"Accepts 10 keyed options plus -join. Uses parse_args::parse_args
with a result-dict variable rather than its set-locals default, so
the return shape matches the other opts-style wrappers. Result keys
are the option names with the leading dash stripped."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
}]
}
proc parse_args {args} {
::parse_args::parse_args $args {
-return {-default string}
-frametype {-default \uFFEF}
-show_edge {-default \uFFEF}
-show_seps {-default \uFFEF}
-join {-boolean}
-x {-default ""}
-y {-default b}
-z {-default c}
-1 {-default 1}
-2 {-default 2}
-3 {-default 3}
} opts
return $opts
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::opts::argparse
@cmd -name argparsingtest::opts::argparse -summary "argparse package arg parsing" -help\
"Accepts 10 keyed options plus -join. Uses argparse -inline, which
returns a dict rather than setting caller locals, so the return
shape matches the other opts-style wrappers. Switches taking a
value need an explicit -argument in the argparse element spec;
without it they are flags and the values become excess positionals."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
}]
}
proc argparse {args} {
return [::argparse -inline {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
} $args]
}
namespace eval argdoc { namespace eval argdoc {
lappend PUNKARGS [list { lappend PUNKARGS [list {
@id -id ::argparsingtest::opts::tepam @id -id ::argparsingtest::opts::tepam
@ -1597,6 +1770,10 @@ namespace eval argparsingtest::opts {
# argp has no wrapper in any positional style: argp::parseArgs walks the entire # argp has no wrapper in any positional style: argp::parseArgs walks the entire
# argument list as {key value} pairs, so any positional word errors as an # argument list as {key value} pairs, so any positional word errors as an
# unknown option (unsupported -> N/A row). # unknown option (unsupported -> N/A row).
# parse_args has no wrapper here either: undashed spec entries are only matched
# after the options, so a leading positional aborts the parse with its generic
# 'Invalid args, should be ?-option ...? ?arg ...?' usage error (unsupported ->
# N/A row). argparse IS supported, but only in -mixed mode - see its wrapper.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval argparsingtest::tkstyle { namespace eval argparsingtest::tkstyle {
namespace export {[a-z]*} ;# Convention: export all lowercase namespace export {[a-z]*} ;# Convention: export all lowercase
@ -1859,6 +2036,51 @@ namespace eval argparsingtest::tkstyle {
return [list $p1 $p2 [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]] return [list $p1 $p2 [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]]
} }
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::tkstyle::argparse
@cmd -name argparsingtest::tkstyle::argparse -summary "argparse package arg parsing, tk style" -help\
"Accepts 2 leading positional arguments then 10 keyed options plus -join.
Uses argparse -inline -mixed: argparse's default mode requires switches
to precede positionals and rejects leading positionals as excess
arguments, so -mixed (switches permitted anywhere) is what makes this
style expressible. Returns a list {p1 p2 optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
}]
}
proc argparse {args} {
set parsed [::argparse -inline -mixed {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
{p1}
{p2}
} $args]
return [list [dict get $parsed p1] [dict get $parsed p2] [dict remove $parsed p1 p2]]
}
#*** !doctools #*** !doctools
#[list_end] [comment {--- end definitions namespace argparsingtest::tkstyle ---}] #[list_end] [comment {--- end definitions namespace argparsingtest::tkstyle ---}]
} }
@ -1872,6 +2094,8 @@ namespace eval argparsingtest::tkstyle {
# options in the parser's natural form. # options in the parser's natural form.
# cmdline IS supported here: cmdline::getoptions consumes leading options and # cmdline IS supported here: cmdline::getoptions consumes leading options and
# leaves the trailing positionals in the caller's args variable. # leaves the trailing positionals in the caller's args variable.
# parse_args and argparse are both supported in this style - options-then-
# positionals is the arrangement each of them handles natively.
# argp has no wrapper (see the tkstyle namespace comment; unsupported -> N/A row). # argp has no wrapper (see the tkstyle namespace comment; unsupported -> N/A row).
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval argparsingtest::tclstyle { namespace eval argparsingtest::tclstyle {
@ -2235,12 +2459,485 @@ namespace eval argparsingtest::tclstyle {
return [list $p1 $p2 [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]] return [list $p1 $p2 [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]]
} }
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::tclstyle::parse_args
@cmd -name argparsingtest::tclstyle::parse_args -summary "parse_args package arg parsing, tcl style" -help\
"Accepts 10 keyed options plus -join, then 2 trailing positional arguments.
Positional arguments are declared by undashed name in the parse_args
spec and are only accepted after the options; -required makes both
mandatory. Returns a list {p1 p2 optsdict}."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
}]
}
proc parse_args {args} {
::parse_args::parse_args $args {
-return {-default string}
-frametype {-default \uFFEF}
-show_edge {-default \uFFEF}
-show_seps {-default \uFFEF}
-join {-boolean}
-x {-default ""}
-y {-default b}
-z {-default c}
-1 {-default 1}
-2 {-default 2}
-3 {-default 3}
p1 {-required}
p2 {-required}
} parsed
return [list [dict get $parsed p1] [dict get $parsed p2] [dict remove $parsed p1 p2]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::tclstyle::argparse
@cmd -name argparsingtest::tclstyle::argparse -summary "argparse package arg parsing, tcl style" -help\
"Accepts 10 keyed options plus -join, then 2 trailing positional arguments.
This is argparse's default arrangement - switches first, then
positionals - so no -mixed is needed here (contrast the tkstyle
wrapper). Returns a list {p1 p2 optsdict}."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
}]
}
proc argparse {args} {
set parsed [::argparse -inline {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
{p1}
{p2}
} $args]
return [list [dict get $parsed p1] [dict get $parsed p2] [dict remove $parsed p1 p2]]
}
#*** !doctools #*** !doctools
#[list_end] [comment {--- end definitions namespace argparsingtest::tclstyle ---}] #[list_end] [comment {--- end definitions namespace argparsingtest::tclstyle ---}]
} }
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Parser implementations - sandwich style: 2 mandatory positionals, THEN the 10
# keyed options (the argvec exercises the valueless -join flag as well as
# -flag value pairs), THEN 3 more mandatory positionals.
# All procs return a 3-element list {leaders values opts} where leaders is
# {p1 p2}, values is {p3 p4 p5} and opts is the parsed options in the parser's
# natural form. The other positional styles return a flat {p1 p2 opts}; with
# positionals on BOTH sides of the options, grouping them keeps which-end-is-which
# unambiguous for the parser_contract test.
#
# Only three libraries have wrappers here. Verified 2026-07-26 against the
# packages available on bin/runtime/win32-ix86/tclsh8.6.10-luck-test2.exe:
# - punk::args models the arrangement directly - @leaders, @opts and @values are
# three separate sections - and rejects any other ordering.
# - opt fills positional slots in declaration order, with the flags declared
# between the leader slots and the trailing slots; it likewise rejects the
# options appearing ahead of the leaders.
# - argparse needs -mixed, which permits switches ANYWHERE. It parses this
# argvec correctly, but unlike the two above it does NOT enforce the
# arrangement: the identical spec also accepts all five positionals trailing,
# or positionals interleaved with options. It is timed here for parsing the
# style, which is not a claim that it can express the constraint.
# Its flag convention also differs, as opts is reported in each parser's
# natural form: a supplied bare flag yields an empty-string value and an
# omitted one is absent from the dict entirely, where the punk::args, opt and
# manual wrappers report -join as 1 or 0.
# Unsupported (N/A rows), each verified to fail on this argvec:
# - cmdline: cmdline::getoptions stops at the first non-option word, so it
# returns WITHOUT error having parsed nothing at all - every option left at its
# default and the whole argument list untouched. A wrapper would have to
# hand-roll the option scan itself, so this is unsupported rather than wrapped;
# the silent no-op is the reason it cannot be credited by a smoke test alone.
# - tepam: unnamed arguments form one contiguous block, either before or after
# the named ones (-named_arguments_first 0/1); it cannot split them across the
# options.
# - argp: argp::parseArgs walks the entire argument list as {key value} pairs.
# - parse_args: undashed spec entries only match after the options.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval argparsingtest::sandwich {
namespace export {[a-z]*} ;# Convention: export all lowercase
#*** !doctools
#[subsection {Namespace argparsingtest::sandwich}]
#[para] parser implementations - sandwich style: 2 leading positional arguments, then options, then 3 trailing positional arguments
#[list_begin definitions]
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::manual_switch
@cmd -name argparsingtest::sandwich::manual_switch -summary "manual parsing - sandwich style baseline" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus the
valueless -join flag, then 3 trailing positional arguments.
The leading positionals are plain proc formal parameters; the option
loop consumes one word for -join and two for every other option, then
whatever remains must be exactly the 3 trailing positionals.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc manual_switch {p1 p2 args} {
set opts [dict create {*}{
-return string
-frametype \uFFEF
-show_edge \uFFEF
-show_seps \uFFEF
-join 0
-x ""
-y b
-z c
-1 1
-2 2
-3 3
}]
set i 0
set argc [llength $args]
while {$i < $argc} {
set k [lindex $args $i]
switch -- $k {
-join {
#valueless flag - consumes one word, not two
dict set opts -join 1
incr i
}
-return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 {
if {$i + 1 >= $argc} {
error "option '$k' requires a value"
}
dict set opts $k [lindex $args [expr {$i + 1}]]
incr i 2
}
default {
#first non-option word ends the option run - the trailing positionals
break
}
}
}
set values [lrange $args $i end]
if {[llength $values] != 3} {
error "expected exactly 3 trailing positional arguments, got [llength $values]"
}
return [list [list $p1 $p2] $values $opts]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::punkargs
@cmd -name argparsingtest::sandwich::punkargs -summary "punk::args withdef, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus -join,
then 3 trailing positional arguments.
Uses punk::args::parse withdef with @leaders, @opts and @values sections -
the arrangement is declared, not inferred.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc punkargs {args} {
set argd [punk::args::parse $args withdef {
@id -id ::argparsingtest::sandwich::punkargs
@cmd -name argparsingtest::sandwich::punkargs -help "test of punk::args::parse comparative performance"
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-join -type none -multiple 1
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
set leaders [tcl::dict::get $argd leaders]
set values [tcl::dict::get $argd values]
return [list\
[list [tcl::dict::get $leaders p1] [tcl::dict::get $leaders p2]]\
[list [tcl::dict::get $values p3] [tcl::dict::get $values p4] [tcl::dict::get $values p5]]\
[tcl::dict::get $argd opts]]
}
#Sole documentation source is the eager punk::args::define (load-time
#registration is part of what this variant measures) - see the
#opts::punkargs_by_id comment.
punk::args::define {
@id -id ::argparsingtest::sandwich::punkargs_by_id
@cmd -name argparsingtest::sandwich::punkargs_by_id\
-summary "punk::args withid pre-registered, sandwich style"\
-help\
"Accepts 2 leading positional arguments, then 10 keyed options plus -join,
then 3 trailing positional arguments.
Parses via a definition pre-registered at module load, using withid.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-join -type none -multiple 1
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}
proc punkargs_by_id {args} {
set argd [punk::args::parse $args withid ::argparsingtest::sandwich::punkargs_by_id]
set leaders [tcl::dict::get $argd leaders]
set values [tcl::dict::get $argd values]
return [list\
[list [tcl::dict::get $leaders p1] [tcl::dict::get $leaders p2]]\
[list [tcl::dict::get $values p3] [tcl::dict::get $values p4] [tcl::dict::get $values p5]]\
[tcl::dict::get $argd opts]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::punkargs_parsecache
@cmd -name argparsingtest::sandwich::punkargs_parsecache -summary "punk::args withid with parse cache, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus -join,
then 3 trailing positional arguments.
Uses withid and -cache 1 for repeated parse reuse.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc punkargs_parsecache {args} {
set argd [punk::args::parse $args -cache 1 withid ::argparsingtest::sandwich::punkargs_by_id]
set leaders [tcl::dict::get $argd leaders]
set values [tcl::dict::get $argd values]
return [list\
[list [tcl::dict::get $leaders p1] [tcl::dict::get $leaders p2]]\
[list [tcl::dict::get $values p3] [tcl::dict::get $values p4] [tcl::dict::get $values p5]]\
[tcl::dict::get $argd opts]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::opt
@cmd -name argparsingtest::sandwich::opt -summary "tcllib opt-based arg parsing, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus the
valueless -join flag, then 3 trailing positional arguments.
Uses tcl::OptProc with the leading positionals declared first, the flags
next and the trailing positionals last - tcl::OptProc fills positional
slots in declaration order, so the arrangement is expressed by the spec.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
tcl::OptProc opt {
{p1 -string "first leading positional argument"}
{p2 -string "second leading positional argument"}
{-return string "return type"}
{-frametype \uFFEF "type of frame"}
{-show_edge \uFFEF "show table outer borders"}
{-show_seps \uFFEF "show separators"}
{-join "solo option"}
{-x "" "x val"}
{-y b "y val"}
{-z c "z val"}
{-1 1 "1val"}
{-2 -int 2 "2val"}
{-3 -int 3 "3val"}
{p3 -string "first trailing positional argument"}
{p4 -string "second trailing positional argument"}
{p5 -string "third trailing positional argument"}
} {
set opts [dict create]
foreach v [info locals] {
if {$v ni {p1 p2 p3 p4 p5 opts args Args}} {
dict set opts $v [set $v]
}
}
return [list [list $p1 $p2] [list $p3 $p4 $p5] $opts]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::argparse
@cmd -name argparsingtest::sandwich::argparse -summary "argparse package arg parsing, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus the
valueless -join flag, then 3 trailing positional arguments.
Uses argparse -inline -mixed. -mixed permits switches anywhere, which is
what lets the leading positionals through; the cost is that this spec
does not CONSTRAIN the arrangement the way the punk::args and opt
wrappers do - it would equally accept all five positionals trailing.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc argparse {args} {
set parsed [::argparse -inline -mixed {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
{p1}
{p2}
{p3}
{p4}
{p5}
} $args]
return [list\
[list [dict get $parsed p1] [dict get $parsed p2]]\
[list [dict get $parsed p3] [dict get $parsed p4] [dict get $parsed p5]]\
[dict remove $parsed p1 p2 p3 p4 p5]]
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace argparsingtest::sandwich ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace # Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++

9
src/modules/argparsingtest-buildversion.txt

@ -1,6 +1,13 @@
1.0.0 1.2.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.
#1.2.0 - fourth calling style 'sandwich': 2 mandatory leading positionals, THEN the 10 keyed options (argvec now exercises the valueless -join flag alongside -flag value pairs), THEN 3 mandatory trailing positionals. The 2/3 split is asymmetric on purpose - an even split could be satisfied by a parser that just halves the positional block. New namespace argparsingtest::sandwich with manual_switch, punkargs, punkargs_by_id, punkargs_parsecache, opt and argparse wrappers; return contract is {{p1 p2} {p3 p4 p5} optsdict} (grouped rather than the flat {p1 p2 opts} of the other positional styles, since positionals sit on both sides of the options)
#1.2.0 - sandwich style: cmdline, tepam, argp and parse_args are 'unsupported' (N/A rows) - each verified to fail on the style's argvec. cmdline is the notable one: cmdline::getoptions returns WITHOUT error having parsed nothing, leaving every option at its default and the argument list untouched
#1.2.0 - sandwich style: punk::args, opt and the manual baseline model the arrangement and reject options ahead of the leaders; argparse reaches the style only via -mixed, which permits switches anywhere and therefore parses the argvec correctly WITHOUT enforcing the arrangement (pinned by the sandwich_strictness / sandwich_argparse_is_permissive tests)
#1.2.0 - -style now accepts sandwich in discover_parsers, timeit, first_call and compare
#1.1.0 - timing wrappers for the optional external packages parse_args (0.5.1) and argparse (0.61): opts and tclstyle wrappers for both, plus a tkstyle wrapper for argparse (which needs argparse -mixed; parse_args cannot consume leading positionals so tkstyle parse_args is 'unsupported'). library_warmups entries added for both so first_call warm timings are real. Both moved out of the optional_externals probe list into the style rosters - where the package is present they are now timed as table rows instead of being reported not_wrappable
#1.1.0 - roster marker {optional <pkg>} generalises the former hardcoded 'argp' marker: loaded when <pkg> is present at module load, not_loaded otherwise
#1.1.0 - compare reports not_wrappable separately from not_loaded: new 'no timing wrapper:' table line and a new not_wrappable key in the -format dict and -format json output (previously both statuses were conflated under the 'not loaded:' line)
#1.0.0 - BREAKING restructure: parser procs moved into per-style child namespaces (argparsingtest::opts, ::tkstyle, ::tclstyle) and renamed - test1_ prefix dropped, hand-rolled parsers now share a manual_ prefix (e.g. test1_switch -> opts::manual_switch, test1_punkargs -> opts::punkargs); test1_prefix2 removed (not meaningfully distinct from test1_prefix); unused struct::set require dropped; opt/cmdline/tepam requires hoisted to module top #1.0.0 - BREAKING restructure: parser procs moved into per-style child namespaces (argparsingtest::opts, ::tkstyle, ::tclstyle) and renamed - test1_ prefix dropped, hand-rolled parsers now share a manual_ prefix (e.g. test1_switch -> opts::manual_switch, test1_punkargs -> opts::punkargs); test1_prefix2 removed (not meaningfully distinct from test1_prefix); unused struct::set require dropped; opt/cmdline/tepam requires hoisted to module top
#1.0.0 - new tkstyle (2 positionals first) and tclstyle (2 positionals last) parser sets returning {p1 p2 opts}; harness procs (discover_parsers/timeit/first_call/compare) gain -style and return results keyed by style; new 'unsupported' discovery status for library/style combos the backing library cannot express (cmdline in tkstyle, argp in both positional styles) - rendered as N/A rows in compare tables; external probe keys renamed (test1_getopt -> getopt etc) #1.0.0 - new tkstyle (2 positionals first) and tclstyle (2 positionals last) parser sets returning {p1 p2 opts}; harness procs (discover_parsers/timeit/first_call/compare) gain -style and return results keyed by style; new 'unsupported' discovery status for library/style combos the backing library cannot express (cmdline in tkstyle, argp in both positional styles) - rendered as N/A rows in compare tables; external probe keys renamed (test1_getopt -> getopt etc)
#0.2.0 - added timeit/compare/discover_parsers procs for rough parser timing comparison; PUNKARGS argdoc blocks added to all procs #0.2.0 - added timeit/compare/discover_parsers procs for rough parser timing comparison; PUNKARGS argdoc blocks added to all procs

152
src/tests/modules/argparsingtest/testsuites/argparsingtest/argparsingtest.test

@ -8,6 +8,13 @@ namespace eval ::testspace {
#added 2026-07-17 (agent) - argp is an optional package; its load-time require is guarded by catch in the module #added 2026-07-17 (agent) - argp is an optional package; its load-time require is guarded by catch in the module
testConstraint argp_loaded [expr {![catch {package present argp}]}] testConstraint argp_loaded [expr {![catch {package present argp}]}]
#added 2026-07-26 (agent) - parse_args and argparse are optional packages with shipped timing
#wrappers; both are binary extensions absent from a stock tclsh and present on different
#runtimes (survey in the module, near its optional-package requires), so each is constrained
#separately rather than assuming they travel together
testConstraint parse_args_loaded [expr {![catch {package present parse_args}]}]
testConstraint argparse_loaded [expr {![catch {package present argparse}]}]
#added 2026-07-18 (agent) - suite rewritten for the argparsingtest 1.0.0 restructure: #added 2026-07-18 (agent) - suite rewritten for the argparsingtest 1.0.0 restructure:
#parser procs live in per-style namespaces (opts/tkstyle/tclstyle), harness results are keyed by style #parser procs live in per-style namespaces (opts/tkstyle/tclstyle), harness results are keyed by style
#Split 2026-07-19 (G-092 - parallel-floor reduction, tests moved verbatim): this file holds #Split 2026-07-19 (G-092 - parallel-floor reduction, tests moved verbatim): this file holds
@ -28,12 +35,12 @@ namespace eval ::testspace {
set ok set ok
} -result 1 } -result 1
#added 2026-07-18 (agent) #added 2026-07-18 (agent), sandwich style added 2026-07-27 (agent)
test timeit_all_styles {Test that timeit default -style all times a parser under each of the three styles} -body { test timeit_all_styles {Test that timeit default -style all times a parser under each of the four styles} -body {
set r [argparsingtest::timeit -iterations 20 -parsers {manual_switch}] set r [argparsingtest::timeit -iterations 20 -parsers {manual_switch}]
set ok 1 set ok 1
if {[lsort [dict keys $r]] ne [lsort {opts tkstyle tclstyle}]} { set ok 0 } if {[lsort [dict keys $r]] ne [lsort {opts tkstyle tclstyle sandwich}]} { set ok 0 }
foreach s {opts tkstyle tclstyle} { foreach s {opts tkstyle tclstyle sandwich} {
if {!$ok} { break } if {!$ok} { break }
if {![dict exists $r $s manual_switch us_per_call]} { set ok "missing $s" ; break } if {![dict exists $r $s manual_switch us_per_call]} { set ok "missing $s" ; break }
if {![string is double -strict [dict get $r $s manual_switch us_per_call]]} { set ok "non-numeric $s" ; break } if {![string is double -strict [dict get $r $s manual_switch us_per_call]]} { set ok "non-numeric $s" ; break }
@ -64,10 +71,20 @@ namespace eval ::testspace {
} -result 1 } -result 1
#added 2026-07-18 (agent) - roster sizes: opts 16 parsers + 4 probed externals, positional styles 9 + 4 #added 2026-07-18 (agent) - roster sizes: opts 16 parsers + 4 probed externals, positional styles 9 + 4
#updated 2026-07-26 (agent) - parse_args and argparse moved from the probed externals into the
#rosters when their timing wrappers were added, so the per-style totals are unchanged: opts is
#now 18 roster entries + 2 probed (getopt, cmdr), each positional style 11 + 2
#updated 2026-07-27 (agent) - fourth style 'sandwich' (2 leaders, options, 3 trailing): 11 + 2
test discover_parsers_shape {Test discover_parsers style keying, entry counts and statuses} -body { test discover_parsers_shape {Test discover_parsers style keying, entry counts and statuses} -body {
set r [argparsingtest::discover_parsers] set r [argparsingtest::discover_parsers]
set ok 1 set ok 1
if {[lsort [dict keys $r]] ne [lsort {opts tkstyle tclstyle}]} { set ok "style keys: [dict keys $r]" } if {[lsort [dict keys $r]] ne [lsort {opts tkstyle tclstyle sandwich}]} { set ok "style keys: [dict keys $r]" }
if {$ok eq 1 && [dict size [dict get $r sandwich]] != 13} { set ok "sandwich size [dict size [dict get $r sandwich]]" }
if {$ok eq 1 && [dict get $r sandwich manual_switch] ne "loaded"} { set ok "sandwich manual_switch status" }
if {$ok eq 1 && [dict get $r sandwich opt] ne "loaded"} { set ok "sandwich opt status" }
foreach pname {cmdline_untyped cmdline_typed tepam argp parse_args} {
if {$ok eq 1 && [dict get $r sandwich $pname] ne "unsupported"} { set ok "sandwich $pname status" }
}
if {$ok eq 1 && [dict size [dict get $r opts]] != 20} { set ok "opts size [dict size [dict get $r opts]]" } if {$ok eq 1 && [dict size [dict get $r opts]] != 20} { set ok "opts size [dict size [dict get $r opts]]" }
if {$ok eq 1 && [dict size [dict get $r tkstyle]] != 13} { set ok "tkstyle size [dict size [dict get $r tkstyle]]" } if {$ok eq 1 && [dict size [dict get $r tkstyle]] != 13} { set ok "tkstyle size [dict size [dict get $r tkstyle]]" }
if {$ok eq 1 && [dict size [dict get $r tclstyle]] != 13} { set ok "tclstyle size [dict size [dict get $r tclstyle]]" } if {$ok eq 1 && [dict size [dict get $r tclstyle]] != 13} { set ok "tclstyle size [dict size [dict get $r tclstyle]]" }
@ -80,6 +97,59 @@ namespace eval ::testspace {
set ok set ok
} -result 1 } -result 1
#added 2026-07-26 (agent) - {optional P} roster entries (parse_args, argparse) report loaded/not_loaded
#by package presence, unlike the probe-only getopt/cmdr entries which report not_wrappable when present
test discover_parsers_optional_externals {Test that optional-external parser statuses track package presence} -body {
set r [argparsingtest::discover_parsers]
set ok 1
foreach {pkg pname styles} {
parse_args parse_args {opts tclstyle}
argparse argparse {opts tkstyle tclstyle sandwich}
} {
set expected [expr {[catch {package present $pkg}] ? "not_loaded" : "loaded"}]
foreach s $styles {
if {[dict get $r $s $pname] ne $expected} {
set ok "$s $pname: [dict get $r $s $pname] (expected $expected)"
break
}
}
if {$ok ne 1} { break }
}
#parse_args cannot consume leading positionals whether or not the package is present
foreach s {tkstyle sandwich} {
if {$ok eq 1 && [dict get $r $s parse_args] ne "unsupported"} { set ok "$s parse_args status" }
}
#getopt and cmdr stay probe-only - no timing wrapper is shipped for either
foreach pname {getopt cmdr} {
if {$ok eq 1 && [dict get $r opts $pname] ni {not_loaded not_wrappable}} {
set ok "opts $pname: [dict get $r opts $pname]"
}
}
set ok
} -result 1
#added 2026-07-26 (agent) - opts-style external wrappers agree with the hand-rolled baseline's option values
test parse_args_opts_values {Test that the parse_args opts wrapper returns the declared option values} -constraints parse_args_loaded -body {
set r [argparsingtest::opts::parse_args -return object -x hello -y world -1 1 -2 2 -3 3]
set ok 1
#list parsing substitutes the \uFFEF escape into the character, as it does in the module's specs
foreach {k v} {return object x hello y world z c 1 1 2 2 3 3 frametype \uFFEF} {
if {[dict get $r $k] ne $v} { set ok "$k: [dict get $r $k]" ; break }
}
set ok
} -result 1
#added 2026-07-26 (agent)
test argparse_opts_values {Test that the argparse opts wrapper returns the declared option values} -constraints argparse_loaded -body {
set r [argparsingtest::opts::argparse -return object -x hello -y world -1 1 -2 2 -3 3]
set ok 1
#list parsing substitutes the \uFFEF escape into the character, as it does in the module's specs
foreach {k v} {return object x hello y world z c 1 1 2 2 3 3 frametype \uFFEF} {
if {[dict get $r $k] ne $v} { set ok "$k: [dict get $r $k]" ; break }
}
set ok
} -result 1
#added 2026-07-18 (agent) - every loaded tkstyle parser honours the {p1 p2 opts} return contract #added 2026-07-18 (agent) - every loaded tkstyle parser honours the {p1 p2 opts} return contract
test parser_contract_tkstyle {Test that each loaded tkstyle parser extracts leading positionals} -body { test parser_contract_tkstyle {Test that each loaded tkstyle parser extracts leading positionals} -body {
set disco [dict get [argparsingtest::discover_parsers -style tkstyle] tkstyle] set disco [dict get [argparsingtest::discover_parsers -style tkstyle] tkstyle]
@ -110,6 +180,46 @@ namespace eval ::testspace {
set ok set ok
} -result 1 } -result 1
#added 2026-07-27 (agent) - every loaded sandwich parser honours the {{p1 p2} {p3 p4 p5} opts}
#return contract, binding BOTH positional groups from one call; the 2/3 split is asymmetric so a
#parser that merely halves the positional block cannot pass
test parser_contract_sandwich {Test that each loaded sandwich parser splits leading and trailing positionals} -body {
set disco [dict get [argparsingtest::discover_parsers -style sandwich] sandwich]
set ok 1
foreach {pname status} $disco {
if {$status ne "loaded"} { continue }
set r [argparsingtest::sandwich::$pname alpha beta -x hello -join -1 0 gamma delta epsilon]
if {[lindex $r 0] ne {alpha beta} || [lindex $r 1] ne {gamma delta epsilon}} {
set ok "$pname returned: $r"
break
}
}
set ok
} -result 1
#added 2026-07-27 (agent) - the sandwich style is the STRICT arrangement: the parsers that model
#it reject options ahead of the leaders. argparse is the documented exception - it reaches this
#style only via -mixed, which permits switches anywhere and so cannot enforce the arrangement;
#that divergence is pinned here rather than left as a silent difference
test sandwich_strictness {Test that the modelling sandwich parsers reject options before the leaders} -body {
set badvec {-x hello -join alpha beta gamma delta epsilon}
set ok 1
foreach pname {manual_switch punkargs punkargs_by_id punkargs_parsecache opt} {
if {![catch {argparsingtest::sandwich::$pname {*}$badvec} r]} {
set ok "$pname accepted options-before-leaders: $r"
break
}
}
set ok
} -result 1
#added 2026-07-27 (agent) - characterization of the permissive counterpart (see sandwich_strictness)
test sandwich_argparse_is_permissive {Test that the argparse sandwich wrapper accepts arrangements the strict parsers reject} -constraints argparse_loaded -body {
#-mixed permits switches anywhere, so the same spec also accepts all five positionals trailing
set r [argparsingtest::sandwich::argparse -x hello -join alpha beta gamma delta epsilon]
expr {[lindex $r 0] eq {alpha beta} && [lindex $r 1] eq {gamma delta epsilon}}
} -result 1
#added 2026-07-17 (agent) #added 2026-07-17 (agent)
test first_call_handrolled {Test that first_call returns cold and warm float timings for a hand-rolled parser} -body { test first_call_handrolled {Test that first_call returns cold and warm float timings for a hand-rolled parser} -body {
set r [argparsingtest::first_call -style opts -parsers {manual_switch}] set r [argparsingtest::first_call -style opts -parsers {manual_switch}]
@ -140,9 +250,10 @@ namespace eval ::testspace {
} -result 1 } -result 1
#added 2026-07-18 (agent) - positional-style parsers must also time cleanly in child interps #added 2026-07-18 (agent) - positional-style parsers must also time cleanly in child interps
test first_call_positional_styles {Test first_call cold and warm for tkstyle and tclstyle parsers} -body { #sandwich entries added 2026-07-27 (agent)
test first_call_positional_styles {Test first_call cold and warm for tkstyle, tclstyle and sandwich parsers} -body {
set ok 1 set ok 1
foreach {s plist} {tkstyle {punkargs_by_id tepam} tclstyle {cmdline_untyped}} { foreach {s plist} {tkstyle {punkargs_by_id tepam} tclstyle {cmdline_untyped} sandwich {manual_switch punkargs_by_id opt}} {
set r [argparsingtest::first_call -style $s -parsers $plist] set r [argparsingtest::first_call -style $s -parsers $plist]
foreach {pname sub} [dict get $r $s] { foreach {pname sub} [dict get $r $s] {
foreach mode {cold warm} { foreach mode {cold warm} {
@ -156,6 +267,33 @@ namespace eval ::testspace {
set ok set ok
} -result 1 } -result 1
#added 2026-07-26 (agent) - the parse_args/argparse warm-ups run in child interps, which must be
#able to load the extension a second time in-process; both cover every style the wrapper supports
test first_call_parse_args {Test first_call cold and warm for parse_args across its supported styles} -constraints parse_args_loaded -body {
set ok 1
foreach s {opts tclstyle} {
set sub [dict get [argparsingtest::first_call -style $s -parsers {parse_args}] $s parse_args]
foreach mode {cold warm} {
if {![string is double -strict [dict get $sub $mode]]} { set ok "$s $mode: [dict get $sub $mode]" ; break }
}
if {$ok ne 1} { break }
}
set ok
} -result 1
#added 2026-07-26 (agent)
test first_call_argparse {Test first_call cold and warm for argparse across its supported styles} -constraints argparse_loaded -body {
set ok 1
foreach s {opts tkstyle tclstyle} {
set sub [dict get [argparsingtest::first_call -style $s -parsers {argparse}] $s argparse]
foreach mode {cold warm} {
if {![string is double -strict [dict get $sub $mode]]} { set ok "$s $mode: [dict get $sub $mode]" ; break }
}
if {$ok ne 1} { break }
}
set ok
} -result 1
#added 2026-07-17 (agent) #added 2026-07-17 (agent)
test first_call_argp {Test first_call cold and warm for argp when argp is loaded} -constraints argp_loaded -body { test first_call_argp {Test first_call cold and warm for argp when argp is loaded} -constraints argp_loaded -body {
set r [argparsingtest::first_call -style opts -parsers {argp}] set r [argparsingtest::first_call -style opts -parsers {argp}]

14
src/tests/modules/argparsingtest/testsuites/argparsingtest/argparsingtest_compare.test

@ -13,20 +13,22 @@ namespace eval ::testspace {
#parser-contract/first_call tests live in argparsingtest.test in this directory. #parser-contract/first_call tests live in argparsingtest.test in this directory.
#added 2026-07-18 (agent) - N/A rows: unsupported library/style combos stay visible in the table #added 2026-07-18 (agent) - N/A rows: unsupported library/style combos stay visible in the table
test compare_table_sections {Test that compare -format table emits all three style sections with N/A rows} -body { #sandwich section added 2026-07-27 (agent)
test compare_table_sections {Test that compare -format table emits all four style sections with N/A rows} -body {
set r [argparsingtest::compare -iterations 20 -format table -parsers {manual_switch punkargs cmdline_typed argp}] set r [argparsingtest::compare -iterations 20 -format table -parsers {manual_switch punkargs cmdline_typed argp}]
set ok 1 set ok 1
foreach expected {"options only" "tk style (positionals first)" "tcl style (positionals last)" "N/A" "not loaded:" "Parser"} { foreach expected {"options only" "tk style (positionals first)" "tcl style (positionals last)" "sandwich style (2 leaders, options, 3 trailing)" "N/A" "not loaded:" "Parser"} {
if {[string first $expected $r] < 0} { set ok "missing: $expected" ; break } if {[string first $expected $r] < 0} { set ok "missing: $expected" ; break }
} }
set ok set ok
} -result 1 } -result 1
#added 2026-07-18 (agent) #added 2026-07-18 (agent), not_wrappable key added 2026-07-26 (agent) - packages present but
test compare_dict {Test that compare -format dict returns style-keyed results/first_call/not_loaded/unsupported} -body { #without a shipped timing wrapper are reported separately from absent ones
test compare_dict {Test that compare -format dict returns style-keyed results/first_call/not_loaded/not_wrappable/unsupported} -body {
set r [argparsingtest::compare -style opts -iterations 20 -parsers {manual_switch} -format dict] set r [argparsingtest::compare -style opts -iterations 20 -parsers {manual_switch} -format dict]
set ok 1 set ok 1
foreach key {results first_call not_loaded unsupported} { foreach key {results first_call not_loaded not_wrappable unsupported} {
if {![dict exists $r $key]} { set ok "missing $key" ; break } if {![dict exists $r $key]} { set ok "missing $key" ; break }
} }
if {$ok eq 1 && ![dict exists $r results opts manual_switch us_per_call]} { set ok "missing results.opts.manual_switch" } if {$ok eq 1 && ![dict exists $r results opts manual_switch us_per_call]} { set ok "missing results.opts.manual_switch" }
@ -43,6 +45,8 @@ namespace eval ::testspace {
if {$ok eq 1 && [dict get $d unsupported tkstyle] ne "cmdline_typed"} { set ok "unsupported: [dict get $d unsupported tkstyle]" } if {$ok eq 1 && [dict get $d unsupported tkstyle] ne "cmdline_typed"} { set ok "unsupported: [dict get $d unsupported tkstyle]" }
if {$ok eq 1 && ![dict exists $d first_call tkstyle manual_switch cold]} { set ok "missing first_call cold" } if {$ok eq 1 && ![dict exists $d first_call tkstyle manual_switch cold]} { set ok "missing first_call cold" }
if {$ok eq 1 && ![dict exists $d first_call tkstyle manual_switch warm]} { set ok "missing first_call warm" } if {$ok eq 1 && ![dict exists $d first_call tkstyle manual_switch warm]} { set ok "missing first_call warm" }
#added 2026-07-26 (agent)
if {$ok eq 1 && ![dict exists $d not_wrappable tkstyle]} { set ok "missing not_wrappable" }
set ok set ok
} -result 1 } -result 1

Loading…
Cancel
Save