Sugar macros collection

A page to collect useful Sugar macros.


inlist command macro

Expands

 [inlist $list $element]

in

 [expr {[lsearch -exact $list $element] != -1}]

This one was suggested by Richard Suchenwirth (but the implementation is mine (SS), so even possible bugs):

 sugar::macro inlist argv {
     if {[llength $argv] != 3} {error "Bad number of arguments"}
     list expr \
         "\{\[lsearch -exact [lindex $argv 1] [lindex $argv 2]\] != -1\}"
 }

AMG: See also in, which can be used as a command as well as a math operator in Tcl 8.5.


tailrec_proc transformer

A wrapper to proc able to optimize tail recursive calls. For example the following:

 tailrec_proc ack {m n} {
    if {$m == 0} {
        return [expr {$n + 1}]
    } elseif {$n == 0} {
        ack [expr {$m - 1}] 1
    } else {
        ack [expr {$m - 1}] [ack $m [expr {$n - 1}]]
    }
 }

will be equivalent to:

 proc ack {m n} {
    while 1 {
        if {$m == 0} {
            return [expr {$n + 1}]
        } elseif {$n == 0} {
            lassign [list [expr {$m - 1}] 1] m n
            continue
        } else {
            lassign [list [expr {$m - 1}] [ack $m [expr {$n - 1}]]] m n
            continue
        }
        break
    }
 }

This is the example in the page Sugar transformers, but with some bugfix. This new version care a bit more about indentation and does not use temp variables, but foreach to perform multi-assignment (thanks to Richard Suchenwirth for the idea).

 proc tailrec_proc {name arglist body} {
     # Convert the script into a Tcl list
     set l [sugar::scriptToList $body]
     # Convert tail calls
     set l [tailrec_convert_calls $name $arglist $l]
     # Add the final break
     lappend l [list {TOK break} {EOL "\n"}]
     # Convert it back to script
     set body [sugar::listToScript $l]
     # Add the surrounding while 1
     set body "while 1 {$body}"
     # Call [proc]
     uplevel proc [list $name $arglist $body]
 }
 
 # Convert tail calls. Helper for tailrec_proc.
 # Recursively call itself on [if] script arguments.
 proc tailrec_convert_calls {name arglist code} {
     # Search the last non-null command.
     set lastidx -1
     for {set j 0} {$j < [llength $code]} {incr j} {
         set cmd [lindex $code $j]
         if {[sugar::indexbytype $cmd TOK 0] != -1} {
             set lastidx $j
             set cmdidx [sugar::indexbytype $cmd TOK 0]
         }
     }
     if {$lastidx == -1} {
         return $code
     }
     set cmd [lindex $code $lastidx]
     set cmdname [lindex $cmd $cmdidx 1]
     if {[lindex $cmd 0 0] eq {SPACE}} {
         set space [lindex $cmd 0 1]
     } else {
         set space " "
     }
     if {$cmdname eq $name} {
         #puts "TAILCALL -> $cmdname"
         set recargs [lrange [sugar::tokens $cmd] 1 end]
         set t [list [list SPACE $space] [list TOK foreach] [list SPACE " "]]
         lappend t [list TOK "\[list "]
         foreach a $arglist {
             lappend t [list TOK $a] [list SPACE " "]
         }
         lappend t [list TOK "\] "]
         lappend t [list TOK "\[list "]
         foreach a $recargs {
             lappend t [list TOK $a] [list SPACE " "]
         }
         lappend t [list TOK "\] "]
         lappend t [list TOK break] [list EOL "\n"]
         set code [linsert $code $lastidx $t]
         incr lastidx
         lset code $lastidx [list [list SPACE $space] [list TOK continue] [list EOL "\n"]]
     } elseif {$cmdname eq {if}} {
         #puts "IF CALL"
         for {set j 0} {$j < [llength $cmd]} {incr j} {
             if {[lindex $cmd $j 0] ne {TOK}} continue 
             switch -- [lindex $cmd $j 1] {
                 if - elseif {
                     incr j 2
                 }
                 else {
                     incr j 1
                 }
                 default {
                     set script [lindex $code $lastidx $j 1]
                     #puts "$j -> $script"
                     set scriptcode [sugar::scriptToList [lindex $script 0]]
                     set converted [tailrec_convert_calls $name $arglist $scriptcode]
                     lset code $lastidx $j 1 [list [sugar::listToScript $converted]]
                 }
             }
         }
     }
     return $code
 }

AMG: See tailcall.


Math commands as macro

Expands

 [+ $a $b $c]

to

 [expr {$a+$b+$c}]

Performs some pre-calculation of the value at compile time when possible. For example

 [+ [* [+ 1 2] 4] 1]

is expanded to

 [expr 13]

and so on.

NOTE: this macro requires sugar 0.1 to be used, it's availabe for download from the Sugar page.

 package require sugar
 
 sugar::macro {+ - / *} {op args} {
     for {set j 0} {$j < [llength $args]} {incr j} {
        # Is the operand in the form [expr {<valid-double>}]?
        # This can be translated to just <valid-double>
        if {[regexp {^\[expr (.*)\]$} [lindex $args $j] => double]} {
            if {[string is double -strict $double]} {
                lset args $j $double
            }
        }
        if {![string is double -strict [lindex $args $j]]} break
     }
     if {$j == [llength $args]} {
        # The expression can be computed at compile-time?
        list expr "[eval expr [join $args $op]]"
     } else {
        list expr "{[join $args $op]}"
     }
 }
 
 sugar::proc test {} {
     puts [* [+ 5 2] 4]
     puts [+ 1]
 }
 
 puts [info body test]
 test

AMG: See tcl::mathfunc and tcl::mathop.


A/AK is using sugar for forward-compatible {*}


A/AK work above had inspired to FM a math expand syntax extension :

# word prefixed by {=} is treated as an expression : ie {=}expression is transformed in [expr {expression}]

sugar::syntaxmacro mathexpand args {
   # The first thing we check: is there anything to expand?
   if {[lsearch $args {{=}?*}]==-1} {
       # and if there is none...
       return $args
    } else {
       set evalCmd []
       foreach token $args {
          # If the arg is expanded
          if {[string match {{=}?*} $token]} {
              set whattoexpand [string range $token 3 end]
              lappend evalCmd "\[expr {$whattoexpand}\]"
          } else {
              # we append a one-element [list]
              # to the eval's argument list
              lappend evalCmd "$token"
          }
      }
      return $evalCmd
   }
}
# test
package require Tk

sugar::proc testmathexpand {} {
   set A [list  {=}([winfo screenwidth .]-[winfo width .])/2 \
                {=}([winfo screenheight .]-[winfo height .])/2]
   return +[join $A +]
}

proc testexpr {} {
   set A [list  [expr {([winfo screenwidth .]-[winfo width .])/2}] \
                [expr {([winfo screenheight .]-[winfo height .])/2}]]
   return +[join $A +]
}
wm geom . [testmathexpand]
wm geom . [testexpr]
time {testmathexpand} 10000 ;# -> 4.7 microseconds per iteration
time {testexpr} 10000; # -> 4.7 microseconds per iteration

AMG: The empty script substitution [] produces empty string, but it's not a very common idiom. In fact, this is the first time I have seen it used in practice. Profiling via time shows it to be as fast as the alternatives. In actual code, I've only ever seen "", {}, and sometimes [list] on the theory that it slightly reduces shimmering. (By the way, using [list] to construct an empty list doesn't improve performance.)