diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index e8df5e78..3e6803f9 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -77,10 +77,42 @@ namespace eval punk::config { } namespace eval punk { + package require control + control::control assert enabled 1 + namespace import ::control::assert + package require struct::list + + #NOTE - always call debug.xxx with braced message instead of double-quoted (unless specifically intending to do double-subtition) + #(or $ within values will be substituted, causing an extra error message if the var doesn't exist - which it quite possibly doesn't) + package require debug + + debug define punk.unknown + debug define punk.pipe + debug define punk.pipe.var + debug define punk.pipe.args + debug define punk.pipe.rep ;#string/list representation with tcl::unsupported::representation + + + #----------------------------------- + # todo - load initial debug state from config + debug on punk.unknown + debug level punk.unknown 1 + debug on punk.pipe + debug level punk.pipe 4 + debug on punk.pipe.var + debug level punk.pipe.var 1 + debug on punk.pipe.args + debug level punk.pipe.args 1 + debug on punk.pipe.rep 1 + + + debug header "dbg> " + variable last_run_display [list] variable ansi_disabled 0 - variable re_headvar1 {([a-zA-Z:@.(),]+?)(?![^(]*\))(,.*)*$} - variable re_headvar {(.+?)(?![^(]*\))(,.*)*$} + #variable re_headvar1 {([a-zA-Z:@.(),]+?)(?![^(]*\))(,.*)*$} + + proc ::punk::var {varname {= {}} args} { if {${=} == "="} { if {[llength $args] > 1} { @@ -92,6 +124,43 @@ namespace eval punk { uplevel 1 [list set $varname] } } + + #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ + # + #we can't provide a float comparison suitable for every situation, + #but we pick something reasonable, keep it stable, and document it. + proc float_almost_equal {a b} { + package require math::constants + set diff [expr {abs($a - $b)}] + if {$diff <= $math::constants::eps} { + return 1 + } + set A [expr {abs($a)}] + set B [expr {abs($b)}] + set largest [expr {($B > $A) ? $B : $A}] + return [expr {$diff <= $largest * $math::constants::eps}] + } + + #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. + proc boolean_equal {a b} { + #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. + expr {($a && 1) == ($b && 1)} + } + proc boolean_almost_equal {a b} { + if {[string is double --strict $a]} { + if {[float_almost_equal $a 0]} { + set a 0 + } + } + if {[string is double --strict $b]} { + if {[float_almost_equal $b 0]} { + set b 0 + } + } + #must handle true,no etc. + 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 (?) @@ -99,6 +168,7 @@ namespace eval punk { #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@}] } @@ -106,19 +176,45 @@ namespace eval punk { }]$existing #} } - proc know? {} { - puts [string range [info body ::unknown] 0 1811] + proc know? {{len 2000}} { + puts [string range [info body ::unknown] 0 $len] + } + + proc varinfo {vname {flag ""}} { + upvar $vname v + if {[array exists $vname]} { + error "can't read \"$vname\": variable is array" + } + if {[catch {set v} err]} { + error "can't read \"$vname\": no such variable" + } + set inf [shellfilter::list_element_info [list $v]] + set inf [dict get $inf 0] + if {$flag eq "-v"} { + return $inf + } + + set output [dict create] + dict set output wouldbrace [dict get $inf wouldbrace] + dict set output wouldescape [dict get $inf wouldescape] + dict set output head_tail_names [dict get $inf head_tail_names] + dict set output len [dict get $inf len] + return $output } - #split a varname of form var1,var2,var3.. at commas - but ignoring commas within brackets (a common array variable convention). - #e.g var(x,y),blah,var(,foo) would be split into var(x,y) blah var(,foo) - proc _split_at_unbracketed_commas {varname} { + + #split a varname of form var1,var2,var3.. at specified char - but ignoring the char within brackets + #(a common array variable convention is to use comma for levels). + #e.g var(x,y),blah,var(,foo) would be split into var(x,y) blah var(,foo) if comma is specified as the char + #Assumption - char not in "(" ")" + #for punk varspecs we use / as the separator + proc _split_at_unbracketed_comma1 {varname} { + set re_headvar {(.+?)(?![^(]*\))(,.*)*$} set varname [string trimleft $varname ,] - variable re_headvar set varlist [list] if {[regexp $re_headvar $varname _ v1 vtail]} { lappend varlist $v1 - set subvars [_split_at_unbracketed_commas $vtail] + set subvars [_split_at_unbracketed_comma $vtail] set varlist [concat $varlist $subvars] return $varlist } else { @@ -126,170 +222,1303 @@ namespace eval punk { } } + #non recursive without regexp is significantly faster + proc _split_at_unbracketed_comma {varspecs} { + set varlist [list] + set in_brackets 0 + set varspecs [string trimleft $varspecs,] + set token "" + if {[string first "," $varspecs] <0} { + return $varspecs + } + foreach c [split $varspecs ""] { + if {$in_brackets} { + if {$c eq ")"} { + set in_brackets 0 + } + append token $c + } else { + if {$c eq ","} { + lappend varlist $token + set token "" + } else { + append token $c + if {$c eq "("} { + set in_brackets 1 + } + } + } + } + if {[string length $token]} { + lappend varlist $token + } + return $varlist + } + + + #called from know_assign/know_dot_assign for lhs of assignment - uplevel 2 to caller's level + #called from know_assign/know_dot_assign for rhs pipelined vars - uplevel 1 to write vars only in 'apply' scope + #return a dict with keys result, setvars, unsetvars + #TODO - implement cross-binding (as opposed to overwrite/reassignment) when a var appears multiple times in a pattern/multivar + #e.g x@0,x@1 will only match if value at positions 0 & 1 is the same (a form of auto-pinning?) + #e.g x,x@0 will only match a single element list + proc _multi_assign_result {multivar data args} { + #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 + set returndict [dict create ismatch 0 result "" setvars {} unsetvars {}] + if {![string length $multivar]} { + #treat the absence of a var as a match to anything + dict set returndict ismatch 1 + dict set returndict result $data + return $returndict + } + + set defaults [list -unset 0 -levelup 2 ] + set opts [dict merge $defaults $args] + set unset [dict get $opts -unset] + set lvlup [dict get $opts -levelup] + + #comma seems a natural choice to split varspecs, + #but also for list and dict subelement access + #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) + #so / will indicate subelements e.g @0/1 for lindex $list 0 1 + if {[string first "," $multivar] >=0} { + set varspeclist [_split_at_unbracketed_comma $multivar] + } else { + set varspeclist [list $multivar] + } + + #first classify into var_returntype of either "pipeline" or "segment" + #segment returntype is indicated by leading % + + + + #mutually exclusive - atom/pin + set map [list "" ' ^] ;#0 = don't-care/other 1 = atom 2 = pin + set var_class [lmap var $varspeclist {expr {([set m [lsearch $map [string index $var 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] + # e.g {a 0} {'b 1'} {c 0} {^x(a,b) 2} + + #raw varspecs without pin/atom modifiers + set varspecs_trimmed [lmap varinfo $var_class {expr {([lindex $varinfo 1] > 0) ? [string range [lindex $varinfo 0] 1 end] : [lindex $varinfo 0]}}] + + #var names (possibly empty portion to the left of ) + set var_names [lmap v $varspecs_trimmed {expr {([set p [string first @ $v]] >= 0) ? [string range $v 0 $p-1] : $v}}] - #called from know_assign - uplevel 2 to caller's level - proc _multi_assign_expression_result {multivar expression1 {unset 0}} { - set lvlup 2 - set varspeclist [_split_at_unbracketed_commas $multivar] - set vidx 0 - foreach vspec $varspeclist { + set v_list_idx 0 ;#for vars with single @ only + + #jn + + #member lists of returndict which will be apppended 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] + + + #varname action value - where value is value to be set if action is set + #actions: + # "" 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 + set var_actions [lmap v $var_names {expr {[list $v "" ""]}}] + + + #e.g {a = abc} {b unset ""} + set expected_values [lmap v $var_names {list $v "-" ""}] + debug.punk.pipe.var "initial map expected_values: $expected_values" 5 + + + set returnval "" + set i 0 + #assert i incremented at each continue and at each end of loop - at end i == list length + 1 + #always use 'assigned' var in each loop + # (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. + # as well as adding the data values to the var_actions list + foreach vspec $varspecs_trimmed { + set assigned "" set firstat [string first "@" $vspec] - if {$firstat > 0} { - set v [string range $vspec 0 $firstat-1] - if {[string is integer -strict $v]} { - error "Cannot set a var named '$v' using this syntax. use == for comparison, or use set $v if you really want a variable named like a number." - } + #set firstq [string first "'" $vspec] + set v [lindex $var_names $i] + #if v contains any * and/or ? - then it is a glob match - not a varname + + if {$firstat >= 0} { + #if {[string is integer -strict $v]} { + # lset var_actions $i 1 matchatom + #} if {$unset} { - uplevel $lvlup [list unset $v] + #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 - set part2 [string range $vspec $firstat+1 end] - if {$part2 eq ""} { - set v [string range $vspec 0 end-1] + + + set after_first_at [string range $vspec $firstat+1 end] + if {$after_first_at eq ""} { #no dict key following @, this is a positional spec - uplevel $lvlup [list set $v [lindex $expression1 $vidx]] - incr vidx ;#only incr each time we have a trailing @ - } elseif {[string match "@*" $part2]} { - # varname@@ = last element - # varname@@x where x is positive or negative integer or zero - use x as lindex - # or x is a range e.g 0-3 suitable for lrange - set selector [string range $part2 1 end] - if {([string is integer -strict $selector]) || ([regexp {^(end)$|^end[-+]{1,2}([0-9]+)$} $selector])} { - uplevel $lvlup [list set $v [lindex $expression1 $selector]] - } elseif {[regexp {^([0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $selector _ start end]} { - uplevel $lvlup [list set $v [lrange $expression1 $start $end]] - } elseif {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $selector _ start end]} { - uplevel $lvlup [list set $v [lrange $expression1 $start $end]] + set assigned [lindex $data $v_list_idx] + lset var_actions $i 1 ?set + lset var_actions $i 2 $assigned + + #if {[string length $v]} { + # uplevel $lvlup [list set $v $assigned] + #} + incr v_list_idx ;#only incr each time we have a trailing @ + } elseif {[string match "@*" $after_first_at]} { + #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc + set keypath [string range $after_first_at 1 end] + set key [split $keypath /] + + if {[dict exists $data {*}$key]} { + set assigned [dict get $data {*}$key] + lset var_actions $i 1 ?set + lset var_actions $i 2 $assigned + #if {[string length $v]} { + # uplevel $lvlup [list set $v $assigned] + #} } else { - error "Unable to interpret $vspec @@ must be followed by index suitable for lindex or lrange commands" + #for consistency with lindex out of range setting var to empty string - we will do same for non existent dict key rather than unset + #This stops us *directly* using @@key for pattern-match testing that key exists - but we can acheive matching using the count mechanism. + #e.g 0+@#@key ? (where 0 is empty list/string and -1 means key not found) + set assigned "" + lset var_actions $i 1 ?set + lset var_actions $i 2 "" } + + } else { - set key $part2 - #part following a single @ is dict key - if {[dict exists $expression1 $key]} { - uplevel $lvlup [list set $v [dict get $expression1 $key]] - } else { - #for consistency with lindex out of range setting var to empty string - we will do same for non existent dict key rather than unset - uplevel $lvlup [list set $v ""] - #catch { - # uplevel $lvlup [list unset $v] - #} + # varname@x where x is positive or negative integer or zero - use x as lindex + # or x is a range e.g 0-3 suitable for lrange + set selector $after_first_at + + set leveldata $data + + set subindices [split $selector /] + foreach index $subindices { + set assigned "" + set get_not 0 + set already_assigned 0 + #not- only valid at beginning if selector is a range. + #e.g not-0-end-1 not-end-4-end-2 + if {[string match "not-*" $index]} { + set get_not 1 + #cherry-pick some easy cases, and either assign, or re-map to corresponding index + if {$index eq "not-tail"} { + set assigned [lindex $leveldata 0]; set already_assigned 1 + } elseif {$index in [list "not-head" "not-0"]} { + #set selector "tail"; set get_not 0 + set assigned [lrange $leveldata 1 end]; set already_assigned 1 + } elseif {$index eq "not-end"} { + set assigned [lrange $leveldata 0 end-1]; set already_assigned 1 + } else { + #trim off the not- and let the remaining index handle based on get_not being 1 + set index [string range $index 4 end] + } + } + if {!$already_assigned} { + if {$index in [list "head" 0]} { + #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax + set assigned [lindex $leveldata 0] + } elseif {$index eq "tail"} { + set assigned [lrange $leveldata 1 end] + } elseif {[string is integer -strict $index]} { + if {$get_not} { + #already handled not-0 + set assigned [lreplace $leveldata $index $index] + } else { + set assigned [lindex $leveldata $index] + } + } elseif {[regexp {^(end)$|^end[-+]{1,2}([0-9]+)$} $index]} { + if {$get_not} { + set assigned [lreplace $leveldata $index $index] + } else { + set assigned [lindex $leveldata $index] + } + } elseif {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $index _ start end]} { + if {$get_not} { + set assigned [lreplace $leveldata $start $end] + } else { + set assigned [lrange $leveldata $start $end] + } + } else { + #puts stderr "selector:$selector" + set msg "Unable to interpret $vspec\n" + append msg "selector: $selector\n" + append msg "@ must be followed by a selector (possibly compound separated by forward slashes) suitable for lindex or lrange commands, or a not-x expression\n" + append msg "Additional accepted keywords include: head tail\n" + append msg "Use var@@key to treat value as a dict and retrieve element at key" + error $msg + } + } + set leveldata $assigned + if {![llength $leveldata]} { + break + } } + + + + #if {[string length $v]} { + # uplevel $lvlup [list set $v $assigned] + #} + lset var_actions $i 1 ?set + lset var_actions $i 2 $assigned } } else { - set v $vspec - if {[string is integer -strict $v]} { - error "Cannot set a var named '$v' using this syntax. use == for comparison, or use set $v if you really want a variable named like a number." + if {[string is double -strict $v]} { + lset var_actions $i 1 ?matchatom-set + lset var_actions $i 2 $data + set assigned $data + lappend assigned_values $assigned + incr i + continue } + if {$unset} { - uplevel $lvlup [list unset $v] + #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 } - uplevel $lvlup [list set $v $expression1] + set assigned $data + lset var_actions $i 1 ?set + lset var_actions $i 2 $assigned + #if {[string length $v]} { + # uplevel $lvlup [list set $v $data] + #} + } + + #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 + } } + lappend assigned_values $assigned + incr i } - } + dict set returndict setvars $returndict_setvars + dict set returndict unsetvars $returndict_unsetvars - #know_assign is tailcalled from unknown - uplevel 1 gets to caller level - proc know_assign {multivar expression1 tail} { - if {$::repl::running} { - #todo - debugrepl? - ::repl::rputs stderr "# '$multivar' '$expression1' '$tail'" - } - if {[string is integer -strict $multivar]} { - 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." + set returnval [lindex $assigned_values 0] + + #assert all var_actions were set with leading question mark + #perform assignments only if matched ok + + debug.punk.pipe.var {VAR_CLASS: $var_class} 5 + debug.punk.pipe.var {VARACTIONS: $var_actions} 5 + + #'atom matches var_class = 1 + set atoms [lsearch -all -inline -index 1 $var_class 1] + debug.punk.pipe.var "atoms: $atoms" 5 + #^pin matches var_class = 2 + set pins [lsearch -all -inline -index 1 $var_class 2] + debug.punk.pipe.var "pins: $pins" 5 + #ints & floats match var_class = 3 + set nums [lsearch -all -inline -index 1 $var_class 3] + debug.punk.pipe.var "pins: $pins" 5 + + set match_state [lrepeat [llength $var_names] -1] + + set mismatched [list] + set i 0 + foreach va $var_actions { + lassign $va nm act val + set isatom [expr {[lindex $var_class $i 1] == 1}] + set ispin [expr {[lindex $var_class $i 1] == 2}] + set isnum [expr {[lindex $var_class $i 1] == 3}] + #also treat integers and numbers as atoms + #marking numbers with pin ^ is not strictly correct - but shouldn't affect anything as we map to atom either way + if {[string is double -strict $nm]} { + lset var_class $i 1 1 ;#make sure we classify unmarked numbers as atoms in var_class 'n is equivalent to n + #we deliberatley rule out using numbers as variables + #for punk assignment syntax. punk allows a subset of possible tcl variable names on LHS of match/assignment. + set isatom 1 + } + if {$isatom} { + #puts stdout "==>isatom $nm" + if {$act in [list "?matchatom-set" "?set"]} { + lset var_actions $i 1 matchatom-set + if {$nm eq $val} { + lset match_state $i 1 + } + } + if {$act eq "?unset"} { + #doesn't make sense for an atom ? + } + } elseif {$ispin} { + #puts stdout "==>ispin $nm" + if {$act in [list "?set" "?matchvar-set"]} { + lset var_actions $i 1 matchvar-set + #attempt to read + if {![catch {uplevel $lvlup [list set $nm]} result]} { + lset match_state $i [expr {$result eq $val}] + lset expected_values $i [list $nm set $val] + + } else { + #puts stdout "var ^$nm result:$result vs val:$val" + lset match_state $i 0 + lset expected_values $i [list $nm unknown ?] + } + } + if {$act in [list "?unset" "?matchvar-unset"]} { + lset var_actions $i 1 matchvar-unset + if {![uplevel $lvlup [list info exists $nm ]]} { + lset match_state $i 1 + } + } + + } else { + #puts stdout "==> $nm" + #unpinned non-atoms will be set/unset - always considered a match + lset match_state $i 1 + lset var_actions $i 1 [string range $act 1 end] + } + + incr i } - - puts stderr "tail len: [llength $tail]" - puts stderr "tail-end: [lindex $tail end]" - - if {![string length [string trim $expression1]]} { - if {[llength $tail] > 0} { - #error "unexpected args following =. use 'var=' to unset var or spaced expression e.g 'var=1 + 2'" - if {![catch {expr {*}$tail} evaluated]} { - _multi_assign_expression_result $multivar $evaluated - #return [uplevel 1 [list set $multivar $evaluated]] - return $evaluated + + #-------------------------------------------------------------------------- + #Variable assignments (set/unset) should only occur down here, and only if we have a match + #-------------------------------------------------------------------------- + debug.punk.pipe.var "MATCH_STATE: $match_state" 4 + debug.punk.pipe.var "VARACTIONS2: $var_actions" 5 + + set match_count_needed [llength $var_actions] + #set match_count [>f . foldl 0 [>f . sum .] $match_state] ;#ok method.. but slow compared to expr with join + set match_count [expr [join $match_state +]] + #catch {unset v} + if {$match_count == $match_count_needed} { + #do assignments + set i 0 + foreach va $var_actions { + lassign $va nm act val + set isatom [expr {[lindex $var_class $i 1] == 1}] + if {[string is double -strict $nm]} { + set isatom 1 } - #set result [string cat {*}$tail] ;#not very useful - set result $tail - - _multi_assign_expression_result $multivar $result - #return [uplevel 1 [list set $multivar [string cat {*}$tail]]] - return $result + set ispin [expr {[lindex $var_class $i 1] == 2}] + if {(!$isatom) && (!$ispin)} { + if {[lindex $var_actions $i 1] eq "set"} { + if {[string length $nm]} { + uplevel $lvlup [list set $nm $val] + } + } + if {[lindex $var_actions $i 1] eq "unset"} { + if {[string length $nm]} { + catch {uplevel $lvlup [list unset $nm]} + } + } + } + incr i } - _multi_assign_expression_result $multivar "" 1 ;#final arg 1 to unset variables - #uplevel 1 [list unset $multivar] - return - } elseif {[llength $tail] == 0} { + } else { + set vidx 0 + set mismatches [lmap m $match_state v $var_names {expr {$m != 1} ? {[list mismatch $v]} : {[list match $v]}}] + set mismatches_display [lmap m $match_state v $var_names {expr {$m != 1} ? {$v} : {[string repeat " " [string length $v]]}}] + set msg "Match error: No match of right hand side for vars in $multivar\n" + append msg "vars/atoms: $var_names\n" + append msg "mismatches: [join $mismatches_display { } ]\n" + set i 0 + foreach mismatchinfo $mismatches { + lassign $mismatchinfo status nm + if {$status eq "mismatch"} { + # nm can be empty string + set varclass [lindex $var_class $i 1] + set val [lindex $var_actions $i 2] + if {$varclass == 1} { + set type "atom" + set e $nm + } elseif {$varclass == 2} { + set type "pinned var" + set e "?" + } else { + set type "var" + set e "*" ;# non-atoms and non-pins should always match.. todo - review attempt to set as var instead of array? + } + append msg " $type: '$nm' expected: '$e' got '$val'\n" + } + incr i + } + error $msg + } + + if {![llength $varspeclist]} { + dict set returndict result $data + } else { + punk::assert {$i == [llength $varspeclist]} + + dict set returndict result $returnval + } + return $returndict + } + + #same as used in unknown func for initial launch + #variable re_assign {^([^\r\n=\{]*)=(.*)} + variable re_assign {^[\{]{0,1}([^\r\n=]*)=(.*)} + variable re_dot_assign {^([^\r\n=\{]*)\.=(.*)} + #know_assign is tailcalled from unknown - uplevel 1 gets to caller level + proc know_assign {multivar e1 fulltail} { + debug.punk.pipe {know_assign '$multivar' '$e1' '$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." + #} + + + #attempting to allow x=y to begin a pipeline e.g x=y |> string tolower + #will stop us from easily assigning an entire pipeline string to x using the 'equals-runon' syntax x=.=something etc |> blah + #The tradeoff + if {1} { + #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 "|*>"] + + if {$firstpipe_posn >=0} { + set firstpipe [lindex $fulltail $firstpipe_posn] + set tail [lrange $fulltail 0 $firstpipe_posn-1] + set nextassignment [lindex $fulltail $firstpipe_posn+1] + set nexttail [lrange $fulltail $firstpipe_posn+1 end] + } else { + set tail $fulltail + set nextassignment [list] + set nexttail [list] + } + #puts stderr "tail len: [llength $fulltail]" + #puts stderr "tail-end: [lindex $fulltail end]" + } + + + set is_listbuilder 0 + + if {![string length $e1]} { + #space after = + if {[llength $tail] == 1} { + set val [lindex $tail 0] + set d [_multi_assign_result $multivar $val] + set r [dict get $d result] + set returnval $r + } elseif {[llength $tail] == 0} { + _multi_assign_result $multivar "" -unset 1 ;#final arg 1 to unset variables + #uplevel 1 [list unset $multivar] + set returnval "" + } else { + set 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 + } + } elseif {([llength $tail] == 0) && ($firstpipe_posn < 0)} { #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 ,] - _multi_assign_expression_result $multivar $expression1 - return $expression1 - } elseif {![catch {expr $expression1 {*}$tail} evaluated]} { - puts stderr ">evaluated $expression1 {*}$tail as expression" - - _multi_assign_expression_result $multivar $evaluated - #return [uplevel 1 [list set $var $evaluated]] - return $evaluated + set d [_multi_assign_result $multivar $e1] + set r [dict get $d result] + set returnval $r } else { - puts stderr ">>expression: $expression1" - set leader [string index $expression1 0] - if {$leader in [list \" \{ ]} { - set expression1 [string range $expression1 1 end] - set newtail [list] - foreach block $tail { - set b [linelist $block] - lappend newtail $b - } - set tail $newtail + set is_listbuilder 1 + #no space concatenation - good for command aliases + debug.punk.pipe "assigning fulltail [llength $fulltail]" 6 + #e1 is not a list - may even be a single char such as double quote. + #set result [concat $e1 $fulltail] ;#concat produces a string rep - and strips escaped whitespace e.g \t or\n from e1 and trailing args. + set result [list] + lappend result $e1 + foreach a $fulltail { + lappend result $a } - #set expression1 [string trimleft $expression1 \"] - #set expression1 [string trimleft $expression1 \{] - - set build "" - set cmdstr "" - set wordlike_parts [regexp -inline -all {\S+} "$expression1 $tail"] - foreach t $wordlike_parts { - set t [string trim $t \"] - if {![string length $build]} { - if {[info complete $t]} { - append cmdstr " $t" - continue + + set d [_multi_assign_result $multivar $result] + set r [dict get $d result] + set returnval $r + } + + #return $returnval + + if {![llength $nexttail] || $is_listbuilder} { + return $returnval + } else { + set exectail [concat [list val $returnval] $firstpipe $nexttail] + #uplevel 1 [list punk::know_exec "" "" {*}$exectail] + tailcall punk::know_exec "" "" {*}$exectail + } + + + } + proc _is_math_func_prefix {e1} { + #also catch starting brackets.. e.g "(min(4,$x) " + if {[regexp {^[^[:alnum:]]*([[:alnum:]]*).*} $e1 _ word]} { + #possible math func + if {$word in [info functions]} { + return true + } + } + return false + } + + #todo - option to disable these traces which provide clarifying errors (performance hit?) + proc pipeline_args_read_trace_error {args} { + error "The pipelined data doesn't appear to be a valid Tcl list\nModify input, or use \$data instead of \$args." + } + + #detect and retrieve %xxx% elements from item without affecting list/string rep + #commas, @, ', ^ and whitespace not part of a valid tag (allows some substitution within varspecs) + #%% is not a valid tag + #(as opposed to using regexp matching which causes string reps) + proc get_tags {item} { + set chars [split $item {}] + set terminal_chars [list , @ ' ^ " " \t \n \r] + #note % is both terminal and initiating - so for the algorithm we don't include it in the list of terminal_chars + set nonterminal [lmap v $chars {expr {$v ni $terminal_chars}}] + set percents [lmap v $chars {expr {$v eq "%"}}] + #useful for test/debug + #puts "CHARS : $chars" + #puts "NONTERMINAL: $nonterminal" + #puts "PERCENTS : $percents" + set sequences [list] + set in_sequence 0 + set start -1 + set end -1 + set i 0 + #todo - some more functional way of zipping/comparing these lists? + set s_length 0 ;#sequence length including % symbols - minimum for tag therefore 2 + foreach n $nonterminal p $percents { + if {!$in_sequence} { + if {$n & $p} { + set s_length 1 + set in_sequence 1 + set start $i + set end $i + } else { + set s_length 0 + } + } else { + if {$n ^ $p} { + incr s_length + incr end + } else { + if {$n & $p} { + if {$s_length == 1} { + # % followed dirctly by % - false start + #start again from second % + set s_length 1 + set in_sequence 1 + set start $i + set end $i + } else { + incr end + lappend sequences [list $start $end] + set in_sequence 0 + set s_length 0 + set start -1; set end -1 + } + } else { + #terminated - not a tag + set in_sequence 0 + set s_length 0 + set start -1; set end -1 + } + } + } + incr i + } + + set tags [list] + foreach s $sequences { + lassign $s start end + set parts [lrange $chars $start $end] + lappend tags [join $parts ""] + } + return $tags + } + + #show underlying rep of list and first level + proc rep_listname {lname} { + upvar $lname l + set output "$lname list rep: [rep $l]\n" + foreach item $l { + append output "-rep $item\n" + append output " [rep $item]\n" + } + return $output + } + + # + # + # relatively slow on even small sized scripts + proc arg_is_script_shaped2 {arg} { + set re {^(\s|;|\n)$} + set chars [split $arg ""] + if {[lsearch -regex $chars $re] >=0} { + return 1 + } else { + return 0 + } + } + + proc arg_is_script_shaped {arg} { + if {[string first " " $arg] >= 0} { + return 1 + } + if {[string first \n $arg] >= 0} { + return 1 + } + if {[string first ";" $arg] >= 0} { + return 1 + } + if {[string first \t $arg] >= 0} { + return 1 + } + return 0 + } + + proc know_exec {initial_returnvarspec e1 args} { + set fulltail $args + debug.punk.pipe {call know_exec: '$initial_returnvarspec' '$e1' '$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 next1 [lindex $fulltail 0] + set nexttail [lrange $fulltail 1 end] + } else { + set next1 $e1 + set nexttail $fulltail + } + if {$next1 eq "pipematch"} { + set results [uplevel 1 [list pipematch {*}$nexttail]] + debug.punk.pipe {>>> pipematch results: $results} 1 + + set d [_multi_assign_result $initial_returnvarspec $results] + set r [dict get $d result] + + return $r + } + + if {![arg_is_script_shaped $next1]} { + if {[regexp $punk::re_dot_assign $next1 _ nextreturnvarspec nextrhs]} { + #non pipelined call to self - return result + debug.punk.pipe {nextreturnvarspec: $nextreturnvarspec nextrhs:$nextrhs tail:$nexttail} 0 + set results [uplevel 1 [list ::punk::know_exec $nextreturnvarspec $nextrhs {*}$nexttail]] + debug.punk.pipe {>>> results: $results} 1 + + set d [_multi_assign_result $initial_returnvarspec $results] + set r [dict get $d result] + + return $r + } + + if {[regexp $punk::re_assign $next1 _ nextreturnvarspec nextrhs]} { + #non pipelined call to plain = assignment - return result + debug.punk.pipe {nextreturnvarspec: $nextreturnvarspec nextrhs:$nextrhs tail:$nexttail} 0 + set results [uplevel 1 [list ::punk::know_assign $nextreturnvarspec $nextrhs $nexttail]] + debug.punk.pipe {>>> results: $results} 1 + + set d [_multi_assign_result $initial_returnvarspec $results] + set r [dict get $d result] + + return $r + } + } + + #--------------------------------------------------------------------- + + #todo add 'op' argument and handle both .= and = + # + #|> data piper symbol + #<| args piper symbol (arguments supplied at end of pipeline e.g from commandline or from calling and/or currying the command) + # + set re_is_piper {^\|(.*)>$} ;#for use with lmap. captures forwarding argspecs within the |> pipe symbol (e.g |x@@key,y@end>) + set re_is_argpiper {^<(.*)\|$} + + set more_pipe_segments 1 ;#first loop + + #this contains the main %data% and %datalist% values going forward in the pipeline + #as well as any extra pipeline vars defined in each |> + #It also contains any 'args' with names supplied in <| + set dict_tagval [dict create] ;#cumulative %x% tag dict which operates on the whole length of the pipeline + + #determine if there are input args at the end of the pipeline indicated by reverse <| symbol possibly with argspecs e.g transform x y z =0} { + set apipe_posn [expr {[llength $fulltail] - $apipe_posn_reverse -1}] + set datatail [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 " e f g |> h + #set firstpipe_posn [lsearch $tailmap {| >}] + + set firstpipe_posn [lsearch $tailremaining "|*>"] + + if {$firstpipe_posn >=0} { + set outpipespec [string range [lindex $tailremaining $firstpipe_posn] 1 end-1] + set segment_members [lrange $tailremaining 0 $firstpipe_posn-1] + set tailremaining [lrange $tailremaining $firstpipe_posn+1 end] + } else { + set segment_members $tailremaining + set tailremaining [list] + } + + + + set script_like_first_word 0 + set rhs $e1 + set segment_members_script_index [list] + if {![string length $e1]} { + set segment_first_word [lindex $segment_members 0] + set segment_second_word [lindex $segment_members 1] + #first word of initial call is alays x.=y even if x and y are empty - so we only need to check second word + if {[arg_is_script_shaped $segment_second_word]} { + set segment_members_script_index 1 + } + + } else { + set segment_first_word $e1 ;#don't look for scriptiness here.. can only be list or expr + set segment_second_word [lindex $segment_members 0] + if {[arg_is_script_shaped $segment_second_word]} { + set segment_members_script_index 0 + } + + } + #tailremaining includes x=y during the loop. + set returnvarspec $initial_returnvarspec + catch {unset previous_result} ;# to emphasize we want it unset for first iteration - differentiate from empty string + + set segment_result_list [list] + set i 0 ;#segment id + set j 1 ;#next segment id + set pipespec(args) $argpipespec ;# from trailing <| + set pipespec(0,in) $inpipespec + set pipespec(0,out) $outpipespec + + set max_iterations 50 ;# configurable? -1 for no limit ? + while {$more_pipe_segments == 1} { + set segment_result "" + if {[info exists previous_result]} { + set prevr $previous_result + } else { + set prevr "" + } + #--------------------------------- + debug.punk.pipe {[a+ yellow bold]i$i SEGMENT MEMBERS([llength $segment_members]): $segment_members[a+]} 4 + debug.punk.pipe {[a+ yellow bold]i$i TAIL REMAINING([llength $tailremaining]): $tailremaining[a+]} 4 + debug.punk.pipe {[a+] inpipespec(prev [a+ yellow bold]|$pipespec($i,in)[a+]>) outpipespec(next [a+ yellow bold]|$pipespec($i,out)>[a+])} 4 + debug.punk.pipe {[a+] previous_iteration_result: $prevr[a+]} 6 + debug.punk.pipe {[a+ cyan bold] segment_members_script_index:$segment_members_script_index} 4 + if {[llength $segment_members_script_index]} { + debug.punk.pipe {[a+ cyan bold] script segment: [lindex $segment_members $segment_members_script_index][a+]} 4 + } + + if {$i == $max_iterations} { + puts stderr "ABORTING. Reached max_iterations $max_iterations (todo: make configurable)" + set more_pipe_segments 0 + } + + + #set firstwordparts [regexp -inline -all {\S+} $segment_first_word] + + + + ##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 pipedvars [dict create] + if {[string length $pipespec($i,in)]} { + #check the varspecs within the input piper even if no %varname% tags present. + # - data and/or args may have been manipulated + set d [apply {{mv res} { + punk::_multi_assign_result $mv $res -levelup 1 + }} $pipespec($i,in) $prevr] + set pipedvars [dict get $d setvars] + #puts stdout "inpipespec:|$pipespec($i,in)> prevr:$prevr setvars: $pipedvars" + } + + #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] + } + } + } + if {[dict exists $pipedvars "data"]} { + dict set dict_tagval %data% [list [dict get $pipedvars "data"]] + } else { + if {[info exists previous_result]} { + dict set dict_tagval %data% $prevr + } + } + foreach {k v} $pipedvars { + #add additionally specified vars and allow overriding of %args% and %data% + if {$k in [list "datalist" "data"]} { + #already done + continue + } + #dict set dict_tagval %$k% [list $v] + dict set dict_tagval %$k% $v + } + + debug.punk.pipe.var {dict_tagval: $dict_tagval} 4 + + + + + + + + #check it's still a valid list? + if {!$segment_has_tags} { + 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 + set segment_members_filled $segment_members + if {[dict exists $dict_tagval %data%]} { + lappend segment_members_filled [dict get $dict_tagval %data%] + } + + } else { + 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 + } + } else { + #nothing to put - omit in output + } + } 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] + } + } else { + lappend segment_members_filled [string map $dict_tagval $mem] + } + } else { + lappend segment_members_filled $mem + } + incr idxmem + } + #note - length of segment_members_filled may now differ from length of original segment_members! + + #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 + + + #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 ".="} { + + + #set subresult [uplevel 1 [list ::punk::know_exec $returnvarspec $rhs $segment_members_filled]] + 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 } + + puts stderr ">evaluating $e as expression\n due to brace \"\{\" immediately following .=" + + if {![catch {uplevel 1 [list expr $e]} evaluated]} { + set forward_result $evaluated + set d [_multi_assign_result $returnvarspec $forward_result] + set r [dict get $d result] + #return $r + set segment_result $r + } else { + set 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 {([string is double -strict $rhs] || [_is_math_func_prefix $rhs])} { + 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_assign_result $returnvarspec $forward_result] + set r [dict get $d result] + #return $r + set segment_result $r + } else { + set 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 + } + lappend cmdlist {*}[lrange $segment_members_filled 1 end] + #set cmdlist [concat $rhs [lrange $segment_members_filled 1 end]] ;#ok if rhs empty + + set firstword [lindex $cmdlist 0] + debug.punk.pipe {>>firstword: $firstword returnvarspec:$returnvarspec} 4 + debug.punk.pipe {>>cmdlist([llength $cmdlist]): $cmdlist} 4 + debug.punk.pipe.rep {[a+ yellow bold][rep_listname cmdlist][a+]} 4 + #set c1 [string index $firstword 0] + #if {$c1 in [list \" "("]} { + # set firstword [string range $firstword 1 end] + # lset cmdlist 0 $firstword + #} + #puts stderr ">>cmdlist: $cmdlist" + set forward_result [uplevel 1 $cmdlist] + debug.punk.pipe {forward_result: $forward_result} 4 + debug.punk.pipe.rep {[a+ yellow bold]forward_result REP: [rep $forward_result][a+]} 4 + + set d [_multi_assign_result $returnvarspec $forward_result] + set r [dict get $d result] + set segment_result $r + #puts stderr ">>forward_result: $forward_result segment_result $r" } - append build " $t" - if {[info complete $build]} { - #append cmdstr " [string trim $build \"]" - append cmdstr $build - set build "" + + + + } elseif {$segment_op eq "="} { + set segment_result [uplevel 1 [list ::punk::know_assign $returnvarspec $rhs [lrange $segment_members_filled 1 end]]] + #review + set forward_result $segment_result + + + + } elseif {[llength $segment_members_script_index]} { + #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%"} { + #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 } - } - #set result [uplevel 1 $cmdstr] - #set result [uplevel 1 [concat $expression1 $tail]] - #set result [uplevel 1 [$expression1 {*}$tail]] - if {$leader in [list \" \{ ]} { - #?? - puts stderr ">>>uplevel 1 [concat $expression1 $tail]" - set result [uplevel 1 [concat $expression1 $tail]] - # - #set result [linelist $result] - puts stderr "-- '$result'" - _multi_assign_expression_result $multivar $result + + 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 + set add_argsdata 1 + if {[dict exists $dict_tagval "%argsdata%"]} { + set argsdatalist [dict get $dict_tagval "%argsdata%"] + #see if the raw result can be treated as a list + if {[catch {lindex $argsdatalist 0}]} { + #we cannot supply 'args' + set pre_script "" + #todo - only add trace if verbose warnings enabled? + append pre_script "trace add variable argsdata read punk::pipeline_args_read_trace_error\n" + set script $pre_script + append script $segment_first_word + set add_argsdata 0 + } + } + + if {!$add_argsdata} { + #puts stderr "APPLY1: args:$segmentargnames" + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals" + set evaluation [apply [list $segmentargnames $script ::] {*}$segmentargvals] + } else { + #puts stderr "APPLY2: args:$segmentargnames" + #puts stderr " script: $script" + #puts stderr " vals: $segmentargvals $argsdatalist" + set evaluation [apply [list [concat $segmentargnames argsdata] $script ::] {*}$segmentargvals $argsdatalist] + } + set forward_result $evaluation + set d [_multi_assign_result $returnvarspec $forward_result] + set r [dict get $d result] + set segment_result $r + } else { - puts stderr ">no leader" - set result [uplevel 1 [concat $expression1 $tail]] - puts stderr "-- '$result'" - _multi_assign_expression_result $multivar $result + #tags ? + debug.punk.pipe {>>raw commandline: [concat $rhs $segment_members_filled]} 5 + set forward_result [uplevel 1 [concat $rhs $segment_members_filled]] + set d [_multi_assign_result $returnvarspec $forward_result] + set r [dict get $d result] + set segment_result $r } - #return [uplevel 1 [list set $multivar [uplevel 1 [concat $expression1 $tail]]]] - return $result - } + #the subresult doesn't need to go backwards - as the final assignment can emit the result into a variable + #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section + #It may however make a good debug point + #puts stderr "segment $i segment_result:$segment_result" + + + + + + + + + + #examine tailremaining. + # either x x x |?> y y y ... + # or just y y y + #we want the x side for next loop + + #set up the conditions for the next loop + #|> x=y args + # inpipespec - contents of previous piper |xxx> + # outpipespec - empty or content of subsequent piper |xxx> + # previous_result + # assignment (x=y) + + + set pipespec($j,in) $pipespec($i,out) + set outpipespec "" + set tailmap "" + set next_pipe_posn -1 + if {[llength $tailremaining]} { + + #set tailmap [lmap v $tailremaining {lreplace [split $v {}] 1 end-1}] + ##e.g for: a b c |> e f g |> h + #set next_pipe_posn [lsearch $tailmap {| >}] + set next_pipe_posn [lsearch $tailremaining "|*>"] + + set outpipespec [string range [lindex $tailremaining $next_pipe_posn] 1 end-1] + } + set pipespec($j,out) $outpipespec + + + set segment_members_script_index [list] + set script_like_first_word 0 + 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 tailremaining [lrange $tailremaining $next_pipe_posn+1 end] + + } else { + set segment_members $tailremaining + set tailremaining [list] + } + + + #assignment is the arg immediately following |> operator e.g x.=blah or x=etc (or a normal command or script!) + set segment_first_word "" + 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] + set segment_members_script_index 0 + set segment_op "" + + } else { + set possible_assignment [lindex $segment_members 0] + if {[regexp $punk::re_dot_assign $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] + } + } elseif {[regexp $punk::re_assign $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] + + } 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] + #puts stderr ">>3 no-operator segment_first_word: '$segment_first_word'" + } + } + + + } else { + #?? two pipes in a row ? + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a+]} 0 + set segment_members return + set segment_first_word return + } + + set forward_result $segment_result + set previous_result $forward_result + } else { + debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a+]} 4 + set more_pipe_segments 0 + } + + #the segment_result is based on the leftmost var on the lhs of the .= + #whereas forward_result is always the entire output of the segment + lappend segment_result_list $segment_result + incr i + incr j + } ;# end while + + return [lindex $segment_result_list end] + #return $forward_result } proc configure_unknown {} { @@ -298,8 +1527,8 @@ namespace eval punk { #---------------- #for var="val {a b c}" - proc ::punk::arg {arg} {return $arg} - proc ::punk::val {v} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {tailcall lindex $v} + proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version #---------------- #can't use know - because we don't want to return before original unknown body is called. @@ -324,19 +1553,43 @@ namespace eval punk { expr $args } - know {[regexp {^([0-9]+)\.\.([0-9]+)$} [lindex $args 0] -> from to]} { - set res {} - while {$from<=$to} {lappend res $from; incr from} - set res + #it is significantly faster to call a proc like this than to inline it in the unknown proc + proc ::punk::range {from to args} { + set count [expr {($to -$from) + 1}] + incr from -1 + return [lmap v [lrepeat $count 0] {incr from}] + } + know {[regexp {^([0-9]+)\.\.([0-9]+)$} [lindex $args 0 0] -> from to]} { + punk::range $from $to } #if {[info proc ::punk::_unknown] eq ""} {rename unknown ::punk::_unknown} - know {[regexp {([^=]*)=(.*)} [lindex $args 0] _ var expression1]} { - if {![string length $var]} { - error "usage var=val Var cannot be empty string using this syntax. Use ''set {} val' if you want to set a var with an empty-string name" + + #variable re_assign {^([^\r\n=\{]*)=(.*)} + know {[regexp $punk::re_assign [lindex $args 0 0] _ varspecs rhs]} { + if {![string length $varspecs]} { + #todo allow = with novar and just return value + #error "usage varspecs=val varspecs cannot be empty string using this syntax. Use ''set {} val' if you want to set a var with an empty-string name" + } + + #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 + if {[lindex $args 0] ne [lindex $args 0 0]} { + regexp $punk::re_assign [lindex $args 0] _ varspecs rhs } + set tail [lrange $args 1 end] - tailcall ::punk::know_assign $var $expression1 $tail + #must be tailcall so know_assign runs at same level as the unknown proc + tailcall ::punk::know_assign $varspecs $rhs $tail + } + #.= must come after = here to ensure it comes before = in the 'unknown' proc + #set punk::re_dot_assign {([^=]*)\.=(.*)} + know {[regexp $punk::re_dot_assign [lindex $args 0 0] _ varspecs rhs]} { + set tail [expr {([lindex $args 0] eq [lindex $args 0 0]) ? [lrange $args 1 end] : [concat [lrange [lindex $args 0] 1 end] [lrange $args 1 end] ] }] + tailcall ::punk::know_exec $varspecs $rhs {*}$tail + #return [uplevel 1 [list ::punk::know_exec $varspecs $rhs {*}$tail]] } #ensure == is after = in know sequence know {[regexp {(.*)==(.*)} [lindex $args 0] _ val1 val2]} { @@ -369,16 +1622,45 @@ namespace eval punk { } } } - know {[regexp {^=([^=]*)} [lindex $args 0] _ v1]} { - set calc [concat $v1 [lrange $args 1 end]] - puts stderr "= $calc" - return [expr $calc] - } + #know {[regexp {^=([^=]*)} [lindex $args 0] _ v1]} { + # set calc [concat $v1 [lrange $args 1 end]] + # puts stderr "= $calc" + # return [expr $calc] + #} } configure_unknown - #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwords. + #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. + proc pipematch {args} { + variable re_dot_assign + variable re_assign + + set assign [lindex $args 0] + set arglist [lrange $args 1 end] + if {$assign eq ".="} { + set cmdlist [list ::punk::know_exec "" "" {*}$arglist] + } elseif {$assign eq "="} { + set cmdlist [list ::punk::know_assign "" "" $arglist] + } elseif {[regexp $re_dot_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::know_exec $returnvarspecs $rhs {*}$arglist] + } elseif {[regexp $re_assign $assign _ returnvarspecs rhs]} { + set cmdlist [list ::punk::know_assign $returnvarspecs $rhs $arglist] + } else { + set cmdlist $args + #return [dict create error [dict create reason [dict create pipematch bad_first_word value $assign pipeline [list pipematch $assign {*}$args]]]] + } + + if {[catch {uplevel 1 $cmdlist} result]} { + debug.punk.pipe {pipematch error $result} 4 + return [dict create error [dict create reason $result]] + } else { + debug.punk.pipe {pipematch result } + return [dict create ok [dict create result $result]] + } + + + } proc ansi+ {args} { variable ansi_disabled if {$ansi_disabled == 1} { @@ -391,8 +1673,10 @@ namespace eval punk { if {[string length $onoff]} { set onoff [string tolower $onoff] if {$onoff in [list 1 on true yes]} { + interp alias "" a+ "" punk::ansi+ set ansi_disabled 0 } elseif {$onoff in [list 0 off false no]} { + interp alias "" a+ "" control::no-op set ansi_disabled 1 } else { error "punk::ansi expected 0|1|on|off|true|false|yes|no" @@ -796,8 +2080,10 @@ namespace eval punk { return $linelist } - # - proc linelist {text} { + # important for know_exec & know_assign + # lineval verbatim|trimmed + proc linelist {text {lineval verbatim}} { + if {$lineval ni [list verbatim trimmed]} {error "linelist 2nd argument valid values are 'verbatim' or 'trimmed'"} set linelist [list] if {[string first \n $text] < 0} { return $text @@ -812,9 +2098,19 @@ namespace eval punk { set end "end-1" } set alist [lrange $nsplit $start $end] - lappend linelist {*}$alist + if {$lineval eq "verbatim"} { + set linelist $alist + #lappend linelist {*}$alist + } else { + foreach ln $alist { + lappend linelist [string trim $ln] + } + } return $linelist } + + + #!!!todo fix - linedict is unfinished and non-functioning #linedict based on indents proc linedict {args} { set data [lindex $args 0] @@ -939,7 +2235,11 @@ namespace eval punk { set existing [interp alias "" $aliasorglob] puts stderr "Overwriting existing alias $aliasorglob -> $existing with $aliasorglob -> $args (in current session only)" } - interp alias "" $aliasorglob "" {*}$args + if {([llength $args] ==1) && [string trim [lindex $args 0]] eq ""} { + #use empty string/whitespace as intention to delete alias + return [interp alias "" $aliasorglob ""] + } + return [interp alias "" $aliasorglob "" {*}$args] } else { if {![string length $aliasorglob]} { set aliaslist [punk aliases] @@ -959,6 +2259,11 @@ namespace eval punk { interp alias {} know {} punk::know interp alias {} know? {} punk::know? + #interp alias {} arg {} punk::val + interp alias {} val {} punk::val + + interp alias {} exitcode {} punk::exitcode + interp alias {} ansi {} punk::ansi interp alias {} a+ {} punk::ansi+ @@ -970,7 +2275,6 @@ namespace eval punk { interp alias {} sh_ECHO {} punk::sh_ECHO - interp alias {} exitcode {} punk::exitcode #friendly sh aliases (which user may wish to disable e.g if conflicts) @@ -991,10 +2295,69 @@ namespace eval punk { #---------------------------------------------- interp alias {} linelistraw {} punk::linelistraw - interp alias {} linelist {} punk::linelist + interp alias {} linelist {} punk::linelist ;#critical for = assignment features interp alias {} linedict {} punk::linedict interp alias {} dictline {} punk::dictline + interp alias {} pipematch {} punk::pipematch + + proc = {value} { + return $value + } + proc .= {args} { + uplevel 1 [list ::punk::know_exec "" "" {*}$args] + } + #interp alias {} = {} punk::know_assign "" + #interp alias {} .= {} punk::know_exec "" + + interp alias {} foldl {} struct::list::Lfold + #foldl helpers + proc add_llength {total listval} { + expr {$total + [llength $listval]} + } + proc add_length {total stringval} { + expr {$total + [string length $stringval]} + } + package require pattern + >pattern .. Create >f + >f .. Method foldl {total func sequence} { + struct::list::Lfold $sequence $total $func + } + #note: foldr is not equivalent to just doing a foldl on the reversed list + #todo - review/test/fix + >f .. Method foldr {total func sequence} { + set this @this@ + if {![llength $sequence]} { + return $total + } + v,h@head,t@tail.=val $sequence |h@head,t@tail> { + puts "-->$h" + $func [$this . foldr $total $func $t] $h + } f .. Method add_llength {total listval} { + expr {$total + [llength $listval]} + } + >f .. Method add_length {total stringval} { + expr {$total + [string length $stringval]} + } + >f .. Method debug {total item} { + puts stderr "incr tally: $total item: $item" + expr {$total + 1} + } + >f .. Method dict_walk {d key} { + dict get $d key + } + >f .. Method sum {total num} { + expr {$total + $num} + } + + #example of aliasing a punk pipeline + interp alias {} _commands {} .=info commands punk::%glob% |> .=lmap v %data% {namespace tail $v}