Browse Source

tclcore 0.3.0: after cancel-id discrimination via harvested id shape (project 0.12.16)

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# on 8.6.11
and 9.0.3) - substituted via a build-time %AFTERIDPREFIX% string map
because tstr placeholders in non-display fields are not expanded for
plain PUNKARGS definitions (G-046 defers display fields only; noted in
G-055 for the regeneration workflow). -typesynopsis id keeps the
synopsis as the man page's 'id'; both ids gain man-page-derived help.

Effect under G-041 form candidacy: 'i after cancel someid' resolves
cleanly to the cancelscript form, matching real Tcl's silent
script-match no-op; 'after info <non-id>' is model-rejected where real
errors at runtime (parity-true). An id-SHAPED word after cancel stays
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.

Tests: three parity pins added in tclcoreparity.test (id-shape harvest
agreement with live ids, cancel discrimination incl the liveness
ambiguity witness, info error-vs-ok parity + accepted dead-id
divergence). punk::args 201/202 (1 pre-existing skip), punk::ns 57/57.
Live-verified on punk902z src ('i after cancel someid' renders info
scheme).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 2 days ago
parent
commit
5a553b4ff3
  1. 4
      CHANGELOG.md
  2. 8
      goals/G-055-tclcore-regen-workflow.md
  3. 2
      punkproject.toml
  4. 44
      src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm
  5. 3
      src/modules/punk/args/moduledoc/tclcore-buildversion.txt
  6. 63
      src/tests/modules/punk/args/testsuites/args/tclcoreparity.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.16] - 2026-07-13
- tclcore moduledoc 0.3.0: 'after cancel <word>' and 'after info <id>' now discriminate by the after-id shape, harvested from the running interpreter at load time (user-directed). 'i after cancel someid' resolves cleanly to the cancel-script form exactly as real Tcl treats it (silent script-match no-op), and only genuinely id-shaped words ('after#N') report the cancelid/cancelscript ambiguity - the junction real Tcl itself resolves by id liveness at runtime. Parity pins added (tclcoreparity.test) including the accepted dead-id over-acceptance boundary.
## [0.12.15] - 2026-07-13
- G-041 doc surface: the help system presents the command form matching the supplied words - 'i after cancel <id>' presents the cancel form's argument table (candidates are named when no form or several forms match, e.g 'after cancel someid' reports the genuine cancelid/cancelscript ambiguity of the documented forms), and 's after cancel someid' underlines the matching synopsis line(s) (punk::ns 0.5.0). punk::args 0.11.0: the documented @form -synopsis override now renders in synopsis output, and candidate-form ranking recognises -choices discriminators (the tclcore models' subcommand words) via the shared choice resolver.

8
goals/G-055-tclcore-regen-workflow.md

