Serves the engine's 'truncate/summarize values in error messages' todo
(G-151 lean 5, explicitly deferred from that goal's acceptance). New
private::error_display_value wraps the G-151 report_display_value /
ansistring TRUNCATE machinery (string mode: single-line-ize with visible
control/ANSI rendering, then grapheme-cap with '...' plus ' (len N)') and
is applied at every site interpolating a received word into a validation
error message: the check_clausecolumn type/size/range/regexprefail
reports, the leaders/values allocation-rejection and choiceviolation/
ambiguous-prefix sites, the multi-value clause echo, the -allow_ansi
character-view (previously unbounded ansistring VIEW), and the
Unexpected-option unknown-flag word. Width cascade: definition-level
'@cmd -reportvaluewidth' > built-in 48; 0 = no cap (define -help
documents the extension). Display-only: -badarg/-badval errorcode fields
keep the raw words.
Safety properties: a short plain value (within budget, no C0/C1/DEL
content) displays byte-identical via a cheap fast path - important both
for the existing message pins (all held unchanged) and because
check_clausecolumn builds candidate messages for type alternatives that
may never be raised; and the helper catches-and-degrades to the plain
non-ansistring fallbacks, since punk::ansi loaded without punk::lib dies
in ansistring TRUNCATE (RANGE calls punk::lib::lindex_resolve) - an
error message must never die on its display helper. parse_report's own
value cells retain that uncaught exposure (pre-existing, success-side).
validation.test +4 pins (long-value truncation with raw -badval
retention, multiline single-line-ization, @cmd -reportvaluewidth cap,
unexpected-option word truncation with raw -badarg retention). Args
subtree 415/0 on tclsh90s (9.0.5) and tclsh86ts; full suite at the
recorded 13-failure environment baseline (maketclhelp re-verified 12/12
with a real tclsh on PATH). Minted modules/punk/args-0.26.0.tm.
Project version 0.66.1 (patch: kit-shipped error-display behaviour) with
CHANGELOG entry; G-151 archive follow-on disposition flipped to landed.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Follow-on: make.tcl retires its interim one-line 'dry-run: line accepted ...' report in favour of parse_report (stacked under the usage render per lean 1) after a bootsupport promotion carrying punk::args 0.18.0- and punk::ansi 0.2.0- => open
Follow-on: serve arg_error's failure-side value-truncation todo ('truncate/summarize values in error messages', args-999999.0a1.0.tm arg_error region) with the same report_display_value/TRUNCATE helpers (lean 5 - explicitly excluded from this goal's acceptance) => open
Follow-on: serve arg_error's failure-side value-truncation todo ('truncate/summarize values in error messages', args-999999.0a1.0.tm arg_error region) with the same report_display_value/TRUNCATE helpers (lean 5 - explicitly excluded from this goal's acceptance) => landed 2026-08-09 (punk::args 0.26.0 private::error_display_value - all received-word message sites incl unexpected-option words; '@cmd -reportvaluewidth' cascade; raw -badarg/-badval retained; validation.test pins)
Follow-on: engine-level rationalization of parse-result storage keys vs definition argument names (aliased optionset last-|-member fold, -parsekey renames) so value-in-effect lookups need no display-level bridging => goal G-084
Follow-on: column-aware (display-width) TRUNCATE variant for CJK-heavy values (the grapheme-count cap under-counts double-wide content; noted in the TRUNCATE argdoc) => open
Follow-on: word-oriented inverse view (word -> consuming argument, e.g a -return words variant) as completion/hinting-facing data - lean 1 records it as a possible later variant, not the primary => open
#this clause is satisfied - no need to process it for other typealt
break
} else {
set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'"
set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '[private::error_display_value $e $argspecs]'"
set msg "$argclass $argname for %caller% requires type regexp with at least $mincap capture groups. Received regex has only $num_caps capture groups. Regex: '$e_check'"
set msg "$argclass $argname for %caller% requires type regexp with at least $mincap capture groups. Received regex has only $num_caps capture groups. Regex: '[private::error_display_value $e_check $argspecs]'"
set msg "$argclass $argname for %caller% requires type regexp with at least $mincap capture groups. Received regex has only $num_caps capture groups. Regex: '$e_check'"
set msg "$argclass $argname for %caller% requires type regexp with at least $mincap capture groups. Received regex has only $num_caps capture groups. Regex: '[private::error_display_value $e_check $argspecs]'"
set msg "$argclass $argname for %caller% requires type regexp with no more than $maxcap capture groups. Received regex has $num_caps capture groups. Regex: '$e_check'"
set msg "$argclass $argname for %caller% requires type regexp with no more than $maxcap capture groups. Received regex has $num_caps capture groups. Regex: '[private::error_display_value $e_check $argspecs]'"
if {$e_check eq "" || [catch {lindex {} $e_check}]} {
set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'"
set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '[private::error_display_value $e_check $argspecs]'"
set msg "$argclass $argname for %caller% requires type indexset. A comma-delimited set of indexes or index-ranges separated by '..' Received: '$e_check'"
set msg "$argclass $argname for %caller% requires type indexset. A comma-delimited set of indexes or index-ranges separated by '..' Received: '[private::error_display_value $e_check $argspecs]'"
if {[catch {::package vsatisfies $e_check $e_check}]} {
set msg "$argclass $argname for %caller% requires type packageversion. A package version number as understood by 'package vsatifies'. Received: '$e_check'"
set msg "$argclass $argname for %caller% requires type packageversion. A package version number as understood by 'package vsatifies'. Received: '[private::error_display_value $e_check $argspecs]'"
set msg "$argclass $argname for %caller% requires type packagerequirement. (form min min- or min-max) Received: '$e_check'"
set msg "$argclass $argname for %caller% requires type packagerequirement. (form min min- or min-max) Received: '[private::error_display_value $e_check $argspecs]'"
if {[catch {::package vsatisfies $vcheck $vcheck}]} {
set msg "$argclass $argname for %caller% requires type packagerequirement. (from min min- or min-max) . Received: '$e_check'"
set msg "$argclass $argname for %caller% requires type packagerequirement. (from min min- or min-max) . Received: '[private::error_display_value $e_check $argspecs]'"
set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'"
set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'[private::error_display_value $e_check $argspecs]'"
set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'"
set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'[private::error_display_value $e_check $argspecs]'"
set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'"
set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '[private::error_display_value $e $argspecs]'"
set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'"
set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '[private::error_display_value $e $argspecs]'"
set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'"
set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '[private::error_display_value $e $argspecs]'"
set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'"
set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '[private::error_display_value $e $argspecs]'"
#elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive
if {![tcl::string::is integer -strict $e_check]} {
set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'"
set msg "$argclass $argname for %caller% requires type integer. Received: '[private::error_display_value $e_check $argspecs]'"
#to maintain support for tcl 8.6 - can't directly use 'string is dict'
if {![punk::args::lib::string_is_dict $e_check]} {
set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'"
set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '[private::error_display_value $e_check $argspecs]'"
#see comments in existingpath/existingfile/existingdirectory case about the challenges of validating filesystem paths in a general way that works across platforms and use cases.
if {[tcl::string::length $e_check]==0 || [string first \0 $e_check] >= 0} {
set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a path, file or directory"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '[private::error_display_value $e $argspecs]' which doesn't look like it could be a path, file or directory"
#NUL is almost universally problematic - so we will reject.
if {[tcl::string::length $e_check]==0 || [string first \0 $e_check] >= 0} {
set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '[private::error_display_value $e $argspecs]' which doesn't look like it could be a file or directory"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '[private::error_display_value $e $argspecs]' which is not an existing directory"
#what about windows specific paths such as //?/ //./ or UNC paths?
if {[tcl::string::length $e_check]==0 || [string first \0 $e_check] >= 0 || [punk::winpath::illegalname_test $e_check]} {
set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a portable file or directory (must pass punk::winpath::illegalname_test)"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '[private::error_display_value $e $argspecs]' which doesn't look like it could be a portable file or directory (must pass punk::winpath::illegalname_test)"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory"
set msg "$argclass $argname for %caller% requires type '$type'. Received: '[private::error_display_value $e $argspecs]' which is not an existing directory"
set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character"
set msg "$argclass $argname for %caller% requires type 'character'. Received: '[private::error_display_value $e $argspecs]' which is not a single character"
set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels."
set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '[private::error_display_value $e $argspecs]' Which does not seem to be in a form as accepted ty Tk_GetPixels."
set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels."
set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '[private::error_display_value $e $argspecs]' Which does not seem to be in a form as accepted ty Tk_GetPixels."
lappend flagsreceived $undefined_flagsupplied ;#adhoc flag name (if --x=1 -> --x)
} else {
if {[llength $OPT_NAMES]} {
set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)"
set errmsg "bad options for %caller%. Unexpected option \"[private::error_display_value $a $argspecs]\": must be one of: $OPT_NAMES (3)"
} else {
set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any|-arbitrary false"
set errmsg "bad options for %caller%. Unexpected option \"[private::error_display_value $a $argspecs]\": No options defined while @opts -any|-arbitrary false"
set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername_multiple', but requires up to [llength $leadertypelist] values."
set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('[private::error_display_value $clauseval $argspecs]') for '$leadername_multiple', but requires up to [llength $leadertypelist] values."
set msg "$rj_argclass '$rj_argname' for %caller% must be one of the listed values:\n [join $rj_choices "\n "]\n$rj_casemsg$rj_prefixmsg. Received: '$ldr'"
set msg "$rj_argclass '$rj_argname' for %caller% must be one of the listed values:\n [join $rj_choices "\n "]\n$rj_casemsg$rj_prefixmsg. Received: '[private::error_display_value $ldr $argspecs]'"
#occurrence limit (same errorcode class as post-loop
#enforcement; count is the attempted occurrence)
set rj_max [dict get $rj max]
set msg "$rj_argclass '$rj_argname' for %caller% accepts at most $rj_max occurrence(s). Received extra word: '$ldr'"
set msg "$rj_argclass '$rj_argname' for %caller% accepts at most $rj_max occurrence(s). Received extra word: '[private::error_display_value $ldr $argspecs]'"
set msg "$rj_argclass '$rj_argname' for %caller% must be one of the listed values:\n [join $rj_choices "\n "]\n$rj_casemsg$rj_prefixmsg. Received: '$val'"
set msg "$rj_argclass '$rj_argname' for %caller% must be one of the listed values:\n [join $rj_choices "\n "]\n$rj_casemsg$rj_prefixmsg. Received: '[private::error_display_value $val $argspecs]'"
#(MAINTENANCE - same selection logic as leaders loop above)
set rj_max [dict get $rj max]
set msg "$rj_argclass '$rj_argname' for %caller% accepts at most $rj_max occurrence(s). Received extra word: '$val'"
set msg "$rj_argclass '$rj_argname' for %caller% accepts at most $rj_max occurrence(s). Received extra word: '[private::error_display_value $val $argspecs]'"
lappend longermatches_display "$cu_lm (recognised but unavailable)"
}
if {[llength $longermatches] || [llength $longermatches_unavail] > 1} {
set msg "$argclass '$argname' for %caller% seems to be an ambiguous prefix. Try one of:\n [join $longermatches_display "\n "]\n$casemsg$prefixmsg. Received: '$c_check'"
set msg "$argclass '$argname' for %caller% seems to be an ambiguous prefix. Try one of:\n [join $longermatches_display "\n "]\n$casemsg$prefixmsg. Received: '[private::error_display_value $c_check $argspecs]'"
set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'"
set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '[private::error_display_value $c_check $argspecs]'"
#arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname
#0.26.0 - G-151 follow-on (failure-side value truncation - the 'truncate/summarize values in error messages' engine todo): received values embedded in validation error messages are now display-bounded via new private::error_display_value - the same report_display_value/ansistring TRUNCATE machinery as parse_report's value cells (string mode: single-line-ize via ansistring VIEW -lf 1 -vt 1 -sp 0 so controls/ANSI render visibly, then grapheme-cap with trailing '...' plus ' (len N)' citing the raw length). Applied at every site interpolating a received word into a message: the check_clausecolumn type/size/range/regexprefail reports, the leaders/values allocation-rejection reports (typemismatch/choiceviolation/occurrence overflow incl the multi-value clause echo), the final-validation choiceviolation/ambiguous-prefix reports, the -allow_ansi character-view (previously unbounded ansistring VIEW), and the Unexpected-option unknown-flag word. Width cascade: definition-level '@cmd -reportvaluewidth' > built-in 48; 0 = no cap (define -help documents the extension). Display-only: -badarg/-badval in errorcodes keep the raw words. A short plain value (within budget, no C0/C1/DEL content) displays byte-identical via a cheap fast path - all existing message pins unchanged - and the helper degrades to the plain (non-ansistring) fallbacks if the display machinery itself errors (known window: punk::ansi loaded without punk::lib - ansistring TRUNCATE dies in RANGE until a later render loads punk::lib; an error message must never die on its display helper). New validation.test pins: long-value truncation + raw -badval retention, multiline single-line-ization, @cmd -reportvaluewidth cap, unexpected-option word truncation + raw -badarg retention. Full punk/args suite 415/0 on tclsh90 (9.0.5) and tclsh86.
#0.25.2 - bugfix (reported 2026-08-08 as 'i ::tcl::prefix' emitting 12x 'warning: update_definitions received unqualified ns: tcl'): ensemble_subcommands_definition now normalizes its ensemble argument to fully-qualified in the CALLER's context (namespace which via uplevel, :: prepend fallback for a not-yet-existing command) before deriving anything from it. Previously a relative multi-component name (the tclcore moduledoc passed 'tcl::prefix') flowed into the space-form id_checks, whose 'namespace qualifiers' derivation produced a non-empty UNQUALIFIED namespace ('tcl') that the empty-qualifier guard did not catch - update_definitions then warned on stderr once per subcommand per call (and @dynamic definitions re-run their builders on every resolve: 4 resolves x 3 subcommands = the reported 12). Single-component ensemble names (info/dict/file/...) were never affected (empty qualifier -> :: guard). Rendered output verified byte-identical for both name forms. The tclcore moduledoc call site now passes ::tcl::prefix as well (tclcore 0.4.2). Pin: ensembledef.test ensembledef_unqualified_ensemble_name_no_warnings (fixture two-component ensemble called unqualified: zero warnings + snippet parity with the qualified call).
#0.25.1 - bugfix: the -type regex|regexp validator's 'regexp -about' lacked its -- end-of-options guard, so a legitimate dash-led regex VALUE (e.g '-group', a valid ARE matching its literal self) was falsely rejected as 'requires type regexp' with regexp's own bad-option text embedded as the reason. Dash-led regexes now validate; a genuinely invalid dash-led regex still fails as a type mismatch with the real compile reason. Found via the 2026-08-08 grepstr dash-led pattern regression report (punk::ansi 0.2.1 carries the sibling 'regexp -about' fix in grepstr itself; the parse/allocation layer was verified NOT at fault). New validation.test pins: type_regexp_dashled_value (accept + real-reason-reject) and opt_dashword_seats_when_values_require_it (the starved-values dash-led seating rule grepstr relies on - no -- marker needed when word supply equals required-values demand).
#0.25.0 - G-053 allocation participation (directed work, post-G-053): bounded -multiple occurrence ranges now PARTICIPATE in positional allocation for leaders and values instead of being post-loop enforcement only. Previously greedy collection overran a bounded max unless a type screen happened to stop it (untyped {3 3} followed by a -multiple tail collected 4 words and occurrencecount then reported the overrun the allocation itself created), and a later REQUIRED ranged argument was starved to a single reserved clause. Three cooperating sites: (a) get_dict_can_assign_value caps a satisfied -multiple argument at its bounded max - it yields the word (no-consume) so the caller's retreat advances to the next argument, serving both the leaders and values loops; the yield carries a G-082 rejection record of new kind 'occurrence', and both loops' overflow selection sites render it as the pointed occurrence-limit report with the G-053 occurrencecount errorcode (count = the attempted occurrence) instead of the generic toomanyarguments - so genuine over-supply keeps its pointed class; (b) reservations are min-occurrence aware - the allocator's tail_needs and the derived valmin floor (leader/value split + option-scan reservation) reserve min-occurrences x min-clause-length for a required ranged argument (e.g a following required -multiple {3 3} reserves 3 clauses; {2 2} pair clauses reserve 4 words); an explicit '@values -min' still overrides the derived floor, and legacy boolean -multiple (no MULTIPLE_RANGES entry) reserves exactly as before; (c) the greedy leader scan caps a bounded -multiple last leader at max occurrences (tracked via leader_posn_names_assigned) so remaining words flow to the opts/values sections. First occurrences are never capped (resolve guarantees range max >= 1); {0 1} at-most-once scalars are unaffected (no collection). define -help -multiple documents the allocation semantics. New multipleranges.test allocation pins: untyped/typed {3 3} cap incl the cap-beats-type-screen 4-ints case, greedy-up-to-max {1 2}, required-range and pair-clause reservations, leaders-side cap + split floor, pointed over-supply report, legacy-greed-unchanged guard. Full punk/args suite 408/0.
#slim an errorcode: extras up to (excluding) -argspecs
proc ecextras {opts} {
set ec [dict get $opts -errorcode]
set extras [dict create]
foreach {k v} [lrange $ec 3 end] {
if {$k eq "-argspecs"} {break}
dict set extras $k $v
}
return $extras
}
#added 2026-08-09 (agent, G-151) - failure-side value truncation follow-on: received
#values embedded in validation error messages are display-bounded via
#private::error_display_value (same report_display_value/TRUNCATE machinery as
#parse_report value cells). Short plain values stay byte-identical (pinned by the
#existing message pins here and in errorselection.test). The expected displays below
#are identical on the full ansistring path and the plain degraded path (pure-ASCII
#values), so the pins are environment-independent.
test errdisplay_long_value_truncated {an over-budget received value is display-truncated in the error message ('...' + raw length) while errorcode -badval carries the raw value}\
test errdisplay_unexpected_option_word_truncated {an over-budget unknown flag word is display-truncated in the Unexpected option message while errorcode -badarg carries the raw word}\