Browse Source
'i ::tcl::prefix' emitted 12x "warning: update_definitions received unqualified ns: tcl" on stderr (6x for subcommand renders). Root cause: the tclcore moduledoc's @dynamic ::tcl::prefix definition passed the ensemble name to punk::args::ensemble_subcommands_definition UNQUALIFIED ('tcl::prefix'). The generator's space-form id_checks derive namespaces via 'namespace qualifiers' - for a relative multi-component name that yields a non-empty UNQUALIFIED namespace ('tcl') which the empty-qualifier guard ('' -> ::) never catches - and update_definitions warned once per subcommand per resolve. @dynamic definitions re-expand on every resolve and the cmdhelp pipeline resolves the id four times (parse_status spec fetch + its internal parse, cmdhelp's get_spec, arg_error/synopsis), so 4 resolves x 3 subcommands = the reported 12. tcl::prefix was the ONLY affected ensemble: all tclcore ensemble docs pass unqualified names, but the single-component ones (info/dict/file/ namespace/array/encoding/zipfs) derive an empty qualifier which was already mapped to ::. Both fixes: - tclcore call site now passes ::tcl::prefix. - ensemble_subcommands_definition normalizes its ensemble argument to fully-qualified in the CALLER's context (uplevel namespace which, :: prepend fallback) before deriving anything - covering any future relative multi-component caller. Verified: zero warnings at both depths; rendered help byte-identical (6301/5585 chars); generator snippet identical for qualified vs unqualified input. New pin ensembledef.test ensembledef_unqualified_ensemble_name_no_warnings (fixture two-component ensemble called relatively under stderr capture; capture_stderr helper added to the file). src/tests/modules/AGENTS.md index updated (ensembledef.test now listed). G-175 drafted (proposed): goals/G-175-punkargs-dynamic-resolve-multiplicity.md - the warning was the only signal of the 4x-per-render @dynamic resolve multiplicity; the goal records the verdict question (render-scoped single resolution vs pinned accept), the attributed resolve sites, the measured cost floor (redundant builder calls ~3ms on 'i ::tcl::prefix', ~22ms on 'i ::dict' steady-state; the full argdata re-processing share is unmeasured - first investigation step), and candidate mechanisms smallest-first. GOALS.md indexed. punk::args 0.25.1 -> 0.25.2, punk::args::moduledoc::tclcore 0.4.1 -> 0.4.2, project 0.62.1 -> 0.62.2 + CHANGELOG (user-visible repl stderr noise fix). Suites: testbody_lint 1688 clean; goals_lint clean (79 active / 96 archived); modules tree 1335 total / 1324 pass / 11 constraint-skipped / 0 fail (zig-built tclsh90s 9.0.5); make.tcl projectversion consistency + staleness OK. Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
10 changed files with 179 additions and 5 deletions
@ -0,0 +1,92 @@
|
||||
# G-175 @dynamic resolve multiplicity - one render, one resolution (verdict + optional landing) |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/args-999999.0a1.0.tm (resolve @dynamic cache-skip, by-id entry points get_spec/parse_status/arg_error/synopsis); src/modules/punk/ns-999999.0a1.0.tm (cmdhelp render pipeline - the four independent by-id fetches); src/tests/modules/punk/args/testsuites/args/dynamic.test + src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test (once-per-render counter pins + cross-render freshness pins, if the landing arm is taken) |
||||
Goal: a verdict with evidence on whether re-resolving @dynamic definitions multiple times within ONE user-level render (the 'i <cmd>' pipeline) is architecturally required - and if not, either a render-scoped single-resolution mechanism lands (each @dynamic definition's substitution scripts run once per cmdhelp invocation, byte-identical output, cross-render freshness untouched) or the per-pass re-resolution is pinned as a recorded decision with its measured cost, the rationale written in this file. |
||||
Acceptance: the resolve-level cost share of the multiplicity is measured (not just the subcommand-builder share - each pass re-runs full argdata processing of the definition text) and recorded here; the by-id fetch sites in the cmdhelp pipeline are enumerated with which could accept a pre-resolved spec; a decision is recorded - EITHER a landed mechanism with pins (a counter fixture proving a dynamic definition's substitution scripts run exactly once per cmdhelp render; byte-identical render output; a mutating fixture proving a SECOND render still observes changed state - the @dynamic freshness contract across renders survives) OR a pinned-as-accepted rationale; punk/args and punk/ns suites pass under the canonical runtests interpreter either way. |
||||
|
||||
## Context |
||||
|
||||
Surfaced 2026-08-08 while root-causing the 'i ::tcl::prefix' unqualified-ns stderr |
||||
warnings (12 = 4 resolves x 3 subcommands - the warning fix landed separately, |
||||
punk::args 0.25.2 / tclcore 0.4.2). The warning was the only SIGNAL that the |
||||
::tcl::prefix @dynamic definition was being re-resolved 4 times in a single 'i' |
||||
render; with it fixed, the multiplicity is silent. |
||||
|
||||
Probe evidence (trace on the tclcore tclprefix_subcommands builder, tclsh90s, |
||||
2026-08-08 - line anchors point-in-time): |
||||
|
||||
RESOLVE#1 via resolve < get_spec < parse_status < cmdhelp |
||||
RESOLVE#2 via resolve < get_dict < parse < parse_status < cmdhelp |
||||
RESOLVE#3 via resolve < get_spec < cmdhelp |
||||
RESOLVE#4 via resolve < get_spec < synopsis < arg_error < cmdhelp |
||||
|
||||
Four independent by-id fetches: parse_status resolves twice on its own (spec fetch |
||||
plus its internal parse), cmdhelp fetches the spec for rendering, and the |
||||
arg_error/synopsis path fetches again. Each fetch is correct in isolation; nothing |
||||
shares the resolved spec across the pipeline. |
||||
|
||||
Architecture (args-999999.0a1.0.tm as at 2026-08-08): resolve consults the |
||||
rawdef_cache_argdata cache ONLY for non-dynamic definitions (`if {!$is_dynamic}` |
||||
~args:2175) - a @dynamic definition re-runs its tstr command substitutions AND the |
||||
full argdata processing on every resolve. That per-resolve re-expansion is the |
||||
documented contract (define -help: "@dynamic definitions re-expand on every |
||||
resolve" / "Use @dynamic only when the value can change between resolves") and is |
||||
what keeps e.g ensemble subcommand choice-tables live when ensembles are extended |
||||
at runtime. ALL tclcore ensemble docs are @dynamic (info, encoding, dict, file, |
||||
namespace, array, zipfs, tcl::prefix), so every 'i <core-ensemble>' pays the |
||||
multiplicity. |
||||
|
||||
Measured cost (steady-state second render, warmed lazy loads, tclsh90s): |
||||
|
||||
i ::tcl::prefix total ~196ms; subcommand-builder 4 calls = 4.4ms (2%) |
||||
i ::dict total ~701ms; subcommand-builder 4 calls = 29.8ms (4%) |
||||
|
||||
The builder share is the FLOOR of the waste (3 redundant calls of 4): the full |
||||
argdata re-processing per redundant resolve is not yet isolated - measuring it is |
||||
the first investigation step. Note the totals themselves: the multiplicity sits |
||||
inside renders already costing 200-700ms. |
||||
|
||||
## Approach |
||||
|
||||
1. Measure the resolve-level share: time resolve for a representative @dynamic |
||||
definition (tcl::prefix small, dict large) and multiply out the redundant |
||||
passes; record here. If the whole-render saving is negligible even for dict, |
||||
the pin-as-accepted arm is available cheaply. |
||||
2. Enumerate the by-id entry points in the cmdhelp pipeline (parse_status x2, |
||||
get_spec, arg_error/synopsis) and determine which can accept a pre-resolved |
||||
spec or share a resolution without API breakage (several already have |
||||
spec-shaped internal forms). |
||||
3. Candidate mechanisms, smallest-first: |
||||
a. cmdhelp resolves once and passes the resolved spec down (plumbing change, |
||||
punk::ns-side; punk::args API additions only where a by-id entry lacks a |
||||
by-spec form). |
||||
b. a render-scoped resolution context in punk::args (explicit begin/end or |
||||
token-passed), dynamic argdata memoized within the context only. |
||||
c. epoch/generation-keyed short cache - REJECT-by-default: invalidation |
||||
semantics are exactly the hard part, and the @dynamic contract is |
||||
per-resolve freshness. |
||||
4. Whichever arm: pin the outcome (once-per-render counter fixture + byte-identical |
||||
render + cross-render freshness for the landing arm; measured-cost rationale |
||||
recorded here for the accept arm). |
||||
|
||||
## Notes |
||||
|
||||
- Risk to respect: a @dynamic substitution script may observe state mutated |
||||
MID-render (in principle even by an earlier substitution). Render-scoped |
||||
memoization changes that observable - almost certainly acceptable (a render is |
||||
one logical moment), but it is the semantic delta to state explicitly in the |
||||
decision. |
||||
- The stale-colour REVIEW comment beside the non-dynamic cache consult |
||||
(~args:2176 "don't use cached version if 'colour off' vs 'colour on' |
||||
different...") shows even the static cache has known freshness caveats - any |
||||
new memoization should not widen that class. |
||||
- parse_status resolving twice by itself (spec + its internal parse) may be worth |
||||
collapsing independently of the dynamic question - it halves the multiplicity |
||||
for ALL definitions, static ones included (static hits the cache, so the win |
||||
there is small but the call-shape cleanup may pay anyway). |
||||
- Related arcs: the G-046 display-field masking (achieved, see |
||||
goals/archive/G-046-punkargs-deferred-help-and-fixes.md - expensive -help |
||||
processing deferred to display time) already splits parse-relevant from |
||||
display-relevant work - a render-scoped mechanism should compose with it, not |
||||
duplicate it. |
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue