Browse Source
User-reported: 'i join' emitted the bad-@dynamic warning 4 times ('i
join test' 6). Root cause was a real inefficiency, not just noisy
logging: a @dynamic definition whose round-1 tstr output has no
round-2 parameters bypassed argdefcache_unresolved entirely, so every
resolve in the 'i' flow (doc walk, advisory parse, get_spec for the
render, synopsis) redid display masking plus the full round-1 tstr and
re-warned. Such definitions now cache as a zero-param unresolved entry
- consistent with the round-1 freezing legitimately dynamic
definitions already get - so later resolves take the cheap cached
branch and the warning emits once per definition per interp.
Warning wording corrected: @dynamic is not a complete no-op for a
parse-inert definition - deferred display fields still re-expand per
render instead of caching - so the message says removal is appropriate
only if that display behaviour is unintended. Sweep of all 462
registered ids: ::split, ::array, ::join carry the parse-inert tag
(left in place pending the display-refresh decision).
Incidental finds from the sweep: the malformed ::tcl_startOfNextWord
tclcore definition (double-quoted -help with unescaped quotes in the
embedded man-page example) failed to resolve at all - now braced per
the define quoting rules, 'i tcl_startOfNextWord' works; and resolve's
malformed-record error path printed dev diagnostics (full records dump
+ ::testrecord global) - commented per the stray-debug convention.
Tests: new args/dynamic.test (warn-once + cached parse, legit-dynamic
round-2 freshness; stderr captured via channel transform). punk::args
203/204 (1 pre-existing skip), punk::ns 57/57. Live-verified: 'i join'
and 'i join test' each emit the warning exactly once (punk902z src).
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
7 changed files with 123 additions and 12 deletions
@ -1,4 +1,4 @@
|
||||
[project] |
||||
name = "punkshell" |
||||
version = "0.12.18" |
||||
version = "0.12.19" |
||||
license = "BSD-2-Clause" |
||||
|
||||
@ -1,6 +1,7 @@
|
||||
0.3.1 |
||||
0.3.2 |
||||
#First line must be a semantic version number |
||||
#all other lines are ignored. |
||||
#0.3.2 - fixed malformed ::tcl_startOfNextWord definition: its -help was double-quoted but the embedded man-page example contains inner double quotes (set theString "The quick brown fox" / puts "Word start index: ..."), so the value terminated early and the definition failed to resolve at all (punk::args::resolve 'bad optionspecs line' error; 'i tcl_startOfNextWord' broken). The -help is now braced (fully literal per the define quoting rules - inner quotes and brackets safe, text verbatim, example braces balance). Found by a bad-@dynamic sweep across all 462 registered ids (punk::args 0.11.2 work); the same sweep identified the parse-inert @dynamic tags on ::split, ::array and ::join (each now warns once per interp) - left in place pending a decision on their display-refresh semantics (@dynamic still forces display-field re-expansion per render). |
||||
#0.3.1 - authoring-style only (user-directed): the ::after id-shape harvest is consumed via tstr ${$after_id_prefix} placeholders instead of the 0.3.0 build-time %AFTERIDPREFIX% string map - the harvest variable now lives in the argdoc namespace (the defspace registered PUNKARGS definitions resolve placeholders in when an argdoc child exists; see the punk::args::define 'Interpolation and the defspace' help section), which makes plain placeholders work in both the -type parse field (expanded at first resolve) and the -help display fields (expanded at display time). This module showcases the tstr style; string map remains reserved for cases where build-time substitution is genuinely necessary. Behaviour identical to 0.3.0 (resolved -type, form discrimination, parity pins and help renders re-verified). |
||||
#0.3.0 - ::after cancel-id discrimination (user-directed 2026-07-13): the cancelid and info forms' id argument is typed stringstartswith(<prefix>) with the prefix harvested from the RUNNING interpreter at define time (safe create+cancel probe 'after 999999 {}' / 'after cancel $id' - G-054 technique; prefix is after# on 8.6.11 and 9.0.3, hardcoded after#%d in tclTimer.c), substituted into the definition via a build-time %AFTERIDPREFIX% string map (CORRECTED finding: parse-field tstr IS expanded for registered PUNKARGS definitions, but in the argdoc subnamespace when one exists - a variable set in the parent namespace is unresolvable there and the param is left silently literal; build-time substitution sidesteps the defspace subtlety). -typesynopsis id keeps the synopsis rendering as the man page's 'id'. Effect under G-041 form candidacy: 'after cancel <non-id-shaped-word>' resolves cleanly to the cancelscript form matching real semantics (real 'after cancel' with a non-id is a silent script-match no-op), and 'after info <non-id>' is model-rejected where real errors at runtime (parity-true); an id-SHAPED word after cancel remains truthfully ambiguous (cancelid+cancelscript) - real Tcl resolves that junction by id liveness at runtime, which no static type expresses; dead-id over-acceptance on 'after info' recorded as the accepted runtime-liveness boundary. Both ids gain man-page-derived -help text. Parity pins added in tclcoreparity.test (id-shape harvest agreement, cancel discrimination incl the liveness ambiguity, info error-vs-ok parity + accepted dead-id divergence). |
||||
#0.2.0 - G-054: 'string is' class choices (and the generated per-class virtual docids) are harvested from the RUNNING interpreter at define time instead of a hand-maintained list - a deliberately invalid probe of the builtin yields the authoritative class set from its error message (8.6: 21 classes, no dict; unreleased 8.7: +dict +unicode; 9.0: +dict, unicode removed), fixing accept/reject drift such as the doc wrongly accepting 'string is dict' under 8.6. Hand-written man-page descriptions apply only to classes the runtime accepts (generic label for unrecognized future classes); static version notes on dict (not in 8.6) and unicode (unreleased 8.7 only). Parity pinned by tclcoreparity.test with expectations derived from the live interpreter (green on 8.6.13, 8.7a6, 9.0.3) |
||||
|
||||
@ -0,0 +1,96 @@
|
||||
package require tcltest |
||||
|
||||
package require punk::args |
||||
|
||||
#@dynamic definition resolution behaviour - added 2026-07-13 (agent, user-reported |
||||
#repeated bad-@dynamic warnings from 'i join'). |
||||
#A definition marked @dynamic whose round-1 tstr output contains no round-2 ${...} |
||||
#parameters is a "bad @dynamic tag": the tag buys nothing (display-field ${...} is |
||||
#deferred regardless - see the define help's interpolation section). resolve warns - |
||||
#and since 2026-07-13 caches the round-1 output as a zero-param unresolved entry, so |
||||
#the warning emits ONCE per definition and later resolves skip the re-mask/re-tstr of |
||||
#round 1 (the same round-1 freezing legitimately dynamic definitions get). |
||||
#Legitimately dynamic definitions are unaffected: round-2 parameters re-substitute on |
||||
#every resolve. |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
variable common { |
||||
set result "" |
||||
} |
||||
|
||||
#capture stderr written during script (chan push transform; popped in all paths) |
||||
proc capture_stderr {script} { |
||||
variable caught "" |
||||
chan push stderr {apply {{cmd chan args} { |
||||
switch -- $cmd { |
||||
initialize {return {initialize finalize write}} |
||||
finalize {return} |
||||
write {append ::testspace::caught [lindex $args 0]; return ""} |
||||
} |
||||
}}} |
||||
set code [catch {uplevel 1 $script} r ropts] |
||||
chan pop stderr |
||||
if {$code} { |
||||
return -options $ropts $r |
||||
} |
||||
return $caught |
||||
} |
||||
|
||||
test dynamic_bad_tag_warns_once {a @dynamic definition with no round-2 parameters warns once and is served from the round-1 cache thereafter}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@dynamic |
||||
@id -id ::testspace::baddyn |
||||
@cmd -name testspace::baddyn -help "display-only placeholder ${$display_only_content}" |
||||
@values -min 1 -max 1 |
||||
v -type string |
||||
} |
||||
set errout [capture_stderr { |
||||
punk::args::get_spec ::testspace::baddyn |
||||
punk::args::get_spec ::testspace::baddyn |
||||
punk::args::parse {hello} withid ::testspace::baddyn |
||||
punk::args::parse_status {hello} withid ::testspace::baddyn |
||||
}] |
||||
lappend result [regexp -all {bad @dynamic tag for id:::testspace::baddyn} $errout] |
||||
#the cached round-1 output still parses correctly |
||||
set argd [punk::args::parse {world} withid ::testspace::baddyn] |
||||
lappend result [dict get $argd values v] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::baddyn 1 |
||||
}\ |
||||
-result [list 1 world] |
||||
|
||||
test dynamic_legit_round2_stays_live {a legitimately dynamic definition re-substitutes round-2 parameters on every resolve, with no bad-tag warning}\ |
||||
-setup $common -body { |
||||
variable dynchoices {a b} |
||||
proc ::testspace::choices_now {} { |
||||
variable dynchoices |
||||
return $dynchoices |
||||
} |
||||
namespace eval ::testspace::dynfix { |
||||
set DYN_CHOICES {${[::testspace::choices_now]}} |
||||
punk::args::define { |
||||
@dynamic |
||||
@id -id ::testspace::gooddyn |
||||
@cmd -name testspace::gooddyn -help "legit dynamic" |
||||
@values -min 1 -max 1 |
||||
v -choices {${$DYN_CHOICES}} |
||||
} |
||||
} |
||||
set errout [capture_stderr { |
||||
lappend result [dict get [punk::args::get_spec ::testspace::gooddyn] FORMS _default ARG_INFO v -choices] |
||||
set ::testspace::dynchoices {c d} |
||||
lappend result [dict get [punk::args::get_spec ::testspace::gooddyn] FORMS _default ARG_INFO v -choices] |
||||
}] |
||||
lappend result [regexp -all {bad @dynamic tag} $errout] |
||||
}\ |
||||
-cleanup { |
||||
namespace delete ::testspace::dynfix |
||||
rename ::testspace::choices_now {} |
||||
punk::args::undefine ::testspace::gooddyn 1 |
||||
}\ |
||||
-result [list {a b} {c d} 0] |
||||
} |
||||
tcltest::cleanupTests ;#needed to produce test summary line. |
||||
Loading…
Reference in new issue