From 5c69954e7e432b1bf355cef7803b112f7a0b6b85 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 7 Jun 2023 06:13:53 +1000 Subject: [PATCH] fix a lot of pipeline semantics - especially assignment --- src/modules/punk-0.1.tm | 907 +++++++++++++++++----------------------- 1 file changed, 387 insertions(+), 520 deletions(-) diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index 9a390773..877b2ad4 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -43,6 +43,7 @@ namespace eval punk::config { variable loaded variable startup ;#include env overrides variable running + variable known_punk_env_vars variable vars @@ -86,7 +87,6 @@ namespace eval punk::config { #env vars override the configuration #todo - define which configvars are settable in env - variable known_punk_env_vars set known_punk_env_vars [list \ PUNK_APPS \ PUNK_SCRIPTLIB \ @@ -113,7 +113,6 @@ namespace eval punk::config { } unset -nocomplain evar unset -nocomplain vars - unset -nocomplain known_punk_env_vars set running [dict create] set running [dict merge $running $startup] @@ -213,24 +212,6 @@ namespace eval punk { expr {($a && 1) == ($b && 1)} } - proc know {cond body} { - set existing [info body ::unknown] - #assuming we can't test on cond being present - because it may be fairly simple and prone to false positives (?) - ##This means we can't have 2 different conds with same body. Not a big drawback. - #if {$body ni $existing} { - proc ::unknown {args} [string map [list @c@ $cond @b@ $body] { - #--------------------------------------- - debug.punk.unknown {punk unknown_handler $args} 4 - if {![catch {expr {@c@}} res] && $res} { - return [eval {@b@}] - } - #--------------------------------------- - }]$existing - #} - } - proc know? {{len 2000}} { - puts [string range [info body ::unknown] 0 $len] - } proc varinfo {vname {flag ""}} { upvar $vname v @@ -891,11 +872,12 @@ namespace eval punk { #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 if {![string length $multivar]} { #treat the absence of a pattern as a match to anything - return [dict create ismatch 1 result $data setvars {} unsetvars {}] + return [dict create ismatch 1 result $data setvars {} script {}] } - set returndict [dict create ismatch 0 result "" setvars {} unsetvars {}] + set returndict [dict create ismatch 0 result "" setvars {}] + set script "" - set defaults [list -unset 0 -levelup 2 -mismatchinfo 1] + set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] set opts [dict merge $defaults $args] set unset [dict get $opts -unset] set lvlup [dict get $opts -levelup] @@ -938,7 +920,7 @@ namespace eval punk { set expected_values [list] #set expected_values [lmap v $var_names {list $v "-" ""}] - #e.g {a = abc} {b unset ""} + #e.g {a = abc} {b set ""} foreach v_key $valsource_key_list { lassign $v_key v key set vname $v ;#default @@ -1034,7 +1016,6 @@ namespace eval punk { #member lists of returndict which will be appended to in the initial value-retrieving loop set returndict_setvars [dict get $returndict setvars] - set returndict_unsetvars [dict get $returndict unsetvars] set assigned_values [list] @@ -1044,12 +1025,9 @@ namespace eval punk { # "" unconfigured - assert none remain unconfigured at end # noop no-change # matchvar-set name is a var to be matched - # matchvar-unset # matchatom-set names is an atom to be matched - # matchatom-unset # matchglob-set # set - # unset # question mark versions are temporary - awaiting a check of action vs var_class # e.g ?set may be changed to matchvar or matchatom or set @@ -1063,7 +1041,7 @@ namespace eval punk { # (for consistency and to assist with returnval) # ^var means a pinned variable - compare value of $var to rhs - don't assign # - # In this loop we don't set or unset variables - but assign an action entry in var_actions - all with leading question mark. + # In this loop we don't set variables - but assign an action entry in var_actions - all with leading question mark. # as well as adding the data values to the var_actions list foreach v_and_key $varspecs_trimmed { set vspec [join $v_and_key ""] @@ -1082,19 +1060,6 @@ namespace eval punk { #if {[string is integer -strict $v]} { # lset var_actions $i 1 matchatom #} - if {$unset} { - #variable unset traces can't raise an error - so presumably the only error we can get is the built-in no such variable error - #we don't want unset of a nonexistent variable to raise an error here.. - #REVIEW - does it really matter? Would consistency with standard tcl 'unset var' be better? - #if {[string length $v]} { - # catch {uplevel $lvlup [list unset $v]} - #} - lset var_actions $i 1 ?unset - set assigned "" - lappend assigned_values $assigned - incr i - continue - } # if @# is found - remove the # and set a flag to indicate we are returning the length/size # for @#@path - size of dict at the level specified by the path @@ -1269,17 +1234,6 @@ namespace eval punk { } } else { #no vkey - whole of RHS to be applied - - if {$unset} { - #if {[string length $v]} { - # uplevel $lvlup [list unset -nocomplain $v] - #} - lset var_actions $i 1 ?unset - set assigned "" - lappend assigned_values $assigned - incr i - continue - } set assigned $data lset var_actions $i 1 ?set lset var_actions $i 2 $assigned @@ -1290,20 +1244,13 @@ namespace eval punk { #update the setvars/unsetvars elements if {[string length $v]} { - if {$unset} { - if {$v ni $returndict_unsetvars} { - lappend returndict_unsetvars $v - } - } else { - dict set returndict_setvars $v $assigned - } + dict set returndict_setvars $v $assigned } lappend assigned_values $assigned incr i } dict set returndict setvars $returndict_setvars - dict set returndict unsetvars $returndict_unsetvars set returnval [lindex $assigned_values 0] @@ -1383,11 +1330,6 @@ namespace eval punk { lset expected_values $i [list var $varname spec $lhsspec info strings-not-equal lhs $lhs rhs $val] break } - } elseif {$act eq "?unset"} { - #doesn't make sense for an atom ? - should fail match - lset match_state $i 0 - lset expected_values $i [list var $varname spec $lhsspec info "unable-to-unset-atom" lhs $lhs rhs $val] - break } else { lset match_state $i 0 lset expected_values $i [list var $varname spec $lhsspec info unkown-action-$act lhs $lhs rhs $val] @@ -1454,19 +1396,6 @@ namespace eval punk { break } } - if {$act in [list "?unset" "?matchvar-unset"]} { - lset var_actions $i 1 matchvar-unset - upvar $lvlup $varname the_var - if {![info exists the_var]} { - lset match_state $i 1 - lset expected_values $i [list var $varname spec $lhsspec info match-already-unset lhs "" rhs ""] - } else { - #attempt to unset a pinned var that has a value - non-match. ^x= will only match an unset variable x - lset match_state $i 0 - lset expected_values $i [list var $varname spec $lhsspec info attempt-to-unset-pinned-var-with-value lhs [set the_var] rhs ""] - break - } - } } @@ -1641,7 +1570,7 @@ namespace eval punk { } else { #puts stdout "==> $lhsspec" - #unpinned non-atoms will be set/unset - always considered a match + #unpinned non-atoms will be set- always considered a match lset match_state $i 1 lset var_actions $i 1 [string range $act 1 end] } @@ -1650,7 +1579,7 @@ namespace eval punk { } #-------------------------------------------------------------------------- - #Variable assignments (set/unset) should only occur down here, and only if we have a match + #Variable assignments (set) should only occur down here, and only if we have a match #-------------------------------------------------------------------------- set match_count_needed [llength $var_actions] #set match_count [expr [join $match_state +]] ;#expr must be unbraced here @@ -1674,8 +1603,6 @@ namespace eval punk { upvar $lvlup $varname the_var if {[lindex $var_actions $i 1] eq "set"} { set the_var $val - } elseif {[lindex $var_actions $i 1] eq "unset"} { - unset -nocomplain the_var } } incr i @@ -1815,138 +1742,158 @@ namespace eval punk { } #same as used in unknown func for initial launch - #variable re_assign {^([^\r\n=\{]*)=(.*)} - variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} + variable re_assign {^([^ \t\r\n=\{]*)=(.*)} variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} #match_assign is tailcalled from unknown - uplevel 1 gets to caller level - proc match_assign {multivar equalsrhs fulltail} { - #equalsrhs is set if ther is something *directly* after the = - debug.punk.pipe {match_assign '$multivar' '$equalsrhs' '$fulltail'} 4 - #can match an integer on lhs with a value - # - #if {[string is integer -strict $multivar]} { - # #todo - implement matching - # error "Cannot set a var named '$multivar' using this syntax. use == for comparison, or use set $multivar if you really want a variable named like a number." - #} - + proc match_assign {scopepattern equalsrhs args} { + set fulltail $args + #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = + #debug.punk.pipe {match_assign '$multivar' '$equalsrhs' '$fulltail'} 4 + #can match pattern on lhs with a value where pattern is a minilang that can refer to atoms (simple non-whitespace strings), numbers, or varnames (possibly pinned) as well as a trailing spec for position within the data. - # allow x=y to begin a pipeline e.g x=y |> string tolower ? - #assigning an entire pipeline string to x using the 'equals-runon' syntax requires an exception. Just "%" in equalsrhs position to be handled differently. x=% .=something etc |> blah - #Review - is this breaking of consistency really worthwhile? we could always require standard tcl assignment for pipelines - which are a list anyway - # more explicit and consistent would be a command that takes args: - # pipeset var % .= etc ... - # lappend var string tolower ? or x=1 a b c <| X to produce a X b c # - if {[llength $fulltail]} { - #avoid use of regexp match on each element - or we will unnecessarily force string reps on lists - #set firstlast [lmap v $fulltail {lreplace [split $v {}] 1 end-1}] - #set firstpipe_posn [lsearch $firstlast {| >}] - set firstpipe_posn [lsearch $fulltail "|*>"] - set argpipe_posn [lsearch $fulltail "<*|"] - if {$firstpipe_posn == -1} { - set endsegment_posn $argpipe_posn - } elseif {$argpipe_posn == -1} { - set endsegment_posn $firstpipe_posn - } else { - set endsegment_posn [expr {min($firstpipe_posn, $argpipe_posn)}] - } - set pipe_args [list] - if {$endsegment_posn >=0} { - #defer to pipeline command for all pipelines. - tailcall punk::pipeline = $multivar $equalsrhs {*}$fulltail - - - set segmenttail [lrange $fulltail 0 $endsegment_posn-1] - set firstpipe [lindex $fulltail $endsegment_posn] - set nextassignment [lindex $fulltail $endsegment_posn+1] - set nexttail [lrange $fulltail $endsegment_posn+1 end] - if {$argpipe_posn >= 0} { - set pipe_args [lrange $fulltail $argpipe_posn+1 end] + #to assign an entire pipeline to a var - use pipeset varname instead. + + set script [string map [list $scopepattern $equalsrhs] { + if {[llength $args]} { + #avoid use of regexp match on each element - or we will unnecessarily force string reps on lists + if {[lsearch $args "|*>"] >=0 || [lsearch $args "<*|"] >= 0} { + #defer to pipeline command for all pipelines. + tailcall punk::pipeline = "" "" {*}$args + } else { + if {[llength $args] == 1} { + set segmenttail [lindex $args 0] + } else { + error "pipesyntax = must take a single argument" + } } } else { - set segmenttail $fulltail - set nextassignment [list] - set nexttail [list] + #set segmenttail [purelist] + set segmenttail [lreplace x 0 0] } - #puts stderr "tail len: [llength $fulltail]" - #puts stderr "tail-end: [lindex $fulltail end]" - } else { - set firstpipe_posn -1 - set argpipe_posn -1 - set segmenttail [list] - set nextassignment [list] - set nexttail [list] - } + }] - set is_listbuilder 0 - if {![string length $equalsrhs]} { - #space after = - if {[llength $segmenttail] == 0} { - - #no longer do unset in pattern-matching ? - #set d [_multi_bind_result $multivar [purelist] -unset 1] ;#final arg 1 to unset variables - #_handle_bind_result $d ;# we can get a mismatch on unsetting a pinned var - so we need _handle_bind_result to give a chance to raise an error etc. - #set returnval "" - } elseif {[llength $segmenttail] == 1} { - #set val [lindex $segmenttail 0] - set d [_multi_bind_result $multivar [lindex [list [lindex $segmenttail 0] [unset segmenttail]] 0]] - set returnval [_handle_bind_result $d] - } else { - #keyword pipesyntax at beginning of error message - set msg "pipesyntax\n" - append msg "Assignment with = accepts only zero or one argument, unless characters immediately follow the = sign.\n" - append msg "Characters immediately after the equals sign form the first element of a list if there is *any* literal whitespace\n" - append msg "e.g x=\"abc\" will assign \"abc\" including the quotes\n" - append msg "but x=\"ab c\" will form a two element list containing \"ab and c\" \n" - append msg "Note the whitespace is interpreted by Tcl as a list separator and collapsed to one space\n" - append msg "To use semantics more equivalent to 'set' leave a space after the = e.g x= \"a b \"\n" - append msg "Note in particular, that for something like: x=\"a b \"\n" - append msg "The second quote is actually the operning quote for the 3rd list element\n" - append msg "so the interpreter or commandline will consume following lines until a closing quote is found\n" - error $msg + if {[string length $equalsrhs]} { + # as we aren't in a pipleine - there is no data to insert - we proably still need to run _split_equalsrhs to verify the syntax. + # review - consider way to turn it off as optimisation for non-pipelined assignment - but generally standard Tcl set could be used for that purpose. + # We are probably only here if testing in the repl - in which case the error messages are important. + set var_position_list [_split_equalsrhs $equalsrhs] + #we may have an insertion-spec that inserts a literal atom e.g to wrap in "ok" + # x='ok'/0 data + # we won't examine for vars as there is no pipeline - ignore + # also ignore trailing * (indicator for variable data to be expanded or not - ie {*}) + # we will differentiate between / and @ in the same way that general pattern matching works. + # /x will simply call linsert without reference to length of list + # @x will check for out of bounds + # + # !TODO - sort by position lowest to highest? or just require user to order the pattern correctly? + + + + foreach v_pos $var_position_list { + lassign $v_pos v positionspec + set offset 0 + if {[string index $v 0] eq "'"} { + set positionspec [string trimright $positionspec "*"] + set ptype [string index $positionspec 0] + set index [string range $positionspec 1 end] + set isint [string is integer -strict $index] + if {$isint || [string match "end*" $index]} { + set v [string range $v 1 end-1] ;#assume trailing ' is present! + if {$ptype eq "@"} { + #compare position to *original* list - note use of $index > $datalen rather than $index+1 > $datalen - (we allow 'insertion' at end of list by numeric index) + if {$isint} { + append script [string map [list $index] { + if {( > [llength $segmenttail])} { + error "insertionpattern index out of bounds. index: vs len: [llength $segmenttail] use /x instead of @x to avoid check" + } + }] + } + #todo check end-x bounds? + } + if {$isint} { + #set segmenttail [linsert $segmenttail $index+$offset $v] + append script [string map [list $v [expr {$index + $offset}]] { + #set segmenttail [linsert $segmenttail ] + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] ] + }] + } else { + #todo - review/test! + set endindex [string range $index 4 end] + if {[string length $endindex]} { + incr endindex -$offset + set idx "end-$endindex" + } else { + set idx "end" + } + #set segmenttail [linsert $segmenttail $idx $v] + append script [string map [list $v $idx] { + #set segmenttail [linsert $segmenttail ] + #use inline K to make sure the list is unshared (optimize for larger lists) + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] ] + }] + + } + incr offset + } else { + error "pipesyntax error in segment insertionpattern - v $v unable to interpret position spec" + } + } + } + + + } + + if {![string length $scopepattern]} { + append script { + return $segmenttail } - } elseif {([llength $segmenttail] == 0) && ($firstpipe_posn == -1)} { - #simple value assignment - even if it looks like an expression - #ie x=4+1 assigns "4+1" as a string - #whereas x=4 + 1 assigns 5 - #set commaparts [split $var ,] - set d [_multi_bind_result $multivar [purelist $equalsrhs]] - set returnval [_handle_bind_result $d] } else { - set is_listbuilder 1 - #no space concatenation - good for command aliases - debug.punk.pipe "assigning fulltail [llength $fulltail]" 6 - #equalsrhs is not a list - may even be a single char such as double quote. - #set result [concat $equalsrhs $fulltail] ;#concat produces a string rep - and strips escaped whitespace e.g \t or\n from equalsrhs and trailing args. - - #set result [list] - #lappend result $equalsrhs - #foreach a $fulltail { - # lappend result $a - #} + append script [string map [list $scopepattern] { + #we still need to bind whether list is empty or not to allow any patternmatch to succeed/fail + set d [punk::_multi_bind_result "" $segmenttail] + #return [punk::_handle_bind_result $d] + #maintenance: inlined + if {![dict exists $d result]} { + #uplevel 1 [list error [dict get $d mismatch]] + error [dict get $d mismatch] + } else { + return [dict get $d result] + } + }] + } + uplevel 1 [list proc $scopepattern=$equalsrhs args $script] - #set result [list] - #lappend result $equalsrhs {*}$fulltail - - set result [list $equalsrhs {*}$fulltail] + tailcall $scopepattern=$equalsrhs {*}$args + } - set d [_multi_bind_result $multivar $result] - set returnval [_handle_bind_result $d] - } - #return $returnval - if {![llength $nexttail] || $is_listbuilder} { - return $returnval - } else { - set exectail [list val $returnval $firstpipe {*}$nexttail] - puts stderr $exectail - tailcall punk::pipeline .= "" "" {*}$exectail + + #pattern for insertion position in the pipeline segment (*not* for insertion within any data elements themselves) + #With the =/assign operator - it does effectively insert into the data at top list level only. probably best to keep it that way - keep it simple. + #deeper data manipulations best kept for functions in the pipeline. + #This is primarily for the .= case. Allowing for example: + # x= a b c d |> .=@2 lsearch -inline b* + # to substitute to lsearch -inline {a b c d} b* + proc _split_equalsrhs {insertionpattern} { + set var_position_list [punk::_split_patterns $insertionpattern] + foreach v_pos $var_position_list { + lassign $v_pos v positionspec + if {![string length $positionspec]} { + error "pipesyntax error in segment insertionpattern $insertionpattern - v $v missing position spec e.g /0" + } + if {[string index $positionspec 0] ni [list "/" "@"]} { + error "pipesyntax error in segment insertionpattern $insertionpattern - v $v bad position spec $positionspec" + } } + return $var_position_list } @@ -2082,32 +2029,25 @@ namespace eval punk { } } - proc pipeline {segment_op initial_returnvarspec e1 args} { + proc pipeline {segment_op initial_returnvarspec equalsrhs args} { set fulltail $args unset args - debug.punk.pipe {call pipeline: op:'$segment_op' '$initial_returnvarspec' '$e1' '$fulltail'} 4 + debug.punk.pipe {call pipeline: op:'$segment_op' '$initial_returnvarspec' '$equalsrhs' '$fulltail'} 4 #debug.punk.pipe.rep {[rep_listname fulltail]} 6 - - #--------------------------------------------------------------------- # test if we have an initial x.=y.= or x.= y.= - if {($e1 eq "") } { - set nexttail [lassign $fulltail next1] ;#tail head - } else { - set next1 $e1 - set nexttail $fulltail - } + #nextail is tail for possible recursion based on first argument in the segment + set nexttail [lassign $fulltail next1] ;#tail head + if {$next1 eq "pipematch"} { set results [uplevel 1 [list pipematch {*}$nexttail]] debug.punk.pipe {>>> pipematch results: $results} 1 set d [_multi_bind_result $initial_returnvarspec $results] - set r [_handle_bind_result $d] - - return $r + return [_handle_bind_result $d] } elseif {$next1 eq "pipecase"} { set msg "pipesyntax\n" append msg "pipecase does not return a value directly in the normal way\n" @@ -2118,13 +2058,10 @@ namespace eval punk { error $msg } - #temp + #temp - this is related to a script for the entire pipeline (functional composition) - not the script for the segment-based x=y or x.=y proc. set ::_pipescript "" - #maintenance: punk::re_dot_assign - #set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} - #set re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} if {([string first = $next1] >= 0) && (![arg_is_script_shaped $next1]) } { @@ -2133,24 +2070,28 @@ namespace eval punk { if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #non pipelined call to self - return result #debug.punk.pipe {nextreturnvarspec: $nextreturnvarspec nextrhs:$nextrhs tail:$nexttail} 4 - set results [uplevel 1 [list ::punk::pipeline .= $nextreturnvarspec $nextrhs {*}$nexttail]] + #set results [uplevel 1 [list ::punk::pipeline .= $nextreturnvarspec $nextrhs {*}$nexttail]] + set results [uplevel 1 [list $next1 {*}$nexttail]] + #debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5 #debug.punk.pipe {>>> results: $results} 1 return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]] } #puts "======> recurse asssign based on next1:$next1 " - #set re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} - if {[regexp {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { + if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #non pipelined call to plain = assignment - return result #debug.punk.pipe {nextreturnvarspec: $nextreturnvarspec nextrhs:$nextrhs tail:$nexttail} 4 - set results [uplevel 1 [list ::punk::pipeline = $nextreturnvarspec $nextrhs {*}$nexttail]] + #set results [uplevel 1 [list ::punk::pipeline = $nextreturnvarspec $nextrhs {*}$nexttail]] + set results [uplevel 1 [list $next1 {*}$nexttail]] #debug.punk.pipe {>>> results: $results} 1 set d [_multi_bind_result $initial_returnvarspec $results] return [_handle_bind_result $d] } } + set procname $initial_returnvarspec.=$equalsrhs + #--------------------------------------------------------------------- #todo add 'op' argument and handle both .= and = @@ -2180,45 +2121,19 @@ namespace eval punk { set apipe_posn_reverse [lsearch [lreverse $fulltail] "<*|"] if {$apipe_posn_reverse >=0} { set apipe_posn [expr {[llength $fulltail] - $apipe_posn_reverse -1}] - set datatail [lrange $fulltail 0 $apipe_posn-1] + set tailremaining [lrange $fulltail 0 $apipe_posn-1] set argslist [lrange $fulltail $apipe_posn+1 end] set argpipe [lindex $fulltail $apipe_posn] set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " 1} { + error "pipesyntax 1 = can only accept a single argument" } + set segment_members $segment_first_word } + + + #tailremaining includes x=y during the loop. set returnvarspec $initial_returnvarspec if {![llength $argslist]} { @@ -2330,279 +2246,198 @@ namespace eval punk { set more_pipe_segments 0 } - ##set dict_tagval [regexp -all -inline {(%[[:alnum:]]*%)} $segment_members] ;# e.g %args% %args% %data% %data% - #set dict_segment_tags [regexp -all -inline {(%[[:alnum:]]*%)} $segment_members] ;# e.g %args% %args% %data% %data% - # - set dict_segment_tags [dict create] - set tagmap [lmap v $segment_members {punk::get_tags $v}] - debug.punk.pipe.var {TAGMAP([llength $tagmap]): $tagmap} 5 - #we definitely don't want to look for tags in scripts - would interfere with sub/nested pipelines - set si 0 - foreach seg $segment_members { - if {$si ni $segment_members_script_index} { - set tags [punk::get_tags $seg] - foreach t $tags { - dict set dict_segment_tags $t $t - } - } - incr si - } - set segment_has_tags [dict size $dict_segment_tags] - debug.punk.pipe.var {segment_tags: $dict_segment_tags} 5 - debug.punk.pipe.rep {[rep_listname segment_members]} 4 - - - + set insertion_patterns [_split_equalsrhs $rhs] ;#raises error if rhs of positionspec not like /* or @* + #puts stdout ">>> insertion_patterns $insertion_patterns" + set segment_has_insertions [expr {[llength $insertion_patterns] > 0}] + debug.punk.pipe.var {segment_has_insertions: $insertion_patterns} 5 + debug.punk.pipe.rep {[rep_listname segment_members]} 4 + - #whether the arguments have %v% tags or not - apply any modification from the piper argspecs (script will use modified args/data) - if {[dict exists $pipedvars "datalist"]} { - dict set dict_tagval %datalist% [dict get $pipedvars "datalist"] - } else { - if {[info exists previous_result]} { - if {![catch {lrange $prevr 0 end} dl]} { - dict set dict_tagval %datalist% $dl ;#deliberately unprotected by 'list' - will be passed through as args *if* a valid tcl list. - } else { - dict set dict_tagval %datalist% [list] - } - } - } + #whether the segment has insertion_patterns or not - apply any modification from the piper argspecs (script will use modified args/data) + #pipedvars comes from either previous segment |>, or <| args if {[dict exists $pipedvars "data"]} { #dict set dict_tagval %data% [list [dict get $pipedvars "data"]] - dict set dict_tagval %data% [dict get $pipedvars "data"] + dict set dict_tagval data [dict get $pipedvars "data"] } else { if {[info exists previous_result]} { - dict set dict_tagval %data% $prevr + dict set dict_tagval data $prevr } } - foreach {k v} $pipedvars { + foreach {vname val} $pipedvars { #add additionally specified vars and allow overriding of %args% and %data% by not setting them here - if {$k in [list "datalist" "data"]} { + if {$vname eq "data"} { #already potentially overridden continue } - #dict set dict_tagval %$k% [list $v] - dict set dict_tagval %$k% $v + dict set dict_tagval $vname $val } + #todo! + #segment_script - not in use yet. + #will require non-iterative pipeline processor to use ... recursive.. or coroutine based + set script "" - - - - #check it's still a valid list? - if {!$segment_has_tags} { + if {!$segment_has_insertions} { #debug.punk.pipe.var {[a+ cyan]SEGMENT has no tags[a+]} 7 - #add previous_result as data only if no tags present (data is just list-wrapped previous_result vs args = forward-result treated as already being a list) - #set segment_members_filled [concat $segment_members [dict get $dict_tagval %data%]] ;# data flows through by default - not args - because some strings are not valid lists + #add previous_result as data in end position by default, only if *no* insertions specified (data is just list-wrapped previous_result) + #set segment_members_filled [concat $segment_members [dict get $dict_tagval %data%]] ;# data flows through by default as single element - not args - because some strings are not valid lists + #insertion-specs with a trailing * can be used to insert data in args format set segment_members_filled $segment_members - if {[dict exists $dict_tagval %data%]} { - lappend segment_members_filled [dict get $dict_tagval %data%] + if {[dict exists $dict_tagval data]} { + lappend segment_members_filled [dict get $dict_tagval data] } } else { - debug.punk.pipe.var {dict_tagval: $dict_tagval} 4 + debug.punk.pipe.var {processing insertion_pattern dict_tagval: $dict_tagval} 4 set segment_members_filled [list] - set idxmem 0 - foreach mem $segment_members { - #todo - skip 'script' segments - set tags [lindex $tagmap $idxmem] - if {[llength $tags]} { - if {"%datalist%" in $tags} { - if {$mem eq "%datalist%"} { - #exact match is the preferred way to use datalist - if {[dict exists $dict_tagval %datalist%]} { - set dl [dict get $dict_tagval %datalist%] - foreach datum $dl { - lappend segment_members_filled $datum + set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign + foreach v_pos $insertion_patterns { + lassign $v_pos v positionspec ;#v may be atom, or varname (in pipeline scope) + #julz + set offset 0 + if {[string index $v 0] eq "'"} { + set v [string range $v 1 end-1] ;#assume trailing ' is present! + } else { + if {$v eq ""} { + set v "data" + } + if {[dict exists $dict_tagval $v]} { + set v [dict get $dict_tagval $v] + } else { + error "insertionpattern varname $v not present in pipeline context" + } + } + #maintenance - index logic should be identical to to match_assign - which only needs to process atoms because it delegates all pipeline ops here, so no vars available (single segment assign) + set positionspecatomic [string trimright $positionspec "*"] + set do_expand [expr {[string index $positionspec end] eq "*"}] ;#only applies to vars - as atoms don't have whitespace (review a proc can have whitespce - but it's harder to call.. atoms probably best kept simple) + set ptype [string index $positionspecatomic 0] + set index [string range $positionspecatomic 1 end] + set isint [string is integer -strict $index] + if {$isint || [string match "end*" $index]} { + if {$ptype eq "@"} { + #compare position to *original* list - note use of $index > $datalen rather than $index+1 > $datalen - (we allow 'insertion' at end of list by numeric index) + if {$isint} { + append script [string map [list $index] { + if {( > [llength $segmenttail])} { + error "insertionpattern index out of bounds. index: vs len: [llength $segmenttail] use /x instead of @x to avoid check" } - } else { - #nothing to put - omit in output + }] + #temp - scriptalternative + if {($index > [llength $segmenttail])} { + error "insertionpattern index out of bounds. index:$index vs len: [llength $segmenttail] use /x instead of @x to avoid check" } + } + #todo check end-x bounds? + } + if {$isint} { + #set segmenttail [linsert $segmenttail $index+$offset $v] + #todo - expansion! + append script [string map [list $v [expr {$index + $offset}]] { + #set segmenttail [linsert $segmenttail ] + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] ] + }] + #temp - scriptalternative + if {$do_expand} { + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] $index+$offset {*}$v] } else { - #assume/hope the user knows what they're doing... - #maybe they are trying to quote the list etc. - lappend segment_members_filled [string map $dict_tagval $mem] + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] $index+$offset $v] } } else { - lappend segment_members_filled [string map $dict_tagval $mem] + #todo - review/test! + set endindex [string range $index 4 end] + if {[string length $endindex]} { + incr endindex -$offset + set idx "end-$endindex" + } else { + set idx "end" + } + #set segmenttail [linsert $segmenttail $idx $v] + #todo - expansion! + append script [string map [list $v $idx] { + #set segmenttail [linsert $segmenttail ] + #use inline K to make sure the list is unshared (optimize for larger lists) + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] ] + }] + #temp - scriptalternative + if {$do_expand} { + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] $idx {*}$v] + } else { + set segmenttail [linsert [lindex [list $segmenttail [unset segmenttail]] 0] $idx $v] + } } + incr offset } else { - lappend segment_members_filled $mem - } - incr idxmem + error "pipesyntax error in segment insertionpattern - v $v unable to interpret position spec" + } } - #note - length of segment_members_filled may now differ from length of original segment_members! + set segment_members_filled $segmenttail + #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) - #set segment_members_filled [string map $dict_tagval $segment_members] - #set segment_members_filled [lrange $segment_members_filled 0 end] ;#back to list rep } set rhs [string map $dict_tagval $rhs] debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 + # script index could have changed!!! todo fix! + #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) if {(![llength $segment_members_script_index]) && $segment_op eq ".="} { + #no scriptiness detected + + #debug.punk.pipe.rep {[a+ yellow bold][rep_listname segment_members_filled][a+]} 4 + set cmdlist_result [uplevel 1 $segment_members_filled] + #debug.punk.pipe {[a+ green bold]forward_result: $forward_result[a+]} 4 + #debug.punk.pipe.rep {[a+ yellow bold]forward_result REP: [rep $forward_result][a+]} 4 - if {[string index $rhs 0] eq "\{"} { - if {[llength $segment_members_filled] == 1} { - if {[string index $rhs end] eq "\}"} { - set e [string range $rhs 1 end-1] - } else { - #missing close bracket - evaluate anyway? - set e [string range $rhs 1 end] - } - } else { - #must be 2 or more total elements in segment_members (which includes the x.=y) - set seg_remainder [lrange $segment_members_filled 1 end] ;#exclude the x.=y - set last2 [string range $seg_remainder end-1 end] - #puts stderr "last2chars.. $last2" - if {$last2 eq "\\\}"} { - set seg_remainder [string range $seg_remainder 0 end-2] - } - set e [string range $rhs 1 end] - append e $seg_remainder - } - - debug.punk.pipe {>evaluating $e as expression\n due to brace \"\{\" immediately following .=} 4 - - if {![catch {uplevel 1 [list expr $e]} evaluated]} { - #set forward_result $evaluated - set d [_multi_bind_result $returnvarspec [lindex [list $evaluated [unset evaluated]] 0 ]] - set segment_result [_handle_bind_result $d] - } else { - set msg "pipesyntax" - append msg "Attempted to evaluate as expression '$e'\n" - append msg "due to brace \"\{\" immediately following .= \n" - append msg "(place other commands immediately following .= or place script block after a space)\n" - append msg "expression error: $evaluated" - error $msg - } - } elseif {($rhs ne "") && ([string is double -strict $rhs] || [_is_math_func_prefix $rhs])} { - #check of rhs ne "" is important to not waste time with _is_math_func_prefix - debug.punk.pipe {evaluating $rhs {*}[lrange $segment_members_filled 1 end] as expression\n due to number or math func immediately following .=} 4 - if {![catch {uplevel 1 [list expr $rhs {*}[lrange $segment_members_filled 1 end]]} evaluated]} { - set forward_result $evaluated - set d [_multi_bind_result $returnvarspec $forward_result] - set segment_result [_handle_bind_result $d] - } else { - set msg "pipesyntax" - append msg "Attempted to evaluate as expression\n" - append msg "due to number or math func immediately following .= \n" - append msg "(place other commands immediately following .= or place script block after a space)\n" - append msg "expression error: $evaluated" - error $msg - } - } else { - #no scriptiness detected - #set cmdlist [list] - if {[llength $rhs]} { - #lappend cmdlist $rhs - set cmdlist [list $rhs] - } else { - set cmdlist [list] - } - lappend cmdlist {*}[lrange $segment_members_filled 1 end] - #set cmdlist [concat $rhs [lrange $segment_members_filled 1 end]] ;#ok if rhs empty - - #debug.punk.pipe {>>firstword: [lindex $cmdlist 0] bindingspec:$returnvarspec >>cmdlist([llength $cmdlist]: $cmdlist)} 4 - #debug.punk.pipe.rep {[a+ yellow bold][rep_listname cmdlist][a+]} 4 - - set cmdlist_result [uplevel 1 $cmdlist] - #debug.punk.pipe {[a+ green bold]forward_result: $forward_result[a+]} 4 - #debug.punk.pipe.rep {[a+ yellow bold]forward_result REP: [rep $forward_result][a+]} 4 - - #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] - set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] - - set segment_result [_handle_bind_result $d] - #puts stderr ">>forward_result: $forward_result segment_result $segment_result" - } - - + #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] + + set segment_result [_handle_bind_result $d] + #puts stderr ">>forward_result: $forward_result segment_result $segment_result" } elseif {$segment_op eq "="} { - #set segment_result [uplevel 1 [list ::punk::match_assign $returnvarspec $rhs [lrange $segment_members_filled 1 end]]] - #slightly different semantics for assigment - set segment_tail [lrange $segment_members 1 end] ;#exclude the x=y - #set segment_members_filled $segment_members - - - - if {!$segment_has_tags} { - if {[dict exists $dict_tagval %data%]} { - lappend segment_members_filled {*}[dict get $dict_tagval %data%] - } - } - set filled_tail [lrange $segment_members_filled 1 end] - - #puts stdout ">>> rhs:'$rhs' segment_members:$segment_members segment_tail:$segment_tail filled_tail:'$filled_tail'" - - if {(![llength $rhs]) && (![llength $segment_tail])} { - #no rhs and no values directly in segment - set list_mode 1 - } elseif {[llength $rhs]} { - set list_mode 1 - } else { - #there is a space after = and also at least one value already present in the tail (even before pipeargs applied) - set list_mode 0 - } - - if {$list_mode == 1} { - if {!$segment_has_tags} { - if {![llength $rhs]} { - set value [purelist] + #slightly different semantics for assigment! + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = + # must return: {a b c} not a b c + # + if {!$segment_has_insertions} { + set segment_members_filled $segment_members + if {[dict exists $dict_tagval data]} { + if {![llength $segment_members_filled]} { + set segment_members_filled [dict get $dict_tagval data] } else { - set value [purelist $rhs] - } - if {[dict exists $dict_tagval %data%]} { - lappend segment_tail {*}[dict get $dict_tagval %data%] - } - lappend value {*}$segment_tail - } else { - set value $rhs - if {[llength $filled_tail] > 0} { - lappend value {*}$filled_tail + lappend segment_members_filled [dict get $dict_tagval data] } } - - } else { - #single value mode (no rhs and 1 or more vals) - if {[llength $filled_tail] == 0} { - set value [purelist] - } elseif {[llength $filled_tail] == 1} { - set value [lindex $filled_tail 0] - } else { - puts stderr "= assignment segment values: $filled_tail" - error "= unable to assign multiple values" - } } - set d [_multi_bind_result $returnvarspec [lindex [list $value [unset value ]] 0]] + set d [_multi_bind_result $returnvarspec [lindex [list $segment_members_filled [unset segment_members_filled ]] 0]] set segment_result [_handle_bind_result $d] - } elseif {[llength $segment_members_script_index]} { + } elseif {[llength $segment_members_script_index] || $segment_op eq "script"} { #script debug.punk.pipe {[a+ cyan bold].. evaluating as script[a+]} 2 set script [lindex $segment_members $segment_members_script_index] ;#default. May have pre_script prepended later #build argument lists for 'apply' set segmentargnames [list] set segmentargvals [list] - foreach {k v} $dict_tagval { - set varname [string range $k 1 end-1] ;# strip off first and last % only - if {$varname eq "%argsdata%"} { + foreach {k val} $dict_tagval { + if {$k eq "argsdata"} { #skip args - it is manually added at the end of the apply list if it's a valid tcl list continue } - lappend segmentargnames $varname - lappend segmentargvals $v + lappend segmentargnames $k + lappend segmentargvals $val } set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list @@ -2657,19 +2492,19 @@ namespace eval punk { } if {![info exists pscript]} { #set pscript $s - set pscript [funcl::o_of_n 1 [list $rhs {*}$segment_members]] + set pscript [funcl::o_of_n 1 $segment_members] } else { #set pscript [string map [list

$pscript] {uplevel 1 [concat $rhs $segment_members_filled [

]]}] #set snew "set pipe_$i \[uplevel 1 \[list $rhs $segment_members_filled " #append snew "set pipe_[expr $i -1]" #append pscript $snew - set pscript [funcl::o_of_n 1 [list $rhs {*}$segment_members] $pscript] + set pscript [funcl::o_of_n 1 $segment_members $pscript] } } - set cmdline_result [uplevel 1 [concat $rhs $segment_members_filled]] - #set d [_multi_bind_result $returnvarspec [punk::K $cmdline_result [unset cmdline_result]]] - set d [_multi_bind_result $returnvarspec [lindex [list $cmdline_result [unset cmdline_result]] 0 ]] + set cmdlist_result [uplevel 1 $segment_members_filled] + #set d [_multi_bind_result $returnvarspec [punk::K $segment_members_filled [unset segment_members_filled]]] + set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result]] 0 ]] #multi_bind_result needs to return a funcl for rhs of: #lindex [list [set syncvar [main pipeline.. ]] [rhs binding funcl...] 1 ] @@ -2725,11 +2560,11 @@ namespace eval punk { if {[llength $tailremaining] || $next_pipe_posn >= 0} { if {$next_pipe_posn >=0} { - set segment_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] } else { - set segment_members $tailremaining + set next_all_members $tailremaining set tailremaining [list] } @@ -2739,42 +2574,45 @@ namespace eval punk { set returnvarspec "" ;# the lhs of x=y set segment_op "" set rhs "" - if {[llength $segment_members]} { - if {[arg_is_script_shaped [lindex $segment_members 0]]} { - set segment_first_word [lindex $segment_members 0] - set segment_second_word [lindex $segment_members 1] + if {[llength $next_all_members]} { + if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + set segment_first_word [lindex $next_all_members 0] set segment_members_script_index 0 set segment_op "" - + set segment_members $next_all_members } else { - set possible_assignment [lindex $segment_members 0] + set possible_assignment [lindex $next_all_members 0] #set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op ".=" - if {![string length $rhs]} { - set segment_first_word [lindex $segment_members 1] - set segment_second_word [lindex $segment_members 2] - set script_like_first_word [arg_is_script_shaped $segment_first_word] - if {$script_like_first_word} { - set segment_members_script_index 1 - } - } else { - set segment_first_word $rhs - set segment_second_word [lindex $segment_members 1] + set segment_first_word [lindex $next_all_members 1] + set script_like_first_word [arg_is_script_shaped $segment_first_word] + if {$script_like_first_word} { + set segment_members_script_index 0 ;#relative to segment_members which no longer includes the .= } - } elseif {[regexp {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { - #set re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} + set segment_members [lrange $next_all_members 1 end] + } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op "=" #never scripts - set segment_first_word [lindex $segment_members 1] - set segment_second_word [lindex $segment_members 2] - + #must be at most a single element after the = ! + if {[llength $next_all_members] > 2} { + error "pipesyntax - at most one element can follow =" + } + set segment_first_word [lindex $next_all_members 1] + if {[catch {llength $segment_first_word}]} { + set segment_is_list 0 ;#only used for segment_op = + } else { + set segment_is_list 1 ;#only used for segment_op = + } + + set segment_members $segment_first_word } else { #no assignment operator and not script shaped set segment_op "" set returnvarspec "" - set segment_first_word [lindex $segment_members 0] - set segment_first_word [lindex $segment_members 1] + set segment_first_word [lindex $next_all_members 0] + set segment_first_word [lindex $next_all_members 1] + set segment_members $next_all_members #puts stderr ">>3 no-operator segment_first_word: '$segment_first_word'" } } @@ -2805,6 +2643,39 @@ namespace eval punk { #return $forward_result } + proc know {cond body} { + set existing [info body ::unknown] + #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) + ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##if {$body ni $existing} { + package require base64 + set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered + #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { + #--------------------------------------- + if {![catch {expr {@c@}} res] && $res} { + debug.punk.unknown {HANDLED BY: punk unknown_handler args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + return [eval {@b@}] + } else { + debug.punk.unknown {skipped: punk unknown_handler args:'$args' "cond_script:'[punk::decodescript @scr@]'" } 4 + } + #--------------------------------------- + }]$existing + #} + } + proc know? {{len 2000}} { + puts [string range [info body ::unknown] 0 $len] + } + proc decodescript {b64} { + if {[ catch { + package require base64 + base64::decode $b64 + } scr]} { + return "" + } else { + return "($scr)" + } + } proc configure_unknown {} { #----------------------------- #these are critical e.g core behaviour or important for repl displaying output correctly @@ -2818,6 +2689,7 @@ namespace eval punk { #can't use know - because we don't want to return before original unknown body is called. proc ::unknown {args} [string map [list] { + package require base64 set ::punk::last_run_display [list] set ::repl::last_unknown [lindex $args 0] ;#jn }][info body ::unknown] @@ -2853,30 +2725,16 @@ namespace eval punk { if {$hd ne $partzerozero} { regexp $punk::re_assign $hd _ varspecs rhs } - #tailcall ::punk::match_assign $varspecs $rhs $tail - return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] - - - - #puts >>1>[rep $result] - if {[catch {lrange $result 0 1} first2wordsorless]} { - #if we can't get as a list then it definitely isn't the semi-structured 'binding mismatch' - return $result - } else { - if {$first2wordsorless eq {binding mismatch}} { - error $result - } else { - #puts >>2>[rep $result] - return $result - } - } + tailcall ::punk::match_assign $varspecs $rhs {*}$tail + #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] } #variable re_assign {^([^\r\n=\{]*)=(.*)} #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list #e.g x=a\nb c #x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained - know {[regexp $punk::re_assign [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall ::punk::_unknown_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + # + know {[regexp {^([^ \t\r\n=\{]*)\=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall ::punk::_unknown_assign_dispatch $partzerozero $varspecs $rhs {*}$args} #variable re_assign {^([^\r\n=\{]*)=(.*)} #know {[regexp $punk::re_assign [lindex $args 0 0] partzerozero varspecs rhs]} { @@ -3160,9 +3018,9 @@ namespace eval punk { } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set re_equals {^([^ \t\r\n=\{]*)=$} - if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { + if {[regexp {^([^ \t\r\n=]*)\.=$} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] - } elseif {[regexp {^([^ \t\r\n=\{]*)=$} $assign _ returnvarspecs]} { + } elseif {[regexp {^([^ \t\r\n=]*)=$} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] } else { error "pipesyntax punk::% unable to interpret pipeline '$args'" @@ -4058,15 +3916,24 @@ namespace eval punk { } |data@@ok/result> {set data} |> {lmap v $data {namespace tail $v}} |> lsort |> {join $data \n}