diff --git a/CHANGELOG.md b/CHANGELOG.md index c95ce299..9a5c4bb0 100644 --- a/CHANGELOG.md +++ b/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 (#.fauxlink|.fxlnk, target relative to src/project_layouts); the bespoke @.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. diff --git a/punkproject.toml b/punkproject.toml index f582831d..bdca30f3 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.12.40" +version = "0.12.41" license = "BSD-2-Clause" diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 9c3d998e..daea85a5 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/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] diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index d9800fc5..a4e64d10 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/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). diff --git a/src/tests/modules/punk/args/testsuites/args/define.test b/src/tests/modules/punk/args/testsuites/args/define.test index 27c74481..871023a9 100644 --- a/src/tests/modules/punk/args/testsuites/args/define.test +++ b/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.