diff --git a/src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm b/src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm index 8dfa9474..4b1e2384 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm and b/src/vfs/_vfscommon.vfs/modules/gridplus-2.12b0.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm index 827b97bf..905a1024 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm and b/src/vfs/_vfscommon.vfs/modules/modpodtest-0.1.1.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.12.7.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.14.0.tm similarity index 98% rename from src/vfs/_vfscommon.vfs/modules/punk/args-0.12.7.tm rename to src/vfs/_vfscommon.vfs/modules/punk/args-0.14.0.tm index 0b52a7c3..4b257760 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.12.7.tm +++ b/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 # @@ 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 ' 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 classes } + -formerrors -argspecs + 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 } + 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 classes - #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 '':" 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 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/lib-0.6.0.tm similarity index 98% rename from src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.1.tm rename to src/vfs/_vfscommon.vfs/modules/punk/lib-0.6.0.tm index 7013a8e5..11a1e0c6 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/lib-0.5.1.tm +++ b/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 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.1.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.2.0.tm similarity index 92% rename from src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.1.2.tm rename to src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.2.0.tm index b91bdca2..10efc821 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.1.2.tm +++ b/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 -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 /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). diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.5.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.6.0.tm similarity index 95% rename from src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.5.2.tm rename to src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.6.0.tm index 47d523f7..99de7593 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/cli-0.5.2.tm +++ b/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 # @@ 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 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.3.0.tm similarity index 79% rename from src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.2.0.tm rename to src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.3.0.tm index f8ba5e98..7dc51b93 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/layout-0.2.0.tm +++ b/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 # @@ 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 /{vendor,custom}// 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 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.1.tm similarity index 99% rename from src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.0.tm rename to src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.1.tm index 7f36384f..6935ba54 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.4.0.tm +++ b/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 # @@ 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 \"" 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 diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm index 132f49d4..13fb1158 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm and b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/punk/tclparser-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/tclparser-0.1.0.tm new file mode 100644 index 00000000..a1e989b1 --- /dev/null +++ b/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 -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 +# parse getstring +# parse countnewline ?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 diff --git a/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm b/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.2.tm similarity index 98% rename from src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm rename to src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.2.tm index 43372880..da683f94 100644 --- a/src/vfs/_vfscommon.vfs/modules/punkboot/utils-0.6.0.tm +++ b/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 diff --git a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.4.tm b/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.4.tm index d060aced..0bf17fd8 100644 --- a/src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.4.tm +++ b/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 #} diff --git a/src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm b/src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm index 816d8e57..cc4d3452 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm and b/src/vfs/_vfscommon.vfs/modules/test/overtype-1.7.4.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/test/punk/ansi-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/test/punk/ansi-0.1.1.tm index 460e8eba..385a39ab 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/test/punk/ansi-0.1.1.tm and b/src/vfs/_vfscommon.vfs/modules/test/punk/ansi-0.1.1.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm b/src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm index 77956f00..8dc58bef 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm and b/src/vfs/_vfscommon.vfs/modules/test/punk/args-0.1.5.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm b/src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm index 08b3514e..e0209ecd 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm and b/src/vfs/_vfscommon.vfs/modules/test/punk/lib-0.1.3.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/test/punk/ns-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/test/punk/ns-0.1.0.tm index faeaf8bf..8fcec319 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/test/punk/ns-0.1.0.tm and b/src/vfs/_vfscommon.vfs/modules/test/punk/ns-0.1.0.tm differ diff --git a/src/vfs/_vfscommon.vfs/modules/voo-1.0.0.tm b/src/vfs/_vfscommon.vfs/modules/voo-1.0.0.tm index a70a19be..64262b07 100644 --- a/src/vfs/_vfscommon.vfs/modules/voo-1.0.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/voo-1.0.0.tm @@ -1,768 +1,768 @@ - -#https://github.com/AlaoPrado/voo -#License: MIT - -namespace eval voo { - # package version - variable version 1.0.0 - variable handlerToObjectMap {} - variable handlerCounter 0 - - - ##\brief Check if a namespace is a valid voo class - # \param[in] namespaceName the namespace to check - # \return 1 if valid voo class, 0 otherwise - proc isVooClass {namespaceName} { - if {![uplevel [list namespace exists $namespaceName]]} { - return 0 - } - return [expr {[uplevel [list namespace eval $namespaceName { - info exists __defaultObj - }]]}] - } - - ##\brief Declare a new voo class namespace and process its class body - # \param[in] args Arguments for class declaration: and optional -extends parent - # \note Creates the class namespace, imports parent fields/methods when using -extends, - # and registers constructors and exports - proc class {args} { - set optDict {} - set defaultArgs {} - set numArgs [llength $args] - for {set i 0} {$i < $numArgs} {incr i} { - set arg [lindex $args $i] - if {$arg eq "-extends"} { - if {$i + 1 >= $numArgs} { - error "Constructor option ’$arg’ requires an argument" - } - dict set optDict $arg [lindex $args [incr i]] - } elseif {$arg eq "-virtual" || $arg eq "-v"} { - dict set optDict "-virtual" {} - } else { - lappend defaultArgs $arg - } - } - lassign $defaultArgs className body - set vooNs [namespace current] - # create the namespace for the class - uplevel [list namespace eval $className [subst -nocommands { - namespace path [list $vooNs] - variable __defaultObj {} - variable __fields {} - variable __tmp_isPublicEnabled 1 - }]] - - uplevel [list namespace eval $className { - ##\brief Access default object for this class - # \return Default class instance (list) - # \note Used for inheritance and constructor defaults - proc class.defaultObj {} { - variable __defaultObj - return $__defaultObj - } - ##\brief Get list of field names for this class - # \return List of field names in declaration order - # \note Useful for introspection and constructor -name new.args - proc class.fields {} { - variable __fields - return $__fields - } - }] - - if {[dict exists $optDict -virtual] && [dict exists $optDict -extends]} { - error "voo::class: cannot use -virtual with -extends; child classes inherit virtual automatically from a -virtual parent" - } - - if {[dict exists $optDict -virtual]} { - set normalizedClassName [uplevel [list namespace eval $className {namespace current}]] - uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]] - uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedClassName]] - # Pre-populate __defaultObj with namespace tag at index 0 BEFORE field declarations - # so that _getClassCurrNumFields returns 1 for the first field declared - uplevel [list namespace eval $className [list set __defaultObj [list $normalizedClassName]]] - } - - #81 - # variable __parentClassNamespace {} - if {[dict exists $optDict -extends]} { - set parentClassName [dict get $optDict -extends] - - if {![uplevel [list namespace exists $parentClassName]]} { - error "Parent class ’$parentClassName’ does not exist." - } - - # check if parent class exists - if {![uplevel [list namespace eval $parentClassName {info exists __defaultObj}]]} { - error "Parent class ’$parentClassName’ is not a valid voo class." - } - - # normalize namespace name of parent class - set parentClassName [uplevel [list namespace eval $parentClassName { - namespace current - }]] - - uplevel [list namespace eval $className [subst -nocommands { - variable __parentClassNamespace $parentClassName - }]] - - # import parent’s default object values - set parentDefaultObj [${parentClassName}::class.defaultObj] - uplevel [list namespace eval $className [list set __defaultObj $parentDefaultObj]] - - - # if parent is virtual, update namespace tag at index 0 to child’s namespace - set parentIsVirtual [uplevel [list namespace eval $parentClassName {info exists __voo_is_virtual_class}]] - if {$parentIsVirtual} { - set normalizedChildName [uplevel [list namespace eval $className {namespace current}]] - uplevel [list namespace eval $className \ - [list set __defaultObj [lreplace $parentDefaultObj 0 0 $normalizedChildName]]] - uplevel [list namespace eval $className [list variable __voo_is_virtual_class 1]] - uplevel [list namespace eval $className [list variable __voo_class_namespace $normalizedChildName]] - } - - # 121 - # import parent’s field index variables by copying actual index values from parent - set parentFields [${parentClassName}::class.fields] - foreach field $parentFields { - set fieldIdx [uplevel [list namespace eval $parentClassName [list set $field]]] - uplevel [list namespace eval $className [list variable $field $fieldIdx]] - uplevel [list namespace eval $className [list lappend __fields $field]] - } - - # import parent’s acessors in child class with namespace import - uplevel [list namespace eval $className [subst -nocommands { - namespace import ${parentClassName}::get.* - namespace import ${parentClassName}::set.* - namespace import ${parentClassName}::update.* - }]] - } - - - # 136 - - uplevel [list namespace eval $className $body] - - uplevel [list namespace eval $className { - if {[info commands new] eq ""} { - constructor - } - if {[info commands new()] eq ""} { - constructor -noargs [_buildConstructorNoArgsBody] - } - if {[info commands new.args] eq ""} { - constructor -name new.args {args} [_buildConstructorArgsBody] - } - }] - - - # 151 - uplevel [list namespace eval $className { - # export class methods - namespace export * - }] - - uplevel [list namespace eval $className { - # clean temporary variable - unset __tmp_isPublicEnabled - }] - return - } - - # 161 - ##\brief Return the default value for a given field type - # \param[in] type the field type token (double,int,bool,...) - # \return The default value appropriate for the type - proc _getDefaultValueByType {type} { - switch -- $type { - double { return 0.0 } - int { return 0 } - bool { return 0 } - default { return {} } - } - } - - ##\brief Get the current number of fields declared in the current class - # \return Number of fields (integer) - proc _getClassCurrNumFields {} { - return [uplevel 2 {llength $__defaultObj}] - } - - ##\brief Check whether public mode is enabled during class body parsing - # \return 1 if public mode is enabled, 0 otherwise - proc _getClassIsPublicEnabled {} { - return [uplevel 2 {set __tmp_isPublicEnabled}] - } - - ##\brief Declare getter/setter/updater accessors for a class field - # \param[in] fieldName name of the field - # \param[in] isPublic boolean whether accessors are public - # \param[in] isStatic boolean whether field is static (class-level) - proc _declareFieldAcessors {fieldName isPublic isStatic} { - set prefix {} - - if {$isStatic} { - append prefix class. - } - if {!$isPublic} { - append prefix my. - } - set getterName "${prefix}get.$fieldName" - set setterName "${prefix}set.$fieldName" - set updaterName "${prefix}update.$fieldName" - if {$isStatic} { - uplevel 2 [list proc $getterName {} [subst -nocommands { - variable $fieldName - return $$fieldName - }]] - uplevel 2 [list proc $setterName {value} [subst -nocommands { - variable $fieldName - set $fieldName "\$value" - }]] - - uplevel 2 [list proc $updaterName {tempVar body} [subst -nocommands { - variable $fieldName - upvar "\$tempVar" temp - set temp $$fieldName - # break link with class variable to avoid copy-on-write - set $fieldName {} - try { - uplevel \$body - } finally { - set $fieldName "\$temp" - } - }]] - - } else { - uplevel 2 [list getter $getterName $fieldName] - uplevel 2 [list setter $setterName $fieldName] - uplevel 2 [list updater $updaterName $fieldName] - } - return - } - - ##\brief Validate a field name for illegal characters - # \param[in] fieldName the field name to validate - # \return Raises an error if invalid - proc _validateFieldName {fieldName} { - if {[string first "." $fieldName] != -1 || [string first "::" $fieldName] != -1} { - error "Field name ’$fieldName’ cannot contain ’.’ or ’::’ substrings." - } - } - - ##\brief Ensure a field name does not already exist in the class - # \param[in] fieldName the field name to check - # \return Raises an error if the field already exists - # \note Uses __fields for instance fields and fully-qualified namespace lookup for static - # fields to avoid false positives from global variables with the same name - proc _validateFieldDoesNotExist {fieldName} { - # Check instance fields tracked in __fields (class-scoped, no global bleed) - if {$fieldName in [uplevel 2 {set __fields}]} { - error "Field name ’$fieldName’ already exists in the class." - } - # Check static fields via fully-qualified namespace variable; info exists ::Ns::var - # only matches that exact namespace variable, never a same-named global - set classNs [uplevel 2 {namespace current}] - if {[info exists ${classNs}::$fieldName]} { - error "Field name ’$fieldName’ already exists in the class." - } - } - - ##\brief Validate a variable initial value according to its declared type - # \param[in] type the declared type (double,int,bool,list,dict) - # \param[in] value the value to validate - # \return Raises an error if the value does not match the type - proc _validateVarValueByType {type value} { - switch -- $type { - double { - if {[string is double -strict $value] == 0} { - error "Value for t_double must be a double, got ’$value’" - } - } - int { - if {[string is integer -strict $value] == 0} { - error "Value for t_int must be an integer, got ’$value’" - } - } - bool { - if {[string is boolean -strict $value] == 0} { - error "Value for t_bool must be a boolean, got ’$value’" - } - } - list { - if {[catch {llength $value}]} { - error "Value for t_list must be a list, got ’$value’" - } - } - dict { - if {[catch {dict size $value}]} { - error "Value for t_dict must be a dict, got ’$value’" - } - } - } - } - - ##\brief Declare a field variable inside the class body - # \param[in] type the field type token (double,int,string,bool,list,dict,obj) - # \param[in] argList arguments: ?-static? ?? - proc _var {type argList} { - set defaultArgs {} - set optDict {} - set numArgs [llength $argList] - for {set i 0} {$i < $numArgs} {incr i} { - set arg [lindex $argList $i] - if {$arg eq "-static"} { - dict set optDict $arg {} - } else { - lappend defaultArgs $arg - } - } - if {[llength $defaultArgs] == 0} { - error "Variable definition requires: ?