12 KiB
G-073 punk::args unavailable choices: displayed with notes and prefix-reserving, but rejected with a tailored message
Status: achieved 2026-08-05 Scope: src/modules/punk/args-999999.0a1.0.tm (spec key, choiceword_match pool, choices rendering in arg_error table+string renderers, validation message); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm ('string is' forward-class adoption + per-class virtual id); src/tests/modules/punk/args/testsuites/args/ (new suite + tclcoreparity.test exemption) Goal: an argument definition can declare choices that are recognised but not available in the current runtime (new key, candidate spelling -choiceunavailable ): they display among the choices with their ordinary -choicelabels notes (visually distinguished), participate in prefix disambiguation like -choiceprefixreservelist phantoms, and are rejected at parse with a tailored message identifying the unavailability rather than the generic listed-values error - adopted by the tclcore 'string is' model so that Tcl 8.6 shows the dict class annotated "(class not present in Tcl 8.6)", rejects 'string is dict' informatively, treats 'string is di' as ambiguous (deliberately stricter than real 8.6, preparing 8.6 users for 9.x where dict/digit/double collide), and documents 'i string is dict' via a per-class virtual id generated from the static description. Acceptance: a definition using the new key renders unavailable entries distinguishably among the choices (mechanism decided in the work: dedicated group heading, dim/warn styling, or marker) with their -choicelabels notes, in both the table and string renderers, and synopsis choice-literal display excludes them; parse/parse_status reject an unavailable choice word - exact, or a unique prefix resolving only to it - with a message identifying it as recognised-but-unavailable rather than the generic choice error; unavailable entries participate in prefix disambiguation via choiceword_match (a prefix shared between an available and an unavailable choice is rejected as ambiguous) and are honoured identically by the punk::ns doc walk and the G-071 allocation screen (parity free via the shared resolver - no second matching rule); value-in-effect/goodarg highlighting never marks an unavailable entry; the tclcore 'string is' definition adopts the key on runtimes lacking known forward classes (dict on 8.6; the forward list is explicitly curated in the definition - unicode deliberately excluded as never-released), with a per-class virtual id generated from the static description so 'i string is dict' documents the class with its note on 8.6; the deliberate strictness divergence (real 8.6 accepts 'string is di' as digit; the model rejects it as ambiguous) is recorded in tclcoreparity.test as a user-sanctioned exemption class rather than silently special-cased, with full-word behaviour staying parity-true on both versions; definitions without the key behave unchanged (full punk::args and punk::ns suites pass).
Context
User requirement 2026-07-12: forward compatibility for the 8.6-vs-9 'string is dict' difference. The G-054 harvest (archived) makes the class choices accurate per-runtime, so 8.6 neither displays nor mentions dict - but an 8.6 programmer who habituates to 'string is di' (unique prefix of digit on 8.6) writes code that breaks on 9.x where dict makes 'di' ambiguous. The requirement: 8.6 should SEE dict (with an unavailability note - the description text with its "(class not present in Tcl 8.6)" annotation already exists in the static string_is_class_descriptions dict, it just never renders on 8.6), have dict participate in prefix disambiguation (so 'di' is ambiguous on 8.6 too), and still reject dict as an actual argument (real 8.6 rejects it).
Capability assessment (2026-07-12/13): the display layer is ready - choicelabels notes, choicegroups headings and choiceinfo markers all exist; the gap is purely that choices-cell membership implies parse acceptance under -choicerestricted 1. The trie mechanics exist as -choiceprefixreservelist (phantom prefix-calculation members, G-040), but reservelist phantoms deliberately do not display (the punk::help c/to/tc phantoms must stay invisible) and reject with the generic choice error. Hence a first-class concept: ~80% parse semantics, ~20% display, riding the existing rendering pipeline.
Approach
- New per-argument key (spelling decided in the work; candidate -choiceunavailable ). Entries must not also appear in -choices/-choicegroups (definition error). Notes come from ordinary -choicelabels entries keyed by the unavailable word.
- choiceword_match: unavailable entries join the prefix pool like reservelist members, but a match landing on one returns a distinct indication so validation can emit the tailored message ("'dict' is a recognised choice but not available in this runtime - see its note") instead of the generic listed-values error. The shared-resolver principle keeps parse, parse_status, the G-071 allocation screen and the (G-051) doc walk automatically consistent.
- Display: render unavailable entries among the choices with their notes, visually distinguished (group heading vs dim/warn styling vs marker - decided in the work with display-cost in mind); excluded from value-in-effect and goodarg marking; excluded from synopsis choice-literal alternates.
- tclcore adoption: the 'string is' define-time harvest computes unavailable = curated_forward_list - live_classes (forward list: dict; unicode excluded). The per-class virtual id loop extends to unavailable classes using the static description so 'i string is dict' works as documentation on 8.6.
- Parity: tclcoreparity.test gains a recorded exemption class for prefix-collision strictness (model stricter than real 8.6 for 'di'-shaped prefixes by user direction); full-word probes stay strict parity.
Notes
- Related: G-040 (choiceword_match single-resolver principle and the reservelist precedent), G-051 (doc walk consumes the resolver - achieved 2026-07-13), G-071 (allocation screen consumes the resolver - achieved 2026-07-12), G-054 archived (harvest + the parity pins this goal amends), G-055 (version-adaptive definition technique; its workflow guidance gains the forward-class pattern once proven here).
- The reject-message wording should steer the user: name the note/annotation and the version boundary, not just "invalid".
- Related: G-082 (achieved 2026-07-14 - goals/archive/G-082-punkargs-error-selection.md) - single-form error selection now surfaces an optional argument's specific choice/type rejection instead of the generic overflow error. The seam this goal wants exists: private::get_dict_can_assign_value returns a 'rejection' record (kind choice|type + word/choices/type) and get_dict_form's alloc_rejections store is consulted at the overflow raise sites - the tailored unavailable-choice message can extend the record's kind vocabulary for recognised-but-unavailable words rather than adding a second mechanism. (Outcome: exactly that landed - kind 'unavailable' records from the singular-clause screen, preferred at both overflow raise sites.)
Progress
- 2026-08-05 (achieved, one session from activation): punk::args 0.16.0 + punk::args::moduledoc::tclcore 0.4.0 + punk::ns 0.9.3 (source modules). Key spelling as proposed: -choiceunavailable (per-argument only, like -choicealiases; requires -choices/-choicegroups). Display mechanism decided: DEDICATED GROUP HEADING "Unavailable (recognised, not selectable here)" - a rendering-side injection into the choicegroups walk, so the table renderer (sub-table title), the string renderer (Group heading + bullets), choicelabels notes, choiceinfo markers, trie shortest-prefix marking and value-in-effect wrapping all apply without new render paths; entries join the display prefix-calc pool mirroring their parse-time reserving effect. Resolver: choiceword_match gains an optional 8th argument (unavailablelist) and an 'unavailable' result key - matched stays 0 so matched-only consumers are unaffected; exact, nocase-exact and unique-prefix landings carry the entry as canonical; a prefix shared with an available choice stays a plain ambiguous non-match. Rejection: PUNKARGS VALIDATION {choiceunavailable choices } with -badarg/-badval; the message (private::unavailable_choice_msg, shared by every raise surface) names the supplied word, the resolved entry, its -choicelabels note (the steering "why" - e.g the version boundary) and the available values; ambiguous-prefix messages annotate unavailable longer matches '(recognised but unavailable)'. G-071 screen honours the list; optional-arg landings ride the G-082 rejection-record seam to both overflow raise sites; required args stay unscreened (final validation reports identically); -choicerestricted 0 keeps full passthrough. Resolve-time validation: collision with choices/alias-names/reservelist rejected; -default may not be unavailable. tclcore: curated forward list {dict} ('unicode' deliberately excluded - unreleased-8.7-only); unavailable = forward minus harvested-live at define time; the choicelabels build and per-class virtual-id loop extend to unavailable classes from the same static descriptions ('i string is dict' documents on 8.6). punk::ns: both cmd_traverse resolver call sites pass the key - doc-walk parity ('string is di' on 8.6 resolves no docs where it previously resolved digit).
- Verification (runtests.tcl): new choiceunavailable.test (10 tests - resolver
matrix incl 7-arg compat, resolve-time validation errors, tailored rejection
in leader/value/option positions incl prefix landing and note content,
shared-prefix ambiguity + annotation, G-082 overflow seam, restricted-0
passthrough, parse_status classification, both renderers' group + note,
synopsis exclusion, parse-result purity); usagemarking.test +
usagemarking_goodchoice_unavailable_never_marked (goodchoice marking never
touches unavailable entries, both renderers); tclcoreparity.test + forward
-class adoption invariants and the USER-SANCTIONED 'di' strictness exemption
(live-derived; the one deliberate divergence arm on 8.6, full words
parity-true) with the pre-G-073 version_notes pin ("labels appear exactly
when the class is live") deliberately flipped (dict's label now renders on
every runtime). Green: the three suites 37/37 on native Tcl 8.6.10
(bin/runtime/win32-ix86/tclsh8.6.10-luck-zip.exe), 8.7a6 and 9.0.3; full
punk/args subtree 293 total / 290 passed / 3 intended skips / 0 failed on
9.0.3 AND 8.7; modules/punk/*** sweep on 9.0.3 green except the three
documented pre-existing box-baseline failures (ansi grepstr x2, ns nslist x1
- unrelated); direct 8.6 probes verified the harvest (unavailable={dict}), the tailored rejection + note render, both renderers' group display, and the doc walk ('di' resolves nothing, exact 'dict' resolves the virtual id). Modern runtimes compute an empty unavailable list - suites pin behaviour unchanged.
Follow-ons
Follow-on: G-055's moduledoc workflow guidance gains the now-proven forward-class authoring pattern (-choiceunavailable + curated forward list + static-description labels, per the 'string is' adoption) => goal G-055 Follow-on: the doc walk resolves only the EXACT unavailable word to its virtual docid - a unique prefix landing (e.g 'i string is dic' on 8.6) conservatively resolves no documentation where parse gives the tailored "resolves to 'dict'" report => goal G-166 Follow-on: an optional -choicemultiple argument's overflow rejection keeps the generic choice record even when a member word landed on an unavailable name (only the singular-clause screen records kind 'unavailable') => open