@ -147,7 +147,13 @@ other commands including the multi-form ::after.
after#-shaped - real Tcl resolves by id LIVENESS at runtime (tries id
first, falls back to script match), which no static type expresses; the
G-074 sanction then covers exactly that id-shaped witness rather than the
whole form pair.
whole form pair. APPLIED 2026-07-13 (tclcore moduledoc 0.3.0, user-directed):
both ids typed stringstartswith(<harvested prefix>) via a build-time
%AFTERIDPREFIX% string map (note for the workflow: tstr ${...} in
non-display fields is NOT expanded for plain PUNKARGS definitions - G-046
defers display fields only; parse-field interpolation needs the explicit
tstr wrap, as ::tcl::string::is does, or a string-map token); parity pins
in tclcoreparity.test including the accepted dead-id divergence.
- TIP 746 (user-flagged 2026-07-12,
https://core.tcl-lang.org/tips/doc/trunk/tip/746.md) removes the expr
behaviour from lseq operands in Tcl 9.1: the lseq model's number|expr operand

2
punkproject.toml

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

44
src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm

@ -4527,7 +4527,27 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
############################################################################################################################################################
lappend PUNKARGS [list {
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
#the after-id shape is harvested from the RUNNING interpreter (G-054 technique;
#user-directed 2026-07-13, probe record in goals/G-055) rather than hard-coded:
#a scheduled command's id is "after#N" in all known releases (tclTimer.c
#"after#%d"), and the cancelid/info forms use it as their id discriminator so
#accept/reject parity follows the interpreter these docs load into.
#The probe is safe: create + immediately cancel - no event loop entry, no output,
#no lasting state.
set after_id_prefix "after#"
if {![catch {after 999999 {}} _aip_id]} {
catch {after cancel $_aip_id}
if {[regexp {^(.+#)\d+$} $_aip_id -> _aip_prefix]} {
set after_id_prefix $_aip_prefix
}
}
#the %AFTERIDPREFIX% token below is substituted at build time (string map) because
#tstr ${...} in non-display fields is not expanded for plain PUNKARGS definitions
#(G-046 defers display fields only; the %TYPECHOICES% pattern in punk::args' own
#argdocs is the precedent)
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list [string map [list %AFTERIDPREFIX% $after_id_prefix] {
#test of @form
@id -id ::after
@cmd -name "Built-in: after"\
@ -4570,7 +4590,15 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
@leaders -min 1 -max 1
cancel -choices {cancel}
@values -min 1 -max 1
id
#id typed by the harvested id shape: a word of any other shape belongs to the
#cancelscript form (real 'after cancel <non-id>' is a script-match no-op).
#An id-SHAPED word remains genuinely ambiguous with a script of the same text -
#real Tcl resolves that by id liveness at runtime (tries the id first, falls
#back to script match), which no static type expresses (goals/G-055 record).
id -type stringstartswith(%AFTERIDPREFIX%) -typesynopsis id -help\
"Identifier of the delayed command to cancel.
It must have been the return value from a previous
after command (%AFTERIDPREFIX%N shaped)."
#@form -form {cancelscript} -synopsis "after cancel script ?script...?"
@ -4593,9 +4621,15 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
@leaders -min 1 -max 1
info -choices {info} -choiceprefixreservelist {idle}
@values -min 0 -max 1
id -optional 1
} "@doc -name Manpage: -url [manpage_tcl after]"\
#real 'after info <non-id-shaped-word>' errors at runtime ("event ... doesn't
#exist") - the model's shape rejection keeps error-vs-ok parity
id -optional 1 -type stringstartswith(%AFTERIDPREFIX%) -typesynopsis id -help\
"Identifier of an existing event handler - the
return value from some previous call to after
(%AFTERIDPREFIX%N shaped). It must not have triggered
yet or been canceled."
}] "@doc -name Manpage: -url [manpage_tcl after]"\
{
@examples -help {
This defines a command to make Tcl do nothing at all for N seconds:

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

@ -1,4 +1,5 @@
0.2.0
0.3.0
#First line must be a semantic version number
#all other lines are ignored.
#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 (tstr ${...} in non-display fields is not expanded for plain PUNKARGS definitions - G-046 defers display fields only). -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)

63
src/tests/modules/punk/args/testsuites/args/tclcoreparity.test

@ -147,5 +147,68 @@ namespace eval ::testspace {
-cleanup {
}\
-result [list [expr {"dict" in [live_classes]}] [expr {"unicode" in [live_classes]}] 1]
#--- ::after cancel-id discrimination (user-directed 2026-07-13; probe record in
#--- goals/G-055) - the cancelid/info forms' id is typed by the id shape harvested
#--- from the running interpreter, so 'after cancel <non-id>' resolves to the
#--- cancelscript form exactly as real Tcl treats it (silent script-match no-op).
#added 2026-07-13 (agent, G-055 subject matter, post G-041)
test tclcoreparity_after_id_shape_harvested {the doc model's id type matches the shape of ids the running interpreter actually issues}\
-constraints have_tclcoredocs\
-setup $common -body {
#live id shape (create + immediately cancel - no event loop, no lasting state)
set liveid [after 999999 {}]
after cancel $liveid
set doctype [dict get [punk::args::get_spec ::after] FORMS cancelid ARG_INFO id -type]
lappend result [string match {stringstartswith(*)} $doctype]
set docprefix [string range $doctype 17 end-1]
lappend result [string match "${docprefix}*" $liveid]
#info form uses the same shape
lappend result [string equal $doctype [dict get [punk::args::get_spec ::after] FORMS info ARG_INFO id -type]]
}\
-cleanup {
}\
-result [list 1 1 1]
test tclcoreparity_after_cancel_discrimination {'after cancel <word>' form resolution matches real semantics: non-id words are scripts, id-shaped words are the genuine liveness ambiguity}\
-constraints have_tclcoredocs\
-setup $common -body {
#real: 'after cancel someid' is a silent script-match no-op - model: cancelscript
lappend result [expr {![catch {after cancel someid}]}]
set ps [punk::args::parse_status {cancel someid} withid ::after]
lappend result [dict get $ps ok] [dict get $ps form]
#multi-word script tail
set ps [punk::args::parse_status {cancel puts hi} withid ::after]
lappend result [dict get $ps ok] [dict get $ps form]
#id-SHAPED word: truthfully ambiguous (a script can carry the same text -
#real Tcl resolves by id liveness at runtime, which no static type expresses)
set liveid [after 999999 {}]
after cancel $liveid
set ps [punk::args::parse_status [list cancel $liveid] withid ::after]
lappend result [dict get $ps ok] [dict get $ps failureclass]
lappend result [lsort [dict keys [dict get $ps formstatus]]]
}\
-cleanup {
}\
-result [list 1 1 cancelscript 1 cancelscript 0 multipleformmatches {cancelid cancelscript}]
test tclcoreparity_after_info_id_parity {'after info <word>' error-vs-ok outcome agrees for non-id-shaped words; dead-id over-acceptance is the accepted runtime-liveness boundary}\
-constraints have_tclcoredocs\
-setup $common -body {
#non-id-shaped word: real errors ("event ... doesn't exist"), model rejects the shape
lappend result [expr {![catch {after info someid}]}]
lappend result [dict get [punk::args::parse_status {info someid} withid ::after] ok]
#bare 'after info' is valid on both sides
lappend result [expr {![catch {after info}]}]
lappend result [dict get [punk::args::parse_status {info} withid ::after] ok]
#accepted divergence (over-acceptance class, goals/G-055): a DEAD id-shaped
#word - real errors on liveness, the model accepts the shape
lappend result [expr {![catch {after info after#999999999}]}]
lappend result [dict get [punk::args::parse_status {info after#999999999} withid ::after] ok]
}\
-cleanup {
}\
-result [list 0 0 1 1 0 1]
}
tcltest::cleanupTests ;#needed to produce test summary line.

Loading…
Cancel
Save