Browse Source

punk::args 0.12.4: collision-aware resolve miss path (undefined-id warning retired)

resolve's first-sighting path for a rawdef whose @id is already registered
with different content now purges the id's existing cache entries (quiet
undefine) before registering, mirroring define's re-creation behaviour.
Previously it repointed id_cache_rawdef while stale rawdef_cache_about /
rawdef_cache_argdata entries lingered - two rawdefs claimed the same id and
a subsequent define with the original text was a silent no-op (stale
cache-about hit), leaving withid lookups stuck on the inline definition.
The disabled blanket "called with undefined id" stderr warning is removed -
it flagged supported inline usage (parse ... withdef, direct resolve
calls). Guard checks id_cache_rawdef directly (id_exists also matches
aliases, where the rawdef lookup would error). Known residual: the miss
path still records no -defspace for @dynamic definitions first seen via
resolve.

New define.test pin define_resolve_id_shadow (characterized pre-fix: the
re-define no-op reproduced with the purge disabled). punk::args suite green
under Tcl 9.0.3 (228 passed, 0 failed). punkshell 0.12.41.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 5 days ago
parent
commit
c55225a392
  1. 4
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 8
      src/modules/punk/args-999999.0a1.0.tm
  4. 3
      src/modules/punk/args-buildversion.txt
  5. 41
      src/tests/modules/punk/args/testsuites/args/define.test

4
CHANGELOG.md

