Macro Facility for Tcl

Macro Facility for Tcl

See Also

Tmac - a Tcl macro processor package, Roy Terry

Description

Todd Coram:

What would it take to add a macro facility to Tcl? What use would such a facility be?

Well, for starters, it would make syntatical sugar (atrocities) such as:

proc first {l} {
  lindex $l 0
}

proc rest {l} {
  lrange $l 1 end
}

cost less in terms of performance. Yes, yes, I know -- if performance really mattered I shouldn't be doing this in Tcl. But the difference between calling my own procs vs internal ones (especially in deep loops) count, right?

Well, what about immediate macros (or macros that have a compile time behavior). Common Lisp should come to mind, but I can't help thinking about Forth immediate words. So, I could do something like:

mac log {msg} {
   global debug
   if {$debug} {
           return "puts stderr $msg"
   } else {
           return "\;"
   }
}

and use it thusly:

set debug 0 
...
proc something {} {
  set stuff "contents not available for introspection by macro yet." 
  log "Some noise I may not want to see depending on the value of debug"
  log $stuff; # if debug was 1, then the macro would expand to: puts stderr $stuff
  ...
}

The above assumes that I have redefined proc to point to a macro evaluator to execute all macros before actually defining the real proc. The return value of the macro evaluation is what replaces the macro in the procedure. In this example, I can do conditional compilation!

This could all be done in plain Tcl if I gave Tcl access to the Tcl C parser code (to properly locate the arguments for the expanding macro). Note: You are limited by what context is available when executing the macro (you can for instance look into the surrounding proc's variables since we are in compile mode --- there aren't any values for the variables yet so the arguments to the macros are not eval'd!).

Another use for an immediate macro facility:

mac  mloop {idx cnt cmd} {
   return "for {set $idx 0} {\$[set $idx] < $cnt} {incr $idx} {$cmd}"
}

proc something {} {
  mloop i 5 {puts "hello $i"}
  # above expands to:  for {set i 0} {$i < 5} {incr i} {puts "hello $i"}
}

PYK 2013-10-26: These days, such a thing could be implemented using tailcall:

proc mloop {idx cnt cmd} {
    set script [string map [
        list \${idx} [list $idx] \${cnt} [list $cnt] \${cmd} [list $cmd]] {
            for {set ${idx} 0} {[set ${idx}] < ${cnt}} {incr ${idx}} ${cmd}
    }]
    tailcall {*}$script
}

proc mloop2 {idx cnt cmd} {
    tailcall for [list set $idx 0] "\[set [list $idx]] < [list $cnt]" [list incr $idx] $cmd
}

proc something {} {
  mloop  {funny name} 5 {puts "hello [set {funny name}]"}
  mloop2 {funny name} 5 {puts "hello [set {funny name}]"}
}

Well the log use of macro you had above looks like an assert function, which I believe has been addressed by cleverness in the core that optimizes null functions out of existence.

I've used macros in a cpp-ish fashion, to eliminate big repeated blocks of code. Here's the macro function I wrote:

# procedure to create macros that operate in caller's frame, with arguments
# no default args yet
proc macro {name formal_args body} {
    proc $name $formal_args [subst -nocommands {
                # locally save all formal variables, and set them in parent conext
                foreach _v [list $formal_args] {
                        if {[uplevel 1 info exists \$_v]} {
                                set __shadow__\$_v [uplevel 1 set \$_v]
                        }
                        uplevel 1 set \$_v [set \$_v]
                }
                uplevel 1 {$body}
                # undo formal variables
                foreach _v [list $formal_args] {
                        if {[info exists __shadow__\$_v]} {
                                uplevel 1 set \$_v [set __shadow__\$_v]
                        } else {
                                uplevel 1 unset \$_v
                        }
                }
    }]
}

So you can do something like

set text "hello"
macro foo {a} {
    puts "$text $a"
}
foo world
foo everybody

output:

hello world
hello everybody

Of course this makes more sense when the body of the macro is 70 lines long and it's used in 8 different files, so it replaces a whole bunch of identical (except for a few bits) code with something a lot more readable.


RS has devised this very simple argument-less "micro-macro" instigated by Literate programming in a wiki. Beware that spaces in proc names, as implemented here, may at some time in the future be no more possible:

proc @ {name {body -}} {
    if {$body != {-}} {
                proc $name {} [list uplevel 1 $body]
    } else {uplevel 1 [list $name]}
}
# Macro definitions:
@ "Prepare input"  {set x 2}
@ "Produce result" {expr sqrt($x)}
# Macro testing:
@ "Prepare input" ;# -> 2
@ "Produce result" ;# -> 1.41421356237

Beware that spaces in proc names, as implemented here, may at some time in the future be no more possible - what do you mean?

Perhaps a reference to [L1 ] item 5?


jcw is more data oriented than procedural, and adds:

proc @ {name {body -}} {
    if {$body ne "-"} {
      set ::snippets($name) $body 
    } else {
      uplevel 1 $::snippets($name) 
    }
}

SS 2004-03-27: Sugar implements a macro facility very similar to what the original author of this page described (Lisp alike).


__LINE__ a la c would be nice?