diff --git a/CHANGELOG.md b/CHANGELOG.md index eedc8403..5148ec58 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.16] - 2026-07-13 + +- tclcore moduledoc 0.3.0: 'after cancel ' and 'after info ' 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 ' 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. diff --git a/goals/G-055-tclcore-regen-workflow.md b/goals/G-055-tclcore-regen-workflow.md index dc4e5fa1..3b275b3b 100644 --- a/goals/G-055-tclcore-regen-workflow.md +++ b/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() 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 diff --git a/punkproject.toml b/punkproject.toml index c21eff18..d07dabf6 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.12.15" +version = "0.12.16" license = "BSD-2-Clause" diff --git a/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm index 1a2a85d5..6c37291e 100644 --- a/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm +++ b/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 ' 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 ' 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: diff --git a/src/modules/punk/args/moduledoc/tclcore-buildversion.txt b/src/modules/punk/args/moduledoc/tclcore-buildversion.txt index d37c03c9..c85c2e1e 100644 --- a/src/modules/punk/args/moduledoc/tclcore-buildversion.txt +++ b/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() 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 ' 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 ' 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) diff --git a/src/tests/modules/punk/args/testsuites/args/tclcoreparity.test b/src/tests/modules/punk/args/testsuites/args/tclcoreparity.test index 5aef3278..5e553c53 100644 --- a/src/tests/modules/punk/args/testsuites/args/tclcoreparity.test +++ b/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 ' 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 ' 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 ' 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.