@ -5,6 +5,10 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.12.41] - 2026-07-17
- punk::args 0.12.4: resolve's undefined-id miss path is now collision-aware - an inline definition (`parse ... withdef`, direct resolve) whose @id is already registered with different content quietly purges the id's existing cache entries before registering (matching define's re-creation behaviour), instead of repointing the id while stale cache entries lingered (previously a subsequent define with the original text was a silent no-op, leaving withid lookups stuck on the inline definition). The disabled blanket 'called with undefined id' stderr warning removed (it flagged supported usage). Known residual: the miss path still records no -defspace for @dynamic definitions first seen via resolve. New define.test pin (define_resolve_id_shadow, characterized pre-fix).
## [0.12.40] - 2026-07-17
- G-087 stage 2: layout refs migrated to fauxlink files. fauxlink 0.2.0 (vendored from upstream) implements fauxlink::link_as - the encoder counterpart of resolve with a round-trip guarantee - and the migration script used it to generate every new ref name. punk::cap::handlers::templates 0.2.0 resolves layout refs via the fauxlink module (<alias>#<encodedtarget>.fauxlink|.fxlnk, target relative to src/project_layouts); the bespoke <alias>@<target>.ref grammar is retired (.ref files no longer recognised). Layout refs must carry a non-empty alias - alias-less (leading-#) refs are warned about and skipped (punkcheck excludes leading-# filenames). All 7 live refs renamed (alias-less ones gained their target-tail aliases); visible layout names unchanged. punk::mix::templates 0.1.5 carries the migrated module-shipped ref. Verified end-to-end: dev project.new resolves the default punk.project layout through fauxlink refs and materializes a full project.

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.12.40"
version = "0.12.41"
license = "BSD-2-Clause"

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

@ -2006,9 +2006,13 @@ tcl::namespace::eval punk::args {
set defspace [dict get $cinfo -defspace]
}
} else {
#should we really be resolving something that hasn't been defined?
#first sighting of this exact rawdef text (e.g inline 'parse ... withdef' or direct resolve call)
set id [rawdef_id $args]
puts stderr "Warning: punk::args::resolve called with undefined id:$id"
if {[dict exists $id_cache_rawdef $id] && [dict get $id_cache_rawdef $id] ne $args} {
#id already registered with different content - purge existing cache entries for the id
#so we don't leave two rawdefs claiming the same id (matches define behaviour)
undefine $id 1
}
set is_dynamic [rawdef_is_dynamic $args]
#-defspace ???
dict set rawdef_cache_about $args [dict create -id $id -dynamic $is_dynamic]

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

@ -1,6 +1,7 @@
0.12.3
0.12.4
#First line must be a semantic version number
#all other lines are ignored.
#0.12.4 - resolve: retired the (already disabled) blanket 'called with undefined id' stderr warning - it fired on the supported inline usages (parse ... withdef, direct resolve calls) where a first-sighting cache miss is by design. In its place the miss path is now collision-aware: a first-seen rawdef whose @id is already registered with different content purges the id's existing cache entries (quiet undefine) before registering, mirroring define's re-creation behaviour - previously it repointed id_cache_rawdef while leaving the old rawdef_cache_about/rawdef_cache_argdata entries in place, so two rawdefs claimed the same id and a subsequent define with the original text was a silent no-op (stale cache-about hit) leaving withid lookups stuck on the inline definition. Guard tests id_cache_rawdef directly (id_exists also matches aliases, where the rawdef lookup would error). Known residual (comment marker retained): the miss path still records no -defspace, so @dynamic definitions first seen via resolve lack a defining namespace for re-substitution. New test define.test define_resolve_id_shadow (characterized pre-fix: re-define no-op reproduced with the purge disabled).
#0.12.3 - fixed the stringstartswith(*) arm of get_dict_can_assign_value's multi-member tail-clause reservation walk: it matched against $tp (the type string) instead of $rv (the candidate value), so for a later optional clause mixing literal(...) and stringstartswith(...) members, genuinely matching trailing words were never reserved from a greedy -multiple argument (the optional tail clause was silently lost), while a prefix text that happened to prefix the literal type string (e.g 'str' vs 'stringstartswith(str)') wrongly reserved non-matching words and raised a spurious toomanyarguments overflow. Found flagged-only in the 0.12.2 comment review; characterized pre-fix then pin flipped (allocation.test allocation_tailclause_ssw_reservation + literal-only and no-match guards). Note the walk still only runs when the tail clause's typelist contains a literal* member (the pre-existing acknowledged approximation) - a stringstartswith-only tail clause remains unreserved.
#0.12.2 - comment/documentation hygiene pass (no behaviour change). Removed ~1100 lines of superseded commented-out code: the old-signature private::check_clausecolumn, the abandoned _check_clausecolumn2 'interim version' experiment (its one keeper idea - collect per-clause validation results and report at end instead of raising on first failure - recorded in an in-place note for G-072-era rework), the pre-parsekey opts ordering loop, a disabled XXXliteral* switch arm in get_dict_can_assign_value's multi-member clause walk (open item noted: literalprefix() members unhandled there), and misc debug remnants. Expanded the leaders/values loop MAINTENANCE notes into a real refactor assessment: shared clause-assignment helper extraction is indicated (drift is real - G-082 duplicated its selection block, parsekey idents exist only values-side) but deferred to ride the G-084 leaders/values parsekey parity work under the G-046 hot-path constraints; until then edits to one loop must be mirrored or justified. Documentation corrections: get_dict PUNKARGS/doctools return shape now lists the real result keys (leaders opts values received solos multis id form + formstatus when candidacy ran); directive lists updated beyond @cmd/@leaders/@opts/@values; removed false pre-modern claims ('only supports -flag val pairs, not solo options', 'only the last value is used'); fixed the broken parse -help example (dict exists bracketing) and the module-header dofilestuff example (stray trailing 1 after -type existingfile); assorted typos. Goal cross-references annotated in code comments (G-053 -multiple occurrence ranges, G-084 values/leaders parsekey, corrected test-name refs to parsekey_repeat_ordering). Flagged (comment only, behaviour unchanged, no pins exist): suspected bug in the multi-member clause stringstartswith( arm - it string-matches against $tp (the type) instead of $rv (the value), so it always takes the success path.
#0.12.1 - G-082 single-form parse error selection: a word rejected by an OPTIONAL argument's allocation screen (G-071 restricted-choice screen, or the basic-type screen: int/double/bool/number/dict) that subsequently overflows the argument list now reports the specific rejection - choiceviolation naming the word and the allowed choices (with -badarg/-badval and the same message template as final validation's report for required args), or typemismatch naming the declared type - instead of the generic 'Received more values than can be assigned' toomanyarguments (regression vs 0.5.0 found during G-030 make.tcl dogfooding; the -optional 0 authoring workaround is no longer required). Mechanism: get_dict_can_assign_value returns a 'rejection' record (single-member clauses only; literal-category mismatches deliberately not recorded - overflow stays the clearer report for syntax-word shapes like lseq 'to'), the three allocation call sites (leader/value split trial, leaders loop, values loop) collect first-rejection-per-word into a per-form store, and the two overflow raise sites prefer a recorded rejection for the unassignable word. Genuinely-surplus words (all optionals satisfied) still report toomanyarguments; words consumed by a later slot parse unchanged; multiform selection (rank_form_failures) unchanged - per-form failures just carry the more specific class. New testsuite errorselection.test (characterized pre-fix, pins flipped deliberately).

41
src/tests/modules/punk/args/testsuites/args/define.test

@ -125,5 +125,46 @@ namespace eval ::testspace {
-result [list\
+++XXX---YYY {A B C} +++XXX---YYY {X Y Z} OK_define_time_var_match OK_resolve_time_2_greater
]
test define_resolve_id_shadow {Test inline withdef reusing a registered @id takes over the id cleanly (stale cache entries purged)}\
-setup $common -body {
punk::args::define {
@id -id ::testspace::shadow1
@values
param -type string -default registered
}
set argd [punk::args::parse {} withid ::testspace::shadow1]
set vals [dict get $argd values]
lappend result [dict get $vals param]
#supported usage: inline definition carrying an @id that is already registered
set argd [punk::args::parse {} withdef {
@id -id ::testspace::shadow1
@values
param -type string -default inline
}]
set vals [dict get $argd values]
lappend result [dict get $vals param]
#last definition wins - id lookups now see the inline definition
set argd [punk::args::parse {} withid ::testspace::shadow1]
set vals [dict get $argd values]
lappend result [dict get $vals param]
#re-define with the original definition text - must take the id back
#(if resolve left a stale rawdef_cache_about entry for the original text this
# define is a silent no-op and withid would still see the inline definition)
punk::args::define {
@id -id ::testspace::shadow1
@values
param -type string -default registered
}
set argd [punk::args::parse {} withid ::testspace::shadow1]
set vals [dict get $argd values]
lappend result [dict get $vals param]
}\
-cleanup {
punk::args::undefine ::testspace::shadow1 1
}\
-result [list registered inline inline registered]
}
tcltest::cleanupTests ;#needed to produce test summary line.

Loading…
Cancel
Save