Browse Source

G-155: vfscommonupdate promotion - kit payload receives punk::mix base 0.2.0 / cli 0.6.0, punkboot::utils 0.6.2, punk::lib 0.6.0 + templates modpod repack

Batched punkcheck-managed outputs per src/AGENTS.md carve-out.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
903c815910
  1. BIN
      src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm
  2. BIN
      src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm
  3. 222
      src/vfs/_vfscommon.vfs/modules/punk/args-0.14.0.tm
  4. 172
      src/vfs/_vfscommon.vfs/modules/punk/lib-0.6.0.tm
  5. 108
      src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.2.0.tm
  6. 97
      src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.6.0.tm
  7. 154
      src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.3.0.tm
  8. 15
      src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.1.tm
  9. BIN
      src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm
  10. 925
      src/vfs/_vfscommon.vfs/modules/punk/tclparser-0.1.0.tm
  11. 14
      src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.2.tm
  12. 8
      src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.4.tm
  13. BIN
      src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm
  14. BIN
      src/vfs/_vfscommon.vfs/modules/test/punk/ansi-0.1.1.tm
  15. BIN
      src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm
  16. BIN
      src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm
  17. BIN
      src/vfs/_vfscommon.vfs/modules/test/punk/ns-0.1.0.tm
  18. 1536
      src/vfs/_vfscommon.vfs/modules/voo-1.0.0.tm
  19. BIN
      src/vfs/_vfscommon.vfs/modules/zipper-0.14.tm

BIN
src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm

Binary file not shown.

BIN
src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm

Binary file not shown.

222
src/vfs/_vfscommon.vfs/modules/punk/args-0.12.7.tm → src/vfs/_vfscommon.vfs/modules/punk/args-0.14.0.tm

@ -8,7 +8,7 @@
# (C) 2024
#
# @@ Meta Begin
# Application punk::args 0.12.7
# Application punk::args 0.14.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::args 0 0.12.7]
#[manpage_begin punkshell_module_punk::args 0 0.14.0]
#[copyright "2024"]
#[titledesc {args parsing}] [comment {-- Name section and table of contents description --}]
#[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}]
@ -835,10 +835,14 @@ tcl::namespace::eval punk::args {
auto-calculated line use -typesynopsis on the arguments
concerned instead of overriding wholesale (see the notation
legend in punk::args::synopsis).
A known case where an override is warranted: script-level
or constructed definition ids, where the auto-synopsis
renders the raw id (e.g '(script)::punkboot::modules')
rather than an invocable command name.
Constructed definition ids whose auto-synopsis would render
the raw id rather than an invocable command name remain a
case where an override is warranted - but note (script)
ids are covered without one: since G-144 the auto-synopsis
leads with the declared @cmd -name for (script)-prefixed
ids (e.g 'make.tcl modules' for (script)::punkboot::modules),
so an override is only needed for a custom notation style
there, not for the name itself.
The -overlapallowed value names other forms this form is
KNOWN to overlap with (an argument list can cleanly match
both - e.g 'after cancel <id|script>' where real Tcl
@ -6524,6 +6528,31 @@ tcl::namespace::eval punk::args {
punk::args::define). When -form permits several forms, the form that
auto-selection settled on is reported here, and a formstatus key is added
recording each attempted form's outcome (see punk::args::parse_status).
Multiform failure contract (stable, machine-readable):
when a multi-form candidacy attempt matches no form, the raised error
carries the errorcode
PUNKARGS VALIDATION {noformmatch forms <fidlist> classes <classlist>}
-formerrors <records> -argspecs <spec>
where 'forms' lists the candidate form names ranked best-candidate
first and 'classes' holds one suffix-viability verdict per listed
form (same order) from the vocabulary:
incomplete - VIABLE: every supplied word was validly consumed as
a prefix of that form and parsing stopped only from
exhaustion at end-of-input - appending further
words can still satisfy the form
invalid - NOT viable: something already supplied contradicts
the form - no appended words can satisfy it
-formerrors holds one record per form (same ranked order) with keys
status/failureclass/badarg/message - 'status' is the same verdict.
The verdict is conservative for arguments whose -type is a
multi-member clause: input ending inside such a clause reports
invalid even when the partial clause words match.
Arguments matching several forms cleanly raise errorcode
PUNKARGS VALIDATION {multipleformmatches forms <fidlist>}
Consumers (form narrowing, interactive completion) should select
candidate forms from these channels rather than parsing message
text.
}
@form -form {withid withdef}
@leaders -min 1 -max 1
@ -6930,7 +6959,10 @@ tcl::namespace::eval punk::args {
}
noformmatch {
#G-041 multi-form candidacy: payload is: forms <fids> classes <per-form statuses>
#incomplete when some candidate form needs only more words
#G-152: the classes are sound per-form suffix-viability verdicts (candidacy
#confirms each 'incomplete' by a viability-mode re-parse before raising) -
#overall incomplete therefore means at least one form is still satisfiable
#by appending words
if {"incomplete" in [Dict_getdef $payload classes {}]} {
return incomplete
}
@ -7172,6 +7204,10 @@ tcl::namespace::eval punk::args {
#G-041: order a formfailures dict (fid -> {status .. failureclass .. badarg .. message ..})
#best-candidate first: literal affinity desc, then incomplete before invalid, then the
#incoming (declaration) order. Returns the re-ordered dict.
#order a noformmatch failure's per-form records best-candidate first: leading-literal
#affinity with the supplied words, then status incomplete before invalid, then
#declaration order (stable sort). G-152: status is the confirmed suffix-viability
#verdict, so the class tiebreak ranks still-satisfiable forms above impossible ones.
proc private::rank_form_failures {argspecs rawargs formfailures} {
set scored [list]
dict for {fid finfo} $formfailures {
@ -7672,7 +7708,14 @@ tcl::namespace::eval punk::args {
failing its -type check can also surface as
an allocation shortfall (missingrequiredvalue)
- badarg and the per-argument statuses carry
the specifics in both situations.
the specifics in both situations. For a
single-form parse this is the engine failure
classification, NOT a viability verdict -
read formstatus for that. For a multiform
noformmatch failure incomplete does carry
verdict weight: it means at least one
candidate form is still viable (its
formstatus status is incomplete).
scheme suggested display scheme: info (ok) | error (failure)
message empty | the validation failure message
errorcode empty | the -errorcode of the validation failure with
@ -7691,14 +7734,33 @@ tcl::namespace::eval punk::args {
then incomplete before invalid, then declaration
order).
formstatus dict keyed by candidate form name - each value a
dict with at least the key 'status'
(valid | incomplete | invalid) and for failed
dict with at least the key 'status' and for failed
candidates failureclass, badarg and message.
'status' is a per-form suffix-viability verdict
(stable consumer contract):
valid - the supplied words satisfy the form
incomplete - VIABLE: every supplied word was
validly consumed as a prefix of the
form and parsing stopped only from
exhaustion at end-of-input -
appending further words can still
satisfy the form
invalid - NOT viable: something already
supplied contradicts the form - no
appended words can satisfy it
The verdict is conservative for arguments whose
-type is a multi-member clause (e.g
{literal(elseif) expr script}): input ending
inside such a clause reports invalid even when
the partial clause words match.
Single-form parses report their one form; when
multi-form candidacy ran, every attempted form is
reported - partial-arglist consumers (e.g command
completion/hinting) can read per-form
compatibility from this key.
completion/hinting, cmdhelp form narrowing) can
select the still-satisfiable form(s) from this
key. The raising parse exposes the same verdicts
in its noformmatch errorcode - see
punk::args::parse.
receivednames received argument names normalized to definition
argument names (opt aliases folded to their
-alias|-name optionset)
@ -7844,7 +7906,17 @@ tcl::namespace::eval punk::args {
}
}
default {
set formstatus [dict create $fid [dict create status $status failureclass $failureclass badarg $badarg message $r]]
#G-152: formstatus statuses are suffix-viability verdicts. The overall
#status above keeps the engine failure classification (it drives the
#display scheme), but the per-form record must not report 'incomplete'
#unless the supplied words are a valid prefix of the form - confirm by
#a viability-mode re-parse exactly as multiform candidacy does.
set formverdict $status
if {$formverdict eq "incomplete"
&& [catch {private::get_dict_form $spec_dict $fid $arglist 1}]} {
set formverdict invalid
}
set formstatus [dict create $fid [dict create status $formverdict failureclass $failureclass badarg $badarg message $r]]
}
}
#the -argspecs payload (the whole resolved spec) is display machinery - too bulky
@ -9508,8 +9580,21 @@ tcl::namespace::eval punk::args {
}
set classinfo [lindex $ecode 2]
set failureclass [lindex $classinfo 0]
set formverdict [private::parse_status_classify $failureclass [lrange $classinfo 1 end]]
#G-152: a per-form 'incomplete' is a suffix-viability verdict - it must hold
#only when every supplied word was validly consumed as a prefix of the form
#and the failure is pure exhaustion at end-of-input. The failure class alone
#cannot witness that (count and missing-required classes fire before final
#validation sees the consumed words), so confirm by re-parsing in viability
#mode: exhaustion raises are skipped there while validation of the consumed
#words still applies - a raise means something already supplied contradicts
#the form.
if {$formverdict eq "incomplete"
&& [catch {private::get_dict_form $argspecs $fid $rawargs 1}]} {
set formverdict invalid
}
dict set formfailures $fid [dict create\
status [private::parse_status_classify $failureclass [lrange $classinfo 1 end]]\
status $formverdict\
failureclass $failureclass\
badarg [Dict_getdef [lrange $ecode 3 end] -badarg ""]\
message [lindex [split $fresult \n] 0]\
@ -9543,7 +9628,16 @@ tcl::namespace::eval punk::args {
set classes [lmap finfo [dict values $formfailures] {dict get $finfo status}]
set msg "Bad arguments for %caller%. No form of the command matches the supplied arguments. Candidate forms: $candidate_fids"
dict for {fid finfo} $formfailures {
append msg \n " form '$fid': [dict get $finfo message]"
#G-152: visibly distinguish forms still satisfiable by appending words
#(status incomplete - the confirmed suffix-viability verdict) from
#forms nothing appended can satisfy. Keep the "form '<fid>':" prefix
#intact - consumers/tests anchor on it.
if {[dict get $finfo status] eq "incomplete"} {
set verdicttag "(viable - needs more arguments)"
} else {
set verdicttag "(not viable)"
}
append msg \n " form '$fid': $verdicttag [dict get $finfo message]"
}
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list noformmatch forms $candidate_fids classes $classes] -argspecs $argspecs -formerrors $formfailures]] $msg
}
@ -9558,7 +9652,23 @@ tcl::namespace::eval punk::args {
#body, extracted verbatim from get_dict when multi-form candidacy landed (G-041).
#argspecs is a RESOLVED spec dict (get_dict resolves the deflist in its caller's
#context before selecting forms - nothing here touches the caller's frame).
proc private::get_dict_form {argspecs fid rawargs} {
#
#viabilitycheck (G-152): when 1, run as a suffix-viability probe - the question
#answered is not "do these arguments satisfy the form" but "are these arguments a
#valid PREFIX of the form" (could appending further words still satisfy it). The
#probe suppresses exactly the raises that represent pure exhaustion at
#end-of-input (below-minimum count checks and missing-required checks, each
#guarded by position conditions ensuring appended words could actually reach the
#missing element, and an option's value lying beyond the supplied words) and lets
#every other raise stand - including the final validation of the words actually
#consumed, which is the soundness backstop: a probe raise means something already
#supplied contradicts the form. Callers only test raised-vs-returned; the
#returned dict of a probe run is partial and must be discarded.
#Known conservatism (documented in the parse/parse_status contracts): input
#ending INSIDE a multi-member type clause raises (clause allocation cannot
#affirm the partial words) - such prefixes report non-viable even when the
#partial clause words match.
proc private::get_dict_form {argspecs fid rawargs {viabilitycheck 0}} {
#puts "-arg_info->$arg_info"
set flagsreceived [list] ;#for checking if required flags satisfied
set solosreceived [list]
@ -10560,6 +10670,15 @@ tcl::namespace::eval punk::args {
tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS
#assert -type value has llength 1 (multitype clauses not allowed for opts)
if {[tcl::dict::get $argstate $a -type] ne "none"} {
if {$viabilitycheck && $i == $maxidx} {
#G-152 viability probe: the adhoc option's value lies beyond
#the supplied words - pure end-of-input exhaustion. End the
#opts scan; the unfinished flag stays unassigned (required
#checks are position-guarded in probe mode).
set arglist [lrange $remaining_rawargs 0 $i-1]
set post_values {}
break
}
if {[tcl::dict::get $argstate $a -multiple]} {
tcl::dict::lappend opts $a $flagval
if {$a ni $multisreceived} {
@ -11168,14 +11287,29 @@ tcl::namespace::eval punk::args {
}
#-----------------------------------------------------
#G-152 viability probe: a below-minimum count is pure end-of-input exhaustion -
#satisfiable by appending words - only while the parse position can still reach
#that section: the leaders section only when nothing was consumed beyond it (no
#option words, no value words - appended words land after those and cannot fill
#a leader); the trailing values always (values end the argument list and the
#loops above consumed every supplied word). The probe skips those raises so
#final validation can pass verdict on the words actually consumed.
#Above-maximum counts are contradictions in any mode.
if {$viabilitycheck} {
set skip_leadershortfall [expr {![llength $flagsreceived] && ![llength $values]}]
set skip_valueshortfall 1
} else {
set skip_leadershortfall 0
set skip_valueshortfall 0
}
if {$leadermax == -1} {
#only check min
if {$num_leaders < $leadermin} {
if {$num_leaders < $leadermin && !$skip_leadershortfall} {
set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg
}
} else {
if {$num_leaders < $leadermin || $num_leaders > $leadermax} {
if {($num_leaders < $leadermin && !$skip_leadershortfall) || $num_leaders > $leadermax} {
if {$leadermin == $leadermax} {
set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg
@ -11188,12 +11322,12 @@ tcl::namespace::eval punk::args {
if {$valmax == -1} {
#only check min
if {$num_values < $valmin} {
if {$num_values < $valmin && !$skip_valueshortfall} {
set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg
}
} else {
if {$num_values < $valmin || $num_values > $valmax} {
if {($num_values < $valmin && !$skip_valueshortfall) || $num_values > $valmax} {
if {$valmin == $valmax} {
set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg
@ -11226,14 +11360,30 @@ tcl::namespace::eval punk::args {
# error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present"
#}
#for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us
if {[llength $LEADER_REQUIRED]} {
#G-152 viability probe: a missing required argument is pure end-of-input
#exhaustion - satisfiable by appending words - only while the parse position
#can still reach its argument class: a required leader only when no option or
#value words followed the leaders; a required option only while no value words
#have begun and no eopts marker (--) has closed the options; a required value
#always (values end the argument list). The probe skips those raises in that
#situation; otherwise the absence stands as a contradiction even in probe mode.
if {$viabilitycheck} {
set skip_leadermissing [expr {![llength $flagsreceived] && ![llength $values]}]
set skip_optionmissing [expr {![llength $values] && "--" ni $flagsreceived}]
set skip_valuemissing 1
} else {
set skip_leadermissing 0
set skip_optionmissing 0
set skip_valuemissing 0
}
if {[llength $LEADER_REQUIRED] && !$skip_leadermissing} {
if {[llength [set missing [punk::args::system::punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} {
set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg
#arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs
}
}
if {[llength $OPT_REQUIRED]} {
if {[llength $OPT_REQUIRED] && !$skip_optionmissing} {
#broken e.g
#punk::args::define {@id -id ::spud} @opts {-x -parsekey -coord -optional 0} {-y -parsekey -coord -optional 0}
#error is:
@ -11255,7 +11405,7 @@ tcl::namespace::eval punk::args {
#arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs
}
}
if {[llength $VAL_REQUIRED]} {
if {[llength $VAL_REQUIRED] && !$skip_valuemissing} {
if {[llength [set missing [punk::args::system::punklib_ldiff $VAL_REQUIRED $valnames_received]]]} {
set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg
@ -12106,6 +12256,12 @@ tcl::namespace::eval punk::args {
text* / *text value must start / end with 'text'
Custom @form -synopsis overrides are passed through verbatim and
may use any style (see punk::args::define).
The leading word(s) of an auto-generated line name the command:
normally the definition id. For a (script)-prefixed id (a
script-level dispatch surface such as (script)::punkboot::modules)
the declared @cmd -name leads instead, since the id is not the
invocation name (G-144).
"
@opts
-noheader -type none
@ -12218,6 +12374,20 @@ tcl::namespace::eval punk::args {
}
dict set SYND COMMAND $cmd_info
#G-144: a (script)-prefixed constructed id is not the command's invocation
#name (it names a script-level dispatch surface, e.g (script)::punkboot::modules
#for 'make.tcl modules') - lead auto-generated synopsis lines with the declared
#@cmd -name (the invocation) when one is present. All other ids lead with the
#id as before: real command paths (byte-identical), (autodef) ids (the id minus
#its tag IS the invocation - arg_error strips the tag for display - and the
#autodef @cmd -name is a display label such as 'destroy' or 'Object: ::x', not
#an invocation), and (shared)/(package)/(default)/(widgetcommand) doc ids
#(none declare an invocation-style @cmd -name today).
set synname $id
if {[string match "(script)*" $id]} {
set synname [Dict_getdef $spec cmd_info -name $id]
}
#leading "# " required (punk::ns::synopsis will pass through)
if {![dict exists $received -noheader]} {
set syn "# [Dict_getdef $spec cmd_info -summary ""]\n"
@ -12240,7 +12410,7 @@ tcl::namespace::eval punk::args {
#override can replace it wholesale (G-041 adjacent fix - the override was
#documented and stored in the spec but ignored by this renderer; arg_error's
#synopsis section already honoured it)
set formline "$id"
set formline $synname
set FORMARGS [list]
foreach argname [dict get $forminfo LEADER_NAMES] {
@ -12530,7 +12700,7 @@ tcl::namespace::eval punk::args {
continue
}
set arglist [dict get $arginfo args]
append summary $id
append summary $synname
set class_state leader
set option_count 0
set value_count 0
@ -14089,7 +14259,7 @@ package provide punk::args [tcl::namespace::eval punk::args {
tcl::namespace::path {::punk::args::lib ::punk::args::system}
variable pkg punk::args
variable version
set version 0.12.7
set version 0.14.0
}]
return

172
src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.1.tm → src/vfs/_vfscommon.vfs/modules/punk/lib-0.6.0.tm

@ -8,7 +8,7 @@
# (C) 2024
#
# @@ Meta Begin
# Application punk::lib 0.5.1
# Application punk::lib 0.6.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::lib 0 0.5.1]
#[manpage_begin punkshell_module_punk::lib 0 0.6.0]
#[copyright "2024"]
#[titledesc {punk general utility functions}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk library}] [comment {-- Description at end of page heading --}]
@ -295,7 +295,7 @@ tcl::namespace::eval punk::lib::check {
if {$replversion eq ""} {
#not loaded - determine what version would be provided, without loading it:
#an unsatisfiable require triggers the package unknown scan (registering ifneeded
#scripts) then fails before any load (0.5.1 dev modules are alpha - below 999999).
#scripts) then fails before any load (0.6.0 dev modules are alpha - below 999999).
catch {package require punk::repl 999999}
set available [package versions punk::repl]
if {[llength $available]} {
@ -2519,7 +2519,7 @@ namespace eval punk::lib {
#for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution.
#---------------------------------
proc tclscript_info {script {nscontext ""}} {
package require parser
tclparser_prefer ;#c library preferred, punk::tclparser pure-Tcl fallback wired otherwise (G-070)
#if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing.
if {[punk::ansi::ta::detect $script]} {
#we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts.
@ -3555,27 +3555,107 @@ namespace eval punk::lib {
}
return $resultd
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::lib::tclparser_prefer
@cmd -name punk::lib::tclparser_prefer\
-summary\
"Resolve which tclparser implementation serves punk::lib's script analysis."\
-help\
"Resolve which tclparser implementation serves punk::lib's
script-analysis procs (tclscript_info and the
tclscript_info::* helpers), returning 'c' or 'tcl'.
With no argument the decision is automatic and memoized for
the process: the tclparser c library is preferred when
'package require parser' succeeds (it provides the global
::parse command the analysis procs call); otherwise the
punk::tclparser pure-Tcl engine (G-070, parity-tested
against the c library) is wired in as namespace-local
'parse' aliases so the same call sites fall back with no
body changes. A c library loaded later is not consulted
once the fallback is wired.
With the optional argument the preference is forced: 'c'
requires the c library (error if unavailable) and removes
any fallback aliases; 'tcl' wires the pure-Tcl engine even
when the c library is present (testing/benchmarks)."
@values -min 0 -max 1
which -type string -choices {c tcl} -optional 1 -help\
"Force the preference instead of deciding automatically"
}]
}
proc tclparser_prefer {args} {
#manual parsing - see PUNKARGS documentation above
variable tclparser_which
if {[llength $args] > 1} {
error "wrong # args: should be \"punk::lib::tclparser_prefer ?c|tcl?\""
}
if {[llength $args] == 1} {
set want [lindex $args 0]
switch -exact -- $want {
c {
package require parser
catch {interp alias {} ::punk::lib::parse {}}
catch {interp alias {} ::punk::lib::tclscript_info::parse {}}
set tclparser_which c
}
tcl {
package require punk::tclparser
interp alias {} ::punk::lib::parse {} ::punk::tclparser::parse
interp alias {} ::punk::lib::tclscript_info::parse {} ::punk::tclparser::parse
set tclparser_which tcl
}
default {
error "punk::lib::tclparser_prefer: unknown preference '$want' - must be c or tcl"
}
}
return $tclparser_which
}
if {[info exists tclparser_which]} {
return $tclparser_which
}
if {![catch {package require parser}]} {
set tclparser_which c
} else {
package require punk::tclparser
interp alias {} ::punk::lib::parse {} ::punk::tclparser::parse
interp alias {} ::punk::lib::tclscript_info::parse {} ::punk::tclparser::parse
set tclparser_which tcl
}
return $tclparser_which
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::lib::tclparser_tcl
@cmd -name punk::lib::tclparser_tcl\
-summary\
"Pure-Tcl 'parse' with the same API as the tclparser c library."\
-help\
"Pure-Tcl implementation of the tclparser c library's 'parse'
command API (G-070) - a thin delegation to
punk::tclparser::parse, which covers the subcommands
punkshell consumes (command, getstring, countnewline) with
parity-tested result shapes and byte ranges. The c library
remains preferred where present - see tclparser_prefer."
@values -min 3 -max 3
subcmd -type string -help\
"parse subcommand (covered set: command, getstring, countnewline)"
string -type string -help\
"the string to operate on"
range -type list -help\
"byte range {start length} - {} means the whole string"
}]
}
proc tclparser_tcl {subcmd string range} {
#provide a tcl parser with the same API as the tclparser c library.
#pure-Tcl fallback for the tclparser c library (G-070).
#Historical upstream references (now vendored/pinned in the
#punkshell-maintained fork c:/repo/jn/tclparser_punk - see
#goals/G-070-pure-tcl-tclparser.md):
#https://chiselapp.com/user/aspect/repository/tclparser/index
#or
#https://github.com/ActiveState/teapot/tree/master/lib/tclparser
set scriptlist [list]
set argchars [split $string ""]
set in_dq 0 ;#in double quotes
set in_cb 0 ;#in curly braces
set in_commandsub 0 ;#in command substitution (i.e within square brackets)
#when we are in a command substitution - we should be able to keep appending whilst testing for info complete. review
set escaped 0
set nesting_level 0
set scripttoken ""
for {set i 0} {$i < [llength $argchars]} {incr i} {
set ch [lindex $argchars $i]
#todo
error "tclparser_tcl not yet implemented - in the meantime install the tclparser c library (package require parser)"
}
#https://github.com/tcltk-depot/tcl-parser
package require punk::tclparser
tailcall ::punk::tclparser::parse $subcmd $string $range
}
namespace eval tclscript_info {
@ -3671,7 +3751,39 @@ namespace eval punk::lib {
#- commonly just a variable e.g 'set cmdname foo; $cmdname arg1 arg2'
# but it could also have command subtitutions e.g 'cmdname_[get_suffix] arg1 arg2'
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::lib::tclscript_info::tclword_to_scriptlist
@cmd -name punk::lib::tclscript_info::tclword_to_scriptlist\
-summary\
"List the toplevel command substitutions within a single tcl word."\
-help\
"Analyse a string representing a single tcl word (a bareword,
double quoted string or curly braced string as it appears in
source - possibly spanning multiple lines) and return the list
of toplevel commands that would be substituted into it, in
order of evaluation. Each element is itself a command list.
A fully brace-quoted word is opaque and returns an empty list.
Command substitutions nested within array-variable indexes are
included; commands nested within OTHER commands' substitutions
are not (this proc is deliberately non-recursive - see
tclscript_info for the recursive analysis).
Parsing uses the tclparser c library when available, else the
punk::tclparser pure-Tcl engine (G-070) - see
punk::lib::tclparser_prefer. Works under a plain tclsh with no
parser binary on the package path."
@leaders
string -type string -help\
"the tcl word to analyse"
@values -min 0 -max 1
nscontext -type string -optional 1 -help\
"namespace context for the analysis (defaults to the caller's
current namespace)"
}]
}
proc tclword_to_scriptlist {string {nscontext ""}} {
#manual args (simple positional signature) - see PUNKARGS documentation above
#consider 'list [puts a]$v(x,[puts b])[puts c]'
# the command substitutions are [puts a], [puts b] and [puts c]
#The order of evaluation is [puts a] then [puts b] then [puts c] but [puts b] is within the variable array syntax and so parses differently to the other two command substitutions.
@ -3680,7 +3792,7 @@ namespace eval punk::lib {
if {$nscontext eq ""} {
set nscontext [uplevel 1 {namespace current}]
}
puts stderr "------------tclword_to_scriptlist called with string: $string nscontext: $nscontext------------"
#puts stderr "------------tclword_to_scriptlist called with string: $string nscontext: $nscontext------------"
#analyse a string that will undergo command substitution to determine the list of commands that will be substituted into the string.
#e.g for "a string with [puts hello] and [puts world]" we would return a list of 2 items, each being a list of the command and its arguments, e.g {puts hello} and {puts world}
#we need to do a full parse of the string to determine which square brackets are actually command substitutions that need to be analysed for complexity, and which are just literal characters within the string.
@ -3697,11 +3809,11 @@ namespace eval punk::lib {
set scriptlist [list]
if {![catch {package require parser}]} {
#use tclparser library if available
if {[punk::lib::tclparser_prefer] eq "c"} {
#tclparser c library (preferred where present - performance)
set parseinfo [::parse command $boguscmdline {0 end}]
} else {
puts stderr "tclparser library not available - using fallback tcl parser which may be less accurate - review"
#punk::tclparser pure-Tcl engine (G-070) - parity-tested against the c library
set parseinfo [::punk::lib::tclparser_tcl command $boguscmdline {0 end}]
}
#returns 4 items.
@ -3780,7 +3892,7 @@ namespace eval punk::lib {
#set var_subscript_cmds [punk::lib::tclscript_info::tclword_to_scriptlist $varstringfull $nscontext]
set varsubnodes [lindex $subnode 2]
foreach varsubnode $varsubnodes {
puts stderr "[a+ cyan]varsubnodes: $varsubnodes type: [lindex $varsubnode 0][a]"
#puts stderr "varsubnodes: $varsubnodes type: [lindex $varsubnode 0]"
if {[lindex $varsubnode 0] eq "command"} {
set pos_bytes [lindex $varsubnode 1]
set cmdstringfull [parse getstring $boguscmdline $pos_bytes] ;#this must be used instead of string range to correctly handle any multibyte characters in the original string
@ -3796,7 +3908,7 @@ namespace eval punk::lib {
} elseif {[lindex $varsubnode 0] eq "variable"} {
set pos_bytes [lindex $varsubnode 1]
set varstringfull [parse getstring $boguscmdline $pos_bytes]
puts stderr "[a+ cyan]varstringfull: $varstringfull[a]"
#puts stderr "varstringfull: $varstringfull"
set varscriptlist [punk::lib::tclscript_info::tclword_to_scriptlist $varstringfull $nscontext]
lappend scriptlist {*}$varscriptlist
}
@ -9390,7 +9502,7 @@ namespace eval ::punk::args::register {
package provide punk::lib [tcl::namespace::eval punk::lib {
variable pkg punk::lib
variable version
set version 0.5.1
set version 0.6.0
}]
return

108
src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.1.2.tm → src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.2.0.tm

@ -1,10 +1,10 @@
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'deck make' or src/make.tcl to update from <pkg>-buildversion.txt
# @@ Meta Begin
# Application punk::mix::base 0.1.2
# Application punk::mix::base 0.2.0
# @@ Meta End
package provide punk::mix::base [namespace eval punk::mix::base {
variable version
set version 0.1.2
set version 0.2.0
}]
package require punk::path
@ -344,7 +344,7 @@ namespace eval punk::mix::base {
}
#we can return module paths even if the project isn't yet under revision control
set src_subs [glob -nocomplain -dir [file join $candidate src] -type d -tail *]
set antipatterns [list *.vfs vendor* lib _build doc embedded runtime bootsupport]
set antipatterns [list *.vfs vendor* lib _build _mint _bake doc embedded runtime bootsupport]
set tm_folders [list]
foreach sub $src_subs {
set is_ok 1
@ -411,17 +411,23 @@ namespace eval punk::mix::base {
return [string map {:: /} $nsq]
}
proc get_build_workdir {path} {
proc get_bake_workdir {path} {
set repo_info [punk::repo::find_repos $path]
set base [lindex [dict get $repo_info project] 0]
if {![string length $base]} {
error "get_build_workdir unable to determine project base for path '$path'"
error "get_bake_workdir unable to determine project base for path '$path'"
}
if {![file exists $base/src] || ![file writable $base/src]} {
error "get_build_workdir unable to access $base/src"
error "get_bake_workdir unable to access $base/src"
}
file mkdir $base/src/_build
return $base/src/_build
file mkdir $base/src/_bake
return $base/src/_bake
}
#legacy name (pre-G-155 stage vocabulary; workdir was src/_build) - old make.tcl copies in
#generated projects may still call this. Delegates to the renamed resolver; retirement is a
#G-156 decision.
proc get_build_workdir {path} {
return [get_bake_workdir $path]
}
@ -963,86 +969,12 @@ namespace eval punk::mix::base {
return [dict create $storedpath $keyvals]
}
#calculate the runtime checksum and vfs checksums
proc get_all_vfs_build_cksums {path {cksum_opts {}}} {
set buildfolder [get_build_workdir $path]
set cksum_base_folder [file dirname $buildfolder] ;#this is the <project>/src folder - a reasonable base for our vfs cksums
set dict_cksums [dict create]
set buildrelpath [punk::repo::path_strip_alreadynormalized_prefixdepth $buildfolder $cksum_base_folder]
set vfs_tail_list [glob -nocomplain -dir $cksum_base_folder -type d -tails *.vfs]
foreach vfstail $vfs_tail_list {
set vname [file rootname $vfstail]
dict set dict_cksums $vfstail [list cksum ""]
dict set dict_cksums [file join $buildrelpath $vname.exe] [list cksum ""]
}
#buildruntime.exe obsolete..
puts stderr "warning obsolete? get_all_vfs_build_cksums 'buildruntime.exe'???"
set fullpath_buildruntime $buildfolder/buildruntime.exe
set ckinfo_buildruntime [cksum_path $fullpath_buildruntime]
set ck [dict get $ckinfo_buildruntime cksum]
set relpath [file join $buildrelpath "buildruntime.exe"]
dict set dict_cksums $relpath [list cksum $ck opts $cksum_opts]
set dict_cksums [fill_relativecksums_from_base_and_relativepathdict $cksum_base_folder $dict_cksums]
return $dict_cksums
}
proc get_vfs_build_cksums_stored {vfsfolder} {
set vfscontainer [file dirname $vfsfolder]
set buildfolder $vfscontainer/_build
set vfs [file tail $vfsfolder]
set vname [file rootname $vfs]
set dict_vfs [list $vname.vfs "" $vname.exe "" buildruntime.exe ""]
set ckfile $buildfolder/$vname.cksums
if {[file exists $ckfile]} {
set data [punk::mix::util::fcat -translation binary $ckfile]
foreach ln [split $data \n] {
if {[string trim $ln] eq ""} {continue}
lassign $ln path cksum
dict set dict_vfs $path $cksum
}
}
return $dict_vfs
}
proc get_all_build_cksums_stored {path} {
set buildfolder [get_build_workdir $path]
set vfscontainer [file dirname $buildfolder]
set vfslist [glob -nocomplain -dir $vfscontainer -type d -tail *.vfs]
set dict_cksums [dict create]
foreach vfs $vfslist {
set vname [file rootname $vfs]
set dict_vfs [get_vfs_build_cksums_stored $vfscontainer/$vfs]
dict set dict_cksums $vname $dict_vfs
}
return $dict_cksums
}
proc store_vfs_build_cksums {vfsfolder} {
if {![file isdirectory $vfsfolder]} {
error "Unable to find supplied vfsfolder: $vfsfolder"
}
set vfscontainer [file dirname $vfsfolder]
set buildfolder $vfscontainer/_build
set dict_vfs [get_vfs_build_cksums $vfsfolder]
set data ""
dict for {path cksum} $dict_vfs {
append data "$path $cksum" \n
}
set fd [open $buildfolder/$vname.cksums w]
chan configure $fd -translation binary
puts $fd $data
close $fd
return $dict_vfs
}
#The legacy vfs-cksums quartet (get_all_vfs_build_cksums, get_vfs_build_cksums_stored,
#get_all_build_cksums_stored, store_vfs_build_cksums) was RETIRED under G-155: repo-wide
#caller search found none, store_vfs_build_cksums called an undefined get_vfs_build_cksums
#(and an unset vname) so it can never have executed, and punkcheck records own the
#change-detection role the cksums files aimed at. Historical copies remain in older
#snapshots (src/vfs/*.vfs, mkzipfix.vfs).

97
src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.5.2.tm → src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.6.0.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::mix::cli 0.5.2
# Application punk::mix::cli 0.6.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -169,11 +169,11 @@ namespace eval punk::mix::cli {
set lc_this_exe [string tolower [info nameofexecutable]]
set lc_proj_bin [string tolower $project_base/bin]
set lc_build_bin [string tolower $project_base/src/_build]
set lc_bake_bin [string tolower $project_base/src/_bake]
if {"project" in $args} {
set is_own_exe 0
if {[string match "${lc_proj_bin}*" $lc_this_exe] || [string match "${lc_build_bin}" $lc_this_exe]} {
if {[string match "${lc_proj_bin}*" $lc_this_exe] || [string match "${lc_bake_bin}" $lc_this_exe]} {
set is_own_exe 1
puts stderr "WARNING - running make using executable that may be created by the project being built"
set answer [util::askuser "Do you want to proceed using this executable? (build will probably stop when it is unable to update the executable) Y|N"]
@ -346,7 +346,7 @@ namespace eval punk::mix::cli {
set opt_errorprefix [dict get $opts -errorprefix]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
validate_name_not_empty_or_spaced $projectname -errorprefix $opt_errorprefix
set reserved_words [list etc lib bin modules src doc vendorlib vendormodules embedded runtime _aside _build]
set reserved_words [list etc lib bin modules src doc vendorlib vendormodules embedded runtime _aside _build _mint _bake]
if {$projectname in $reserved_words } {
error "$opt_errorprefix '$projectname' cannot be one of reserved_words: $reserved_words"
}
@ -707,7 +707,7 @@ namespace eval punk::mix::cli {
}
proc build_modules_from_source_to_base {srcdir basedir args} {
set antidir [list "#*" "_build" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders (at any level) we don't want to search in or copy.
set antidir [list "#*" "_build" "_mint" "_bake" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders (at any level) we don't want to search in or copy.
set defaults [list {*}{
-installer punk::mix::cli::build_modules_from_source_to_base
-call-depth-internal 0
@ -742,14 +742,13 @@ namespace eval punk::mix::cli {
if {[file tail [file dirname $srcdir]] ne "src"} {
puts stderr "ERROR build_modules_from_source_to_base can only be called with a srcdir that is a subfolder of your 'src' directory"
puts stderr "The .tm modules are namespaced based on their directory depth - so we need to start at the root"
puts stderr "To build a subtree of your modules - use an appropriate src/modules folder and pass in the -subdirlist."
puts stderr "e.g if your modules are based at /x/src/modules2 and you wish to build only the .tm files at /x/src/modules2/skunkworks/lib"
puts stderr "To mint a subtree of your modules - use an appropriate src/modules folder and pass in the -subdirlist."
puts stderr "e.g if your modules are based at /x/src/modules2 and you wish to mint only the .tm files at /x/src/modules2/skunkworks/lib"
puts stderr "Use: >build_modules_from_source_to_base /x/src/modules2 /x/modules2 -subdirlist {skunkworks lib}"
exit 2
}
set srcdirname [file tail $srcdir]
set build [file dirname $srcdir]/_build/$srcdirname ;#relative to *original* srcdir - not current_source_dir
if {[llength $subdirlist] == 0} {
set target_module_dir $basedir
set current_source_dir $srcdir
@ -859,8 +858,8 @@ namespace eval punk::mix::cli {
set module_build_version $tmfile_versionsegment
}
set buildfolder $current_source_dir/_build
file mkdir $buildfolder
set mintfolder $current_source_dir/_mint
file mkdir $mintfolder
# -- ---
set config [dict create {*}{
-glob *
@ -870,13 +869,13 @@ namespace eval punk::mix::cli {
# -max-depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
$build_installer set_source_target $current_source_dir/$modpath $buildfolder
set build_installer [punkcheck::installtrack new $build_installername $mintfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $mintfolder/.punkcheck stderr] ;#with debugchannel
$build_installer set_source_target $current_source_dir/$modpath $mintfolder
set build_event [$build_installer start_event $config]
# -- ---
set podtree_copy $buildfolder/#modpod-$basename-$module_build_version
set modulefile $buildfolder/$basename-$module_build_version.tm
set podtree_copy $mintfolder/#modpod-$basename-$module_build_version
set modulefile $mintfolder/$basename-$module_build_version.tm
#todo - use modpod version as a source for change detection
#package require modpod
@ -894,12 +893,12 @@ namespace eval punk::mix::cli {
if {$did_skip} {set did_skip 0; puts -nonewline stdout \n}
set delete_failed 0
if {[file exists $buildfolder/]} {
puts stderr "deleting existing _build copy at $podtree_copy"
if {[file exists $mintfolder/]} {
puts stderr "deleting existing _mint copy at $podtree_copy"
if {[catch {
file delete -force $podtree_copy
} errMsg]} {
puts stderr "[punk::ansi::a+ red]deletion of _build copy at $podtree_copy failed: $errMsg[punk::ansi::a]"
puts stderr "[punk::ansi::a+ red]deletion of _mint copy at $podtree_copy failed: $errMsg[punk::ansi::a]"
set delete_failed 1
}
}
@ -911,9 +910,9 @@ namespace eval punk::mix::cli {
flush stdout
file copy $current_source_dir/$modpath $podtree_copy
if {$tmfile_versionsegment eq $magicversion} {
set tmfile $buildfolder/#modpod-$basename-$module_build_version/$basename-$magicversion.tm
set tmfile $mintfolder/#modpod-$basename-$module_build_version/$basename-$magicversion.tm
if {[file exists $tmfile]} {
set newname $buildfolder/#modpod-$basename-$module_build_version/$basename-$module_build_version.tm
set newname $mintfolder/#modpod-$basename-$module_build_version/$basename-$module_build_version.tm
file rename $tmfile $newname
set tmfile $newname
}
@ -927,20 +926,20 @@ namespace eval punk::mix::cli {
#delete and regenerate zip and modpod stubbed zip
set notes [list]
if {[catch {
file delete $buildfolder/$basename-$module_build_version.zip
file delete $mintfolder/$basename-$module_build_version.zip
} err] } {
set had_error 1
lappend notes "zip_delete_failed"
}
if {[catch {
file delete $buildfolder/$basename-$module_build_version.tm
file delete $mintfolder/$basename-$module_build_version.tm
} err]} {
set had_error 1
lappend notes "tm_delete_failed"
}
#create ordinary zip file without using external executable
package require punk::zip
set zipfile $buildfolder/$basename-$module_build_version.zip ;#ordinary zip file (deflate)
set zipfile $mintfolder/$basename-$module_build_version.zip ;#ordinary zip file (deflate)
#zipfs mkzip does exactly what we need anyway in this case
#unfortunately it's not available in all Tclsh versions we might be running..
@ -949,7 +948,7 @@ namespace eval punk::mix::cli {
#(Therefore no timestamps)
#zip reading utils generally intuit their existence and display them - but often an editor can't add comments to them
set wd [pwd]
cd $buildfolder
cd $mintfolder
puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version"
set mkzip_failed [catch {zipfs mkzip $zipfile #modpod-$basename-$module_build_version} errMkzip]
cd $wd
@ -963,14 +962,14 @@ namespace eval punk::mix::cli {
#archive variant; modpod stubs read both shapes).
puts stderr "zipfs mkzip failed under Tcl [info patchlevel] ($errMkzip) - falling back to punk::zip::mkzip (known pre-c971e6c7c4 Tcl 8.7 zipfs dotfile defect - core tkt 7d5f1c13089d463e7796)"
catch {file delete -- $zipfile} ;#a failed zipfs mkzip can leave a partial target zip - punk::zip::mkzip refuses to overwrite
punk::zip::mkzip -base $buildfolder -directory $buildfolder/#modpod-$basename-$module_build_version -- $zipfile *
punk::zip::mkzip -base $mintfolder -directory $mintfolder/#modpod-$basename-$module_build_version -- $zipfile *
}
} else {
#use -base $buildfolder so that -directory is included in the archive - the modpod stub relies on this - and extraction would be potentially messy otherwise
#use -base $mintfolder so that -directory is included in the archive - the modpod stub relies on this - and extraction would be potentially messy otherwise
#put in an archive-level comment to aid in debugging
#punk
punk::zip::mkzip -base $buildfolder -directory $buildfolder/#modpod-$basename-$module_build_version -- $zipfile *
punk::zip::mkzip -base $mintfolder -directory $mintfolder/#modpod-$basename-$module_build_version -- $zipfile *
#punk::zip::mkzip stores permissions - (unix style) - which zipfs mkzip doesn't
#Directory ident in zipfs relies on folders ending with trailing slash - if missing, it misidentifies dirs as files.
#(ie it can't use permissions/attributes alone to determine directory vs file)
@ -1078,8 +1077,8 @@ namespace eval punk::mix::cli {
set module_build_version $tmfile_versionsegment
}
set buildfolder $current_source_dir/_build
file mkdir $buildfolder
set mintfolder $current_source_dir/_mint
file mkdir $mintfolder
# -- ---
set config [dict create {*}{
-glob *
@ -1089,12 +1088,12 @@ namespace eval punk::mix::cli {
# -max-depth -1 for no limit
set build_installername tarjars_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $buildfolder
set build_installer [punkcheck::installtrack new $build_installername $mintfolder/.punkcheck]
$build_installer set_source_target $current_source_dir/$modpath $mintfolder
set build_event [$build_installer start_event $config]
# -- ---
set podtree_copy $buildfolder/#tarjar-$basename-$module_build_version
set modulefile $buildfolder/$basename-$module_build_version.tm
set podtree_copy $mintfolder/#tarjar-$basename-$module_build_version
set modulefile $mintfolder/$basename-$module_build_version.tm
$build_event targetset_init INSTALL $podtree_copy
@ -1110,12 +1109,12 @@ namespace eval punk::mix::cli {
if {$did_skip} {set did_skip 0; puts -nonewline stdout \n}
set delete_failed 0
if {[file exists $buildfolder/]} {
puts stderr "deleting existing _build copy at $podtree_copy"
if {[file exists $mintfolder/]} {
puts stderr "deleting existing _mint copy at $podtree_copy"
if {[catch {
file delete -force $podtree_copy
} errMsg]} {
puts stderr "[punk::ansi::a+ red]deletion of _build copy at $podtree_copy failed: $errMsg[punk::ansi::a]"
puts stderr "[punk::ansi::a+ red]deletion of _mint copy at $podtree_copy failed: $errMsg[punk::ansi::a]"
set delete_failed 1
}
}
@ -1126,7 +1125,7 @@ namespace eval punk::mix::cli {
puts stdout "$podtree_copy"
file copy $current_source_dir/$modpath $podtree_copy
if {$tmfile_versionsegment eq $magicversion} {
set tmfile $buildfolder/#tarjar-$basename-$module_build_version/#tarjar-loadscript-$basename.tcl
set tmfile $mintfolder/#tarjar-$basename-$module_build_version/#tarjar-loadscript-$basename.tcl
#we don't need to modify version or name of the loadscript
if {![file exists $tmfile]} {
set had_error 1
@ -1150,16 +1149,16 @@ namespace eval punk::mix::cli {
#delete and regenerate .tm
set notes [list]
if {[catch {
file delete $buildfolder/$basename-$module_build_version.tm
file delete $mintfolder/$basename-$module_build_version.tm
} err]} {
set had_error 1
lappend notes "tm_delete_failed"
}
#create ordinary tar file without using external executable
package require tar ;#tcllib
set tarfile $buildfolder/$basename-$module_build_version.tm ;#ordinary tar file (no compression - store)
set tarfile $mintfolder/$basename-$module_build_version.tm ;#ordinary tar file (no compression - store)
set wd [pwd]
cd $buildfolder
cd $mintfolder
puts "tar::create $tarfile #tarjar-$basename-$module_build_version"
if {[catch {
tar::create $tarfile #tarjar-$basename-$module_build_version
@ -1269,23 +1268,23 @@ namespace eval punk::mix::cli {
#} else {
#}
##REVIEW - should be in same structure/depth as $target_module_dir in _build?
##REVIEW - should be in same structure/depth as $target_module_dir in _mint?
##TODO
#set buildfolder $current_sourcedir/_build
#file mkdir $buildfolder
#set mintfolder $current_sourcedir/_mint
#file mkdir $mintfolder
#set tmfile $buildfolder/$basename-$module_build_version.tm
#file delete -force $buildfolder/#tarjar-$basename-$module_build_version
#set tmfile $mintfolder/$basename-$module_build_version.tm
#file delete -force $mintfolder/#tarjar-$basename-$module_build_version
#file delete -force $tmfile
#file copy -force $current_source_dir/#tarjar-$basename-$magicversion $buildfolder/#tarjar-$basename-$module_build_version
#file copy -force $current_source_dir/#tarjar-$basename-$magicversion $mintfolder/#tarjar-$basename-$module_build_version
##
##bsdtar doesn't seem to work.. or I haven't worked out the right options?
##exec tar -cvf $buildfolder/$basename-$module_build_version.tm $buildfolder/#tarjar-$basename-$module_build_version
##exec tar -cvf $mintfolder/$basename-$module_build_version.tm $mintfolder/#tarjar-$basename-$module_build_version
#package require tar
#tar::create $tmfile $buildfolder/#tarjar-$basename-$module_build_version
#tar::create $tmfile $mintfolder/#tarjar-$basename-$module_build_version
#if {![file exists $tmfile]} {
# puts stdout "ERROR: failed to build tarjar file $tmfile"
# exit 4
@ -1664,6 +1663,6 @@ namespace eval punk::mix::cli {
## Ready
package provide punk::mix::cli [namespace eval punk::mix::cli {
variable version
set version 0.5.2
set version 0.6.0
}]
return

154
src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.2.0.tm → src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.3.0.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::mix::commandset::layout 0.2.0
# Application punk::mix::commandset::layout 0.3.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -232,6 +232,19 @@ namespace eval punk::mix::commandset::layout {
# layout_materialize is the entry point; project.new copies from its result path.
# -- --- --- --- --- --- --- --- --- --- ---
# -- --- --- --- --- --- --- --- --- --- ---
# G-012: inert VCS-config payloads. A payload file stored under a tail name that is a
# key of materialize_renames would be LIVE configuration under its real name (git
# treats any .gitignore in the tree as live), so layouts store it under the inert key
# name and it is renamed to the materialized value name when the layout's effective
# content is composed (layout_stage_chain). layout_materialize withholds the in-place
# fast path from folders carrying inert payloads, so the rename only ever happens in
# the disposable staging dir - never in the store. .anti markers target STORE names
# (renames run after every layer is composed). The map is the extension point for
# future live-config payload classes (e.g .gitattributes).
# -- --- --- --- --- --- --- --- --- --- ---
variable materialize_renames {gitignore.in .gitignore}
punk::args::define {
@id -id ::punk::mix::commandset::layout::lib::layout_resolve_chain
@cmd -name punk::mix::commandset::layout::lib::layout_resolve_chain\
@ -334,7 +347,11 @@ namespace eval punk::mix::commandset::layout {
deleting it.
Returns a dict: path (stagingdir), chain, deletions (relative paths removed
by .anti markers), copied (number of files copied)."
by .anti markers), copied (number of files copied), materialized (list of
{from to} relative-path pairs of inert VCS-config payloads renamed to
their live names - G-012: gitignore.in stored inert in the store becomes
.gitignore in the composed folder; .anti markers always target store
names, since the rename runs after every layer is composed)."
@values -min 2 -max 2
chain -type list -help\
"Ordered list of layout source folders, lowest priority first
@ -376,7 +393,9 @@ namespace eval punk::mix::commandset::layout {
}
incr copied [_merge_copy_into $sourcefolder $stagingdir]
}
return [dict create path $stagingdir chain $chain deletions $deletions copied $copied]
#G-012: with every layer composed under store names, rename inert payloads to their live names
set materialized [_apply_materialize_renames $stagingdir]
return [dict create path $stagingdir chain $chain deletions $deletions copied $copied materialized $materialized]
}
punk::args::define {
@ -388,12 +407,15 @@ namespace eval punk::mix::commandset::layout {
"Resolves the named layout's overlay chain (layout_resolve_chain) within its
store and returns a folder holding the effective layout content.
When the chain is a single folder containing no .anti markers (the common
case - e.g the thin punk.project layout, or any module-carried layout) that
folder is returned as-is (staged 0) and no copying occurs. Otherwise the
chain is composed into a staging directory (layout_stage_chain) and the
staged folder is returned (staged 1) - the caller may delete it when done
(the default location is deterministic, so a later call recomposes it).
When the chain is a single folder containing no .anti markers AND no inert
VCS-config payloads (G-012: files named as keys of
layout_materialize_renames, e.g gitignore.in) that folder is returned
as-is (staged 0) and no copying occurs. Otherwise the chain is composed
into a staging directory (layout_stage_chain) and the staged folder is
returned (staged 1) - the caller may delete it when done (the default
location is deterministic, so a later call recomposes it). Inert payloads
always force staging: their rename to live names (.gitignore) must never
touch the store.
Layout folders whose store position does not follow the
<storeroot>/{vendor,custom}/<vendor>/<layoutfolder> shape (including any
@ -406,7 +428,9 @@ namespace eval punk::mix::commandset::layout {
0 if it is the store folder itself
chain - ordered contributing folders, lowest priority first
deletions - relative paths removed by .anti markers (staged only)
copied - number of files copied into staging (staged only)"
copied - number of files copied into staging (staged only)
materialized - {from to} relative-path pairs of inert payloads renamed
to live names (staged only; G-012)"
@leaders -min 1 -max 1
layoutname -type string -optional 0 -help\
"Visible layout name as shown by 'dev project.layouts' (e.g punk.project)"
@ -452,16 +476,21 @@ namespace eval punk::mix::commandset::layout {
}
}
}
#fast path - a single-source chain with no .anti markers is usable in place
if {[llength $chain] == 1 && ![llength [_find_anti_relpaths [lindex $chain 0]]]} {
return [dict create path [lindex $chain 0] staged 0 chain $chain deletions [list] copied 0]
#fast path - a single-source chain with no .anti markers and no inert payloads is
#usable in place. Inert VCS-config payloads (G-012) force staging: their rename to
#live names must never touch the store.
if { [llength $chain] == 1
&& ![llength [_find_anti_relpaths [lindex $chain 0]]]
&& ![llength [_find_inert_payload_relpaths [lindex $chain 0]]]
} {
return [dict create path [lindex $chain 0] staged 0 chain $chain deletions [list] copied 0 materialized [list]]
}
if {$opt_stagingdir eq ""} {
set sanitized [regsub -all {[^A-Za-z0-9_.-]} $layoutname _]
set opt_stagingdir [file join [punk::lib::tempdir] punk_layout_staging $sanitized]
}
set stageinfo [layout_stage_chain $chain $opt_stagingdir]
return [dict create path [dict get $stageinfo path] staged 1 chain $chain deletions [dict get $stageinfo deletions] copied [dict get $stageinfo copied]]
return [dict create path [dict get $stageinfo path] staged 1 chain $chain deletions [dict get $stageinfo deletions] copied [dict get $stageinfo copied] materialized [dict get $stageinfo materialized]]
}
punk::args::define {
@ -531,6 +560,101 @@ namespace eval punk::mix::commandset::layout {
return $antis
}
punk::args::define {
@id -id ::punk::mix::commandset::layout::lib::layout_materialize_renames
@cmd -name punk::mix::commandset::layout::lib::layout_materialize_renames\
-summary\
"Return the inert-payload rename map applied at layout materialization"\
-help\
"Returns a dict of {inertTail materializedTail} (G-012). Layout payload
files stored under an inert tail name - so they are not live
configuration in the layout store (git never reads gitignore.in) - are
renamed to their materialized tail name (.gitignore) when a layout's
effective content is composed by layout_stage_chain. The map is the
extension point for future live-config payload classes (e.g
.gitattributes stored as gitattributes.in)."
@values -min 0 -max 0
}
proc layout_materialize_renames {} {
variable materialize_renames
return $materialize_renames
}
punk::args::define {
@id -id ::punk::mix::commandset::layout::lib::_find_inert_payload_relpaths
@cmd -name punk::mix::commandset::layout::lib::_find_inert_payload_relpaths\
-summary\
"Recursively collect inert-payload files below a layout folder"\
-help\
"Internal helper for the overlay merge primitive. Returns the relative
paths of files whose tail is an inert-payload store name (a key of
layout_materialize_renames) anywhere below basefolder."
@values -min 1 -max 2
basefolder -type string
prefix -type string -optional 1
}
proc _find_inert_payload_relpaths {basefolder {prefix ""}} {
#manual args - see PUNKARGS ::punk::mix::commandset::layout::lib::_find_inert_payload_relpaths
variable materialize_renames
if {$prefix eq ""} {
set here $basefolder
} else {
set here [file join $basefolder $prefix]
}
set found [list]
foreach entry [_dir_entries $here] {
if {$prefix eq ""} {
set rel $entry
} else {
set rel [file join $prefix $entry]
}
if {[dict exists $materialize_renames $entry]} {
lappend found $rel
} elseif {[file isdirectory [file join $here $entry]]} {
lappend found {*}[_find_inert_payload_relpaths $basefolder $rel]
}
}
return $found
}
punk::args::define {
@id -id ::punk::mix::commandset::layout::lib::_apply_materialize_renames
@cmd -name punk::mix::commandset::layout::lib::_apply_materialize_renames\
-summary\
"Rename inert-payload files in a composed folder to their live names"\
-help\
"Internal helper for the overlay merge primitive. Renames every
inert-payload file below folder (tail matching a key of
layout_materialize_renames) to its materialized tail name in place, and
returns the list of applied {from to} relative-path pairs. Errors when
both the inert name and its materialized name exist at the same location
- a layout carrying both is contradictory (the materialized name is live
VCS config and must never be store content)."
@values -min 1 -max 1
folder -type string
}
proc _apply_materialize_renames {folder} {
#manual args - see PUNKARGS ::punk::mix::commandset::layout::lib::_apply_materialize_renames
variable materialize_renames
set applied [list]
foreach relpath [_find_inert_payload_relpaths $folder] {
set newtail [dict get $materialize_renames [file tail $relpath]]
set relbase [file dirname $relpath]
if {$relbase eq "."} {
set tgtrel $newtail
} else {
set tgtrel [file join $relbase $newtail]
}
set tgt [file join $folder $tgtrel]
if {[file exists $tgt]} {
error "layout materialize: both inert payload '$relpath' and materialized name '$tgtrel' are present - the materialized name is live VCS config and must never be layout store content"
}
file rename -- [file join $folder $relpath] $tgt
lappend applied [list $relpath $tgtrel]
}
return $applied
}
punk::args::define {
@id -id ::punk::mix::commandset::layout::lib::_merge_copy_into
@cmd -name punk::mix::commandset::layout::lib::_merge_copy_into\
@ -681,6 +805,6 @@ namespace eval punk::mix::commandset::layout {
## Ready
package provide punk::mix::commandset::layout [namespace eval punk::mix::commandset::layout {
variable version
set version 0.2.0
set version 0.3.0
}]
return

15
src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.0.tm → src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.1.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::mix::commandset::project 0.4.0
# Application punk::mix::commandset::project 0.4.1
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -17,7 +17,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::mix::commandset::project 0 0.4.0]
#[manpage_begin punkshell_module_punk::mix::commandset::project 0 0.4.1]
#[copyright "2023"]
#[titledesc {dec commandset - project}] [comment {-- Name section and table of contents description --}]
#[moddesc {deck CLI commandset - project}] [comment {-- Description at end of page heading --}]
@ -529,6 +529,15 @@ namespace eval punk::mix::commandset::project {
puts $fd "\[project\]"
puts $fd "name = \"$projectname\""
puts $fd "version = \"0.1.0\""
puts $fd "#url: project home/repository url. Consumed by buildsuite artifact"
puts $fd "#emission (punkbin-artifact.toml 'project_url' field; -Dprojecturl"
puts $fd "#overrides). Uncomment and set before publishing artifacts."
puts $fd "#url = \"https://example.com/$projectname\""
puts $fd "#packager: declared identity for published artifacts (declarative, not"
puts $fd "#proof - signing is the verification layer). Resolution: -Dpackager >"
puts $fd "#PUNKBIN_PACKAGER env > this field > git identity > fossil user default >"
puts $fd "#unrecorded."
puts $fd "#packager = \"Your Name <you@example.com>\""
close $fd
puts stdout "seeded punkproject.toml (\[project\] name '$projectname' version 0.1.0)"
}
@ -1612,6 +1621,6 @@ namespace eval punk::mix::commandset::project {
## Ready
package provide punk::mix::commandset::project [namespace eval punk::mix::commandset::project {
variable version
set version 0.4.0
set version 0.4.1
}]
return

BIN
src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

925
src/vfs/_vfscommon.vfs/modules/punk/tclparser-0.1.0.tm

@ -0,0 +1,925 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application punk::tclparser 0.1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package require Tcl 8.6-
#No other package requirements by design (G-070): this module is the pure-Tcl
#fallback for the tclparser C library and must load on a plain tclsh.
#punk::args is used for documentation only via the inert PUNKARGS/register
#mechanism - it is not required at runtime.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# punk::tclparser
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Pure-Tcl implementation of (a subset of) the 'parse' command API provided by
# the tclparser C library (TclPro lineage - punkshell fork/reference at
# c:/repo/jn/tclparser_punk, pin recorded in goals/G-070-pure-tcl-tclparser.md).
#
# Covered subcommands (the set punkshell consumers actually use - G-070):
# parse command <string> <range>
# parse getstring <string> <range>
# parse countnewline <string> ?range?
# Uncovered (error advising the C library): expr varname list charindex charlength
#
# IMPORTANT: this module deliberately does NOT provide the package name
# 'parser'. Consumers use `package require parser` as the capability probe
# meaning "the fast C library is present" (textblock::height fast path,
# punk::lib dispatch). The pure-Tcl engine is always the explicit fallback.
#
# All ranges ({start length} pairs) are BYTE offsets over the string's
# (modified) utf-8 representation, matching what the C library reports
# against Tcl's internal string bytes. Parsing is performed over a byte view
# so emitted offsets are byte offsets by construction: every Tcl
# syntax-significant character is ASCII and utf-8 continuation bytes cannot
# alias them.
tcl::namespace::eval punk::tclparser {
variable PUNKARGS
tcl::namespace::export parse
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::tclparser::parse
@cmd -name punk::tclparser::parse\
-summary\
"Pure-Tcl subset of the tclparser C library's 'parse' command."\
-help\
"Pure-Tcl implementation of the tclparser C library's 'parse'
command API, covering the subcommands punkshell consumers use:
command, getstring, countnewline.
Subcommand results and the ranges within them use BYTE offsets
({start length} pairs) over the string's utf-8 bytes, exactly as
the C library reports them - use 'parse getstring' (not string
range) to extract substrings by range.
parse command string range
Parse one command from string. Returns a 4 element list:
commentRange commandRange restRange tree.
The tree is a list of word nodes {type {start len} subnodes}
with word types simple/word/expand and subnode types
text/backslash/command/variable. Literal {*} expansions are
expanded at parse time into per-element simple nodes.
parse getstring string range
Return the substring of string covered by the byte range.
parse countnewline string ?range?
Return the number of newline characters in the range
(default: the whole string).
The uncovered C library subcommands (expr, varname, list,
charindex, charlength) raise an error advising installation of
the C library (package require parser).
This module intentionally does not provide the package name
'parser' - that name is the capability probe for the C library."
@values -min 2 -max 3
subcommand -type string -choices {command getstring countnewline} -choicerestricted 0 -help\
"Operation to perform (covered set: command, getstring, countnewline)"
string -type string -help\
"The string to operate on"
range -type list -optional 1 -help\
"Byte range {start length} within string. {} or omitted means the
whole string. length may be the literal 'end' to mean through to
the end of the string."
}]
}
proc parse {args} {
#parity-oriented manual parsing - documentation via PUNKARGS above (see src/modules/AGENTS.md)
if {[llength $args] < 2} {
error "wrong # args: should be \"punk::tclparser::parse subcommand string ?range?\""
}
set subcommand [lindex $args 0]
set str [lindex $args 1]
if {[llength $args] >= 3} {
set range [lindex $args 2]
} else {
set range {}
}
switch -exact -- $subcommand {
command {
if {[llength $args] != 3} {
error "wrong # args: should be \"punk::tclparser::parse command string range\""
}
return [engine::parse_command $str $range]
}
getstring {
if {[llength $args] != 3} {
error "wrong # args: should be \"punk::tclparser::parse getstring string range\""
}
return [engine::parse_getstring $str $range]
}
countnewline {
return [engine::parse_countnewline $str $range]
}
expr - varname - list - charindex - charlength {
error "punk::tclparser::parse: subcommand '$subcommand' is not implemented in the pure-Tcl engine (covered set: command, getstring, countnewline). Install the tclparser C library (package require parser) for the full API."
}
default {
error "punk::tclparser::parse: bad subcommand \"$subcommand\": must be command, getstring or countnewline"
}
}
}
}
tcl::namespace::eval punk::tclparser::engine {
#Internal parsing engine. All procs here operate on a 'byte view' string in
#which each character is one byte of the original string's (modified) utf-8
#encoding - so tcl string indices into the byte view ARE byte offsets.
#Tcl syntax characters are all ASCII; utf-8 continuation bytes (0x80-0xBF)
#and lead bytes (0xC0+) can never alias them, so scanning the byte view
#gives byte-correct ranges with no separate bookkeeping.
variable TCL9 [package vsatisfies [package provide Tcl] 9-]
# -- byte view conversion ------------------------------------------------
proc to_bytes {s} {
#NUL must round-trip as modified utf-8 (0xC0 0x80) because that is the
#internal representation the C parser indexes over.
if {[string is ascii $s] && [string first \x00 $s] < 0} {
return $s
}
variable TCL9
if {$TCL9} {
set b [encoding convertto -profile tcl8 utf-8 $s]
} else {
set b [encoding convertto utf-8 $s]
}
return [string map [list \x00 \xC0\x80] $b]
}
proc from_bytes {b} {
if {[string is ascii $b]} {
return $b
}
variable TCL9
set b [string map [list \xC0\x80 \x00] $b]
if {$TCL9} {
return [encoding convertfrom -profile tcl8 utf-8 $b]
}
return [encoding convertfrom utf-8 $b]
}
proc resolve_range {range total} {
#returns {first len} in bytes. {} means whole string.
if {[llength $range] == 0} {
return [list 0 $total]
}
if {[llength $range] != 2} {
error "invalid range \"$range\": should be \"\" or a list of two items: start length"
}
lassign $range first len
if {![string is integer -strict $first] || $first < 0} {
error "invalid range start \"$first\""
}
if {$len eq "end"} {
set len [expr {$total - $first}]
} elseif {![string is integer -strict $len]} {
error "invalid range length \"$len\""
}
if {$first > $total} {
set first $total
}
if {$first + $len > $total} {
set len [expr {$total - $first}]
}
if {$len < 0} {
set len 0
}
return [list $first $len]
}
# -- public-facing operations (called by punk::tclparser::parse) --------
proc parse_command {str range} {
set bytes [to_bytes $str]
set total [string length $bytes]
lassign [resolve_range $range $total] first len
set endpos [expr {$first + $len}]
lassign [parse_command_bytes $bytes $first $endpos 0] cs cl ks kl rest term tree
if {$cs < 0} {
set cs 0
set cl 0
}
return [list [list $cs $cl] [list $ks $kl] [list $rest [expr {$endpos - $rest}]] $tree]
}
proc parse_getstring {str range} {
set bytes [to_bytes $str]
set total [string length $bytes]
lassign [resolve_range $range $total] first len
return [from_bytes [string range $bytes $first [expr {$first + $len - 1}]]]
}
proc parse_countnewline {str range} {
set bytes [to_bytes $str]
set total [string length $bytes]
lassign [resolve_range $range $total] first len
set seg [string range $bytes $first [expr {$first + $len - 1}]]
return [expr {[string length $seg] - [string length [string map [list \n {}] $seg]]}]
}
# -- core scanner --------------------------------------------------------
proc skip_white {bytes pos endpos} {
#inter-word whitespace: space tab vtab ff cr, plus backslash-newline
#(with its trailing space/tab run) which acts as a word separator.
#Never consumes bare newline or semicolon (command terminators).
while {$pos < $endpos} {
set c [string index $bytes $pos]
switch -exact -- $c {
" " - \t - \v - \f - \r {
incr pos
}
"\\" {
if {$pos + 1 < $endpos && [string index $bytes $pos+1] eq "\n"} {
incr pos 2
while {$pos < $endpos} {
set c2 [string index $bytes $pos]
if {$c2 eq " " || $c2 eq "\t"} {
incr pos
} else {
break
}
}
} else {
return $pos
}
}
default {
return $pos
}
}
}
return $pos
}
proc bs_advance {bytes pos endpos} {
#pos is at a backslash: return the position just after the full escape
#sequence, per Tcl_ParseBackslash rules. \xHH consumes at most 2 hex
#digits on every supported runtime (TIP 388, in since 8.6 - verified
#against the c library on 8.6.13, 8.7a6 and 9.0.3; unlimited-run \x is
#Tcl 8.5, out of scope).
set p [expr {$pos + 1}]
if {$p >= $endpos} {
return $p
}
set c [string index $bytes $p]
switch -exact -- $c {
"\n" {
incr p
while {$p < $endpos} {
set c2 [string index $bytes $p]
if {$c2 eq " " || $c2 eq "\t"} {
incr p
} else {
break
}
}
return $p
}
x {
incr p
set hex 0
while {$p < $endpos && $hex < 2 && [string match {[0-9a-fA-F]} [string index $bytes $p]]} {
incr p
incr hex
}
return $p
}
u {
incr p
set hex 0
while {$p < $endpos && $hex < 4 && [string match {[0-9a-fA-F]} [string index $bytes $p]]} {
incr p
incr hex
}
return $p
}
U {
incr p
set hex 0
while {$p < $endpos && $hex < 8 && [string match {[0-9a-fA-F]} [string index $bytes $p]]} {
incr p
incr hex
}
return $p
}
default {
if {[string match {[0-7]} $c]} {
set oct 0
while {$p < $endpos && $oct < 3 && [string match {[0-7]} [string index $bytes $p]]} {
incr p
incr oct
}
return $p
}
#single (possibly multibyte) character: consume the full utf-8
#sequence so the escape range never splits a character.
set b [scan $c %c]
incr p
if {$b >= 0xC0} {
while {$p < $endpos} {
set nb [scan [string index $bytes $p] %c]
if {$nb >= 0x80 && $nb < 0xC0} {
incr p
} else {
break
}
}
}
return $p
}
}
}
proc parse_command_bytes {bytes pos endpos nested} {
#Parse a single command starting at pos.
#Returns: commentStart commentLen cmdStart cmdLen restPos term tree
#term is one of: eof, nl, semi, bracket (bracket: ']' seen but NOT
#consumed - the command-substitution scanner consumes it).
set commentStart -1
set commentLen 0
while {1} {
set pos [skip_white $bytes $pos $endpos]
if {$pos < $endpos && [string index $bytes $pos] eq "\n"} {
#before the command starts, newlines are ordinary whitespace
#(they only terminate once a command is in progress)
incr pos
continue
}
if {$pos < $endpos && [string index $bytes $pos] eq "#"} {
if {$commentStart < 0} {
set commentStart $pos
}
while {$pos < $endpos} {
set c [string index $bytes $pos]
if {$c eq "\\"} {
set pos [bs_advance $bytes $pos $endpos]
} elseif {$c eq "\n"} {
incr pos
break
} else {
incr pos
}
}
set commentLen [expr {$pos - $commentStart}]
} else {
break
}
}
set cmdStart $pos
set tree [list]
set term eof
while {1} {
set pos [skip_white $bytes $pos $endpos]
if {$pos >= $endpos} {
set term eof
break
}
set c [string index $bytes $pos]
if {$c eq "\n"} {
set term nl
incr pos
break
}
if {$c eq ";"} {
set term semi
incr pos
break
}
if {$nested && $c eq "\]"} {
set term bracket
break
}
lassign [parse_word $bytes $pos $endpos $nested] nodes pos
lappend tree {*}$nodes
}
set cmdLen [expr {$pos - $cmdStart}]
return [list $commentStart $commentLen $cmdStart $cmdLen $pos $term $tree]
}
proc parse_word {bytes pos endpos nested} {
#Parse one word. Returns {nodes nextpos} - nodes is a list of word
#nodes (usually one; a literal {*} expansion may produce zero or more).
set wordStart $pos
set expand 0
if {[string range $bytes $pos [expr {$pos + 2}]] eq "\{*\}" && $pos + 3 < $endpos} {
set c3 [string index $bytes $pos+3]
set sep 0
switch -exact -- $c3 {
" " - \t - \v - \f - \r - "\n" - ";" {
set sep 1
}
"\\" {
#backslash-newline is a word separator, so a {*} followed
#by a line continuation is a plain braced word
if {$pos + 4 < $endpos && [string index $bytes $pos+4] eq "\n"} {
set sep 1
}
}
"\]" {
if {$nested} {
set sep 1
}
}
}
if {!$sep} {
set expand 1
incr pos 3
}
}
set c [string index $bytes $pos]
set bodykind bare
if {$c eq "\{"} {
set bodykind brace
lassign [parse_braces $bytes $pos $endpos] tokens pos
} elseif {$c eq "\""} {
set bodykind quote
lassign [parse_quoted $bytes $pos $endpos] tokens pos
} else {
lassign [parse_tokens $bytes $pos $endpos bare $nested] tokens pos
if {[llength $tokens] == 0} {
#can only happen for a lone backslash-newline handled by
#skip_white, or at a stop char - defensive: emit empty text
set tokens [list [list text [list $pos 0] {}]]
}
}
if {$bodykind ne "bare" && $pos < $endpos} {
#closing brace/quote must be followed by whitespace or a terminator
set c2 [string index $bytes $pos]
set ok 0
switch -exact -- $c2 {
" " - \t - \v - \f - \r - "\n" - ";" {
set ok 1
}
"\]" {
if {$nested} {
set ok 1
}
}
"\\" {
if {$pos + 1 < $endpos && [string index $bytes $pos+1] eq "\n"} {
set ok 1
}
}
}
if {!$ok} {
if {$bodykind eq "brace"} {
error "extra characters after close-brace"
} else {
error "extra characters after close-quote"
}
}
}
set wordLen [expr {$pos - $wordStart}]
if {$expand} {
if {[llength $tokens] == 1 && [lindex $tokens 0 0] eq "text"} {
#literal expansion: parse the literal as a list; each element
#becomes its own simple word node with ranges into the
#original string. If it is not a valid list, fall back to an
#expand node (the runtime raises the expansion error).
lassign [lindex $tokens 0 1] tstart tlen
if {![catch {list_elements $bytes $tstart [expr {$tstart + $tlen}]} elements]} {
set nodes [list]
foreach e $elements {
lassign $e estart elen etstart etlen
lappend nodes [list simple [list $estart $elen] [list [list text [list $etstart $etlen] {}]]]
}
return [list $nodes $pos]
}
}
return [list [list [list expand [list $wordStart $wordLen] $tokens]] $pos]
}
if {[llength $tokens] == 1 && [lindex $tokens 0 0] eq "text"} {
return [list [list [list simple [list $wordStart $wordLen] $tokens]] $pos]
}
return [list [list [list word [list $wordStart $wordLen] $tokens]] $pos]
}
proc parse_braces {bytes pos endpos} {
#pos at the open brace. Returns {tokens nextpos} with nextpos just
#after the close brace. Tokens: text runs and backslash tokens for
#backslash-newline (the only substitution inside braces).
set level 1
set p [expr {$pos + 1}]
set textStart $p
set tokens [list]
while {1} {
if {$p >= $endpos} {
error "missing close-brace"
}
set c [string index $bytes $p]
switch -exact -- $c {
"\{" {
incr level
incr p
}
"\}" {
incr level -1
if {$level == 0} {
if {$p > $textStart || [llength $tokens] == 0} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
incr p
return [list $tokens $p]
}
incr p
}
"\\" {
if {$p + 1 < $endpos && [string index $bytes $p+1] eq "\n"} {
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
set bsend [expr {$p + 2}]
while {$bsend < $endpos} {
set c2 [string index $bytes $bsend]
if {$c2 eq " " || $c2 eq "\t"} {
incr bsend
} else {
break
}
}
lappend tokens [list backslash [list $p [expr {$bsend - $p}]] {}]
set p $bsend
set textStart $p
} else {
#backslash quotes the next byte for brace counting
#purposes; both stay part of the text run
incr p 2
if {$p > $endpos} {
set p $endpos
}
}
}
default {
incr p
}
}
}
}
proc parse_quoted {bytes pos endpos} {
#pos at the open quote. Returns {tokens nextpos} with nextpos just
#after the close quote.
set p [expr {$pos + 1}]
lassign [parse_tokens $bytes $p $endpos quote 0] tokens p
if {$p >= $endpos || [string index $bytes $p] ne "\""} {
error "missing \""
}
if {[llength $tokens] == 0} {
set tokens [list [list text [list [expr {$pos + 1}] 0] {}]]
}
incr p
return [list $tokens $p]
}
proc parse_tokens {bytes pos endpos mode nested} {
#Scan substitution tokens. mode: quote (stop at unescaped double
#quote), index (stop at close paren - array subscript), bare (stop at
#whitespace/terminators; backslash-newline acts as a word separator).
#Returns {tokens stoppos} - stoppos is AT the stopping character.
variable TCL9
set tokens [list]
set textStart $pos
set p $pos
while {$p < $endpos} {
set c [string index $bytes $p]
if {$mode eq "quote"} {
if {$c eq "\""} {
break
}
} elseif {$mode eq "index"} {
if {$c eq ")"} {
break
}
if {$c eq "(" && $TCL9} {
#Tcl 9 rejects a bare open paren in an array index at
#index-token level (inside nested [ ] or $v( ) is fine);
#Tcl 8.x treats it as ordinary index text.
error "invalid character in array index"
}
} else {
set stop 0
switch -exact -- $c {
" " - \t - \v - \f - \r - "\n" - ";" {
set stop 1
}
"\]" {
if {$nested} {
set stop 1
}
}
}
if {$stop} {
break
}
}
switch -exact -- $c {
"\\" {
if {$mode eq "bare" && $p + 1 < $endpos && [string index $bytes $p+1] eq "\n"} {
#backslash-newline separates words in bare context
break
}
if {$p + 1 >= $endpos} {
#trailing backslash with nothing following becomes its
#own text token (like a bare $)
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
lappend tokens [list text [list $p 1] {}]
incr p
set textStart $p
continue
}
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
set next [bs_advance $bytes $p $endpos]
lappend tokens [list backslash [list $p [expr {$next - $p}]] {}]
set p $next
set textStart $p
}
"$" {
lassign [parse_varname $bytes $p $endpos] vtok next
if {$vtok eq ""} {
#a $ with no variable name following becomes its own
#text token (it does not merge with adjacent text)
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
lappend tokens [list text [list $p 1] {}]
incr p
set textStart $p
} else {
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
lappend tokens $vtok
set p $next
set textStart $p
}
}
"\[" {
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
set q [expr {$p + 1}]
while {1} {
lassign [parse_command_bytes $bytes $q $endpos 1] cs cl ks kl q2 term tr
set q $q2
if {$term eq "bracket"} {
incr q
break
}
if {$term eq "eof"} {
error "missing close-bracket"
}
#nl or semi: further commands inside the brackets
}
lappend tokens [list command [list $p [expr {$q - $p}]] {}]
set p $q
set textStart $p
}
default {
incr p
}
}
}
if {$p > $textStart} {
lappend tokens [list text [list $textStart [expr {$p - $textStart}]] {}]
}
return [list $tokens $p]
}
proc parse_varname {bytes pos endpos} {
#pos at '$'. Returns {token nextpos}, or {"" pos} when the $ does not
#introduce a variable (plain text).
set p [expr {$pos + 1}]
if {$p >= $endpos} {
return [list "" $pos]
}
set c [string index $bytes $p]
if {$c eq "\{"} {
#${name}: Tcl 8.x scans to the FIRST close brace (no escapes);
#Tcl 9 balances nested braces with backslash-escaped braces not
#counted (the backslash stays part of the name text).
variable TCL9
if {$TCL9} {
set level 1
set q [expr {$p + 1}]
while {1} {
if {$q >= $endpos} {
error "missing close-brace for variable name"
}
set c2 [string index $bytes $q]
switch -exact -- $c2 {
"\\" {
incr q 2
}
"\{" {
incr level
incr q
}
"\}" {
incr level -1
if {$level == 0} {
break
}
incr q
}
default {
incr q
}
}
}
set close $q
} else {
set close [string first "\}" $bytes $p]
if {$close < 0 || $close >= $endpos} {
error "missing close-brace for variable name"
}
}
set nametok [list text [list [expr {$p + 1}] [expr {$close - $p - 1}]] {}]
set next [expr {$close + 1}]
return [list [list variable [list $pos [expr {$next - $pos}]] [list $nametok]] $next]
}
set nameStart $p
while {$p < $endpos} {
set c [string index $bytes $p]
if {[string match {[a-zA-Z0-9_]} $c]} {
incr p
continue
}
if {$c eq ":" && $p + 1 < $endpos && [string index $bytes $p+1] eq ":"} {
incr p 2
while {$p < $endpos && [string index $bytes $p] eq ":"} {
incr p
}
continue
}
break
}
set nameLen [expr {$p - $nameStart}]
if {$p < $endpos && [string index $bytes $p] eq "("} {
#array element - name may be empty. Index parsed for
#substitutions, terminated by the first top-level close paren.
incr p
set idxstart $p
lassign [parse_tokens $bytes $p $endpos index 0] idxtokens p
if {$p >= $endpos || [string index $bytes $p] ne ")"} {
error "missing )"
}
if {[llength $idxtokens] == 0} {
#an empty index still carries one empty text token
set idxtokens [list [list text [list $idxstart 0] {}]]
}
incr p
set nametok [list text [list $nameStart $nameLen] {}]
set subnodes [list $nametok]
lappend subnodes {*}$idxtokens
return [list [list variable [list $pos [expr {$p - $pos}]] $subnodes] $p]
}
if {$nameLen == 0} {
return [list "" $pos]
}
set nametok [list text [list $nameStart $nameLen] {}]
return [list [list variable [list $pos [expr {$p - $pos}]] [list $nametok]] $p]
}
proc list_elements {bytes start end} {
#Scan the byte range as a Tcl list (TclFindElement semantics) for
#parse-time {*} literal expansion. Returns a list of
#{elemstart elemlen textstart textlen} with text excluding one level
#of brace/quote enclosure. Errors if the range is not a valid list.
set p $start
set out [list]
while {1} {
while {$p < $end && [string index $bytes $p] in [list " " \t \n \r \f \v]} {
incr p
}
if {$p >= $end} {
break
}
set c [string index $bytes $p]
set estart $p
set kind bare
if {$c eq "\{"} {
set kind brace
set level 1
incr p
set tstart $p
while {1} {
if {$p >= $end} {
error "unmatched open brace in list"
}
set c2 [string index $bytes $p]
if {$c2 eq "\\"} {
incr p 2
continue
}
if {$c2 eq "\{"} {
incr level
} elseif {$c2 eq "\}"} {
incr level -1
if {$level == 0} {
break
}
}
incr p
}
set tlen [expr {$p - $tstart}]
incr p
if {$p < $end && [string index $bytes $p] ni [list " " \t \n \r \f \v]} {
error "list element in braces followed by \"[string index $bytes $p]\" instead of space"
}
} elseif {$c eq "\""} {
set kind quote
incr p
set tstart $p
while {1} {
if {$p >= $end} {
error "unmatched open quote in list"
}
set c2 [string index $bytes $p]
if {$c2 eq "\\"} {
incr p 2
continue
}
if {$c2 eq "\""} {
break
}
incr p
}
set tlen [expr {$p - $tstart}]
incr p
if {$p < $end && [string index $bytes $p] ni [list " " \t \n \r \f \v]} {
error "list element in quotes followed by \"[string index $bytes $p]\" instead of space"
}
} else {
set tstart $p
while {$p < $end} {
set c2 [string index $bytes $p]
if {$c2 in [list " " \t \n \r \f \v]} {
break
}
if {$c2 eq "\\"} {
incr p 2
continue
}
incr p
}
if {$p > $end} {
set p $end
}
set tlen [expr {$p - $tstart}]
}
#Parse-time literal expansion only applies when every element's
#value equals its source bytes. A backslash in a bare or quoted
#element (or backslash-newline in a braced one) needs collapsing,
#so the whole expansion stays a runtime matter (expand node).
set seg [string range $bytes $tstart [expr {$tstart + $tlen - 1}]]
if {$kind eq "brace"} {
if {[string first "\\\n" $seg] >= 0} {
error "list element needs backslash collapsing"
}
} else {
if {[string first "\\" $seg] >= 0} {
error "list element needs backslash collapsing"
}
}
lappend out [list $estart [expr {$p - $estart}] $tstart $tlen]
}
return $out
}
}
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::tclparser
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package provide punk::tclparser [tcl::namespace::eval punk::tclparser {
variable pkg punk::tclparser
variable version
set version 0.1.0
}]
## Ready
return

14
src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm → src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.2.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.6.0
# Application punkboot::utils 0.6.2
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -126,7 +126,7 @@ namespace eval punkboot::utils {
-help\
"Walks up from path to the nearest fossil and/or git root and
returns warning lines describing uncommitted changes.
Artifacts built from a dirty source tree have no committed
Artifacts produced from a dirty source tree have no committed
provenance - callers such as make.tcl vendorupdate use this
to flag source projects that should be committed first.
@ -206,7 +206,7 @@ namespace eval punkboot::utils {
lappend flines $line
}
if {[llength $flines]} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts built from a dirty tree have no committed provenance"
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes${scopedesc} ([llength $flines] file(s)) - artifacts produced from a dirty tree have no committed provenance"
}
}
cd $original_cwd
@ -222,7 +222,7 @@ namespace eval punkboot::utils {
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes${scopedesc} ([llength [split $gchanges \n]] file(s)) - artifacts produced from a dirty tree have no committed provenance"
}
}
return $warnings
@ -749,7 +749,7 @@ namespace eval punkboot::utils {
punkshell bake pipeline emits ARCHIVE-relative payloads by
construction; this probe makes that a checked contract - a
'file' result is the pipeline-regression signal make.tcl
surfaces as a recapped BUILD-WARNING (advisory: the kit still
surfaces as a recapped BAKE-WARNING (advisory: the kit still
builds and deploys). 'plain' (the file is a bare zip), 'none'
(no zip attached - e.g the metakit kit shape, or any non-zip
file) and 'unreadable' are silence, not warnings: the pin
@ -805,8 +805,8 @@ namespace eval ::punk::args::register {
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.6.0, is a literal used in src module folders
#- this version number, exactly 0.6.2, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.6.0
set version 0.6.2
}]
return

8
src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.4.tm

@ -747,6 +747,14 @@ namespace eval shellfilter::chan {
method finalize {transform_handle} {
my destroy
}
#G-145 warning (2026-08-01): the Tcl core delivers the transform 'clear' op
#between ordinary write flush-downs (observed before every 'write' on
#file/pipe channels, tclsh 8.7 and 9.0.3) - it is not a rare seek/truncate
#event. Any future 'clear' method here must therefore NOT discard o_encbuf
#(held partial multi-byte character) or other stream state: dropping held
#state on 'clear' corrupts content split across write chunks. This is the
#defect class behind the ::punkboot::ansistrip carry-drop remnant bug in
#src/make.tcl - see goals/G-145-piped-usage-ansi-remnants.md.
#method clear {transform_handle} {
# return
#}

BIN
src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm

Binary file not shown.

BIN
src/vfs/_vfscommon.vfs/modules/test/punk/ansi-0.1.1.tm

Binary file not shown.

BIN
src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm

Binary file not shown.

BIN
src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm

Binary file not shown.

BIN
src/vfs/_vfscommon.vfs/modules/test/punk/ns-0.1.0.tm

Binary file not shown.

1536
src/vfs/_vfscommon.vfs/modules/voo-1.0.0.tm

File diff suppressed because it is too large Load Diff

BIN
src/vfs/_vfscommon.vfs/modules/zipper-0.14.tm

Binary file not shown.
Loading…
Cancel
Save