#
# KMG (03 Sep 2005) - back-port of sugar 0.1 to Tcl8.3
#
# 1) Changed all ' eq ' to ' == '
# 2) Changed all ' ne ' to ' != '
# 3) Source lset.tcl and lindex.tcl to emulate the 8.4 versions of those commands
#
# Runs the standard example files shipped with sugar (except dictsugar.tcl which requires 8.5)
#
source [file join [file dirname [info script]] lset.tcl]
source [file join [file dirname [info script]] lindex.tcl]
# Sugar - a macro system for Tcl
# Copyright (C) 2004 Salvatore Sanfilippo
# Under the same license as Tcl version 8.4
### Changes
#
# 25Mar2004 - Added support for unique identifiers (sugar::uniqueName).
# 25Mar2004 - Now macros can specify a list of arguments instead of
# a unique argument that will receive a list. For old behaviour
# just use 'args'.
# 24Mar2004 - Modified the tailcal_proc transformer to use [foreach] for
# multiple assignments instead to create temp vars. Thanks
# to Richard Suchenwirth for the suggestion.
### TODO
#
# - better macro error reporting (line numbers).
# - call the macro system recursively for variable expansions?
# this allows to expand syntax that have to deal with
# variables interpolated inside other strings. (probably not).
# - Write a better macro for [switch] using sugar::scriptToList.
# - Write a macro that convert a subset of Tcl to critcl.
# - sugar::interleaveSpace should remove spaces before the first
# element of type TOK from the original parsed command.
# This is not needed for simple macro expansion because the
# sugar::expand function does this automatically, but it's needed
# when playing raw with the output of sugar::scriptToList.
# - Investigate on indentation changes with the tailrec macro
# (DONE: Fixed thanks to another related bug found by NEM).
# - An API to provide unique variable names for macro expansions.
package provide sugar 0.1
namespace eval sugar {}
namespace eval sugar::macro {}
namespace eval sugar::syntaxmacro {}
namespace eval sugar::transformermacro {}
# An incremental id used to create unique identifiers.
set sugar::unique_id 0
# This global variable contains the name of the procedure being
# expanded.
set sugar::currentprocedure {}
# Return the fully-qualified name of the current procedure.
proc sugar::currentProcName {} {
return $sugar::currentprocedure
}
# Return the "tail" of the current procedure name.
proc sugar::currentProcTail {} {
namespace tail $sugar::currentprocedure
}
# Return the namespace of the current procedure name.
proc sugar::currentProcNamespace {} {
namespace qualifiers $sugar::currentprocedure
}
# Return an unique identifier that macros can use as variable/proc names.
proc sugar::uniqueName {} {
set id [incr sugar::unique_id]
return __sugarUniqueName__$id
}
# Initialize the state of the interpreter.
# Currently this parser is mostly stateless, it only needs
# to save the type of the last returned token to know
# if something starting with '#' is a comment or not.
proc sugar::parserInitState statevar {
upvar $statevar state
set state [list EOL]
}
# The parser. It does not discard info about space and comments.
# The return value is the "type" of the token (EOF EOL SPACE TOKEN).
#
# It may be interesting to note that this is half of a simple
# Tcl interpreter. variable expansions is ignored, while command
# expansion is performed expanding macros if needed.
#
# The fact that it is still so simple, compared to what it can
# be in Python just to say one (much worst in Perl), it's an advice
# that to add syntax to Tcl is a bad idea.
proc sugar::parser {text tokenvar indexvar statevar {dosubst 0}} {
upvar $tokenvar token $indexvar i $statevar state
set token {}
set inside {}
set dontstop $dosubst
while 1 {
# skip spaces
while {!$dontstop && [string match "\[ \t\]" [string index $text $i]]} {
append token [string index $text $i]
incr i
}
# skip comments
if {$state == {EOL} && !$dontstop && [string equal [string index $text $i] #]} {
while {[string length [string index $text $i]] &&
![string match [string index $text $i] \n]} \
{
append token [string index $text $i]
incr i
}
}
# return a SPACE token if needed
if {[string length $token]} {return [set state SPACE]}
# check for special conditions
if {!$dontstop} {
switch -exact -- [string index $text $i] {
{} {return [set state EOF]}
{;} -
"\n" {
append token [string index $text $i]
incr i
return [set state EOL]
}
}
}
# main parser loop
while 1 {
switch -exact -- [string index $text $i] {
{} break
{ } -
"\t" -
"\n" -
";" {
if {!$dontstop} {
break;
}
}
\\ {
incr i
append token \\ [string index $text $i]
incr i
continue
}
\" {
if {[string equal $inside {}]} {
incr dontstop
set inside \"
append token \"
incr i
continue
} elseif {[string equal $inside \"]} {
incr dontstop -1
set inside {}
append token \"
incr i
continue
}
}
"\{" {
if {[string equal $inside {}]} {
incr dontstop
set inside "\{"
append token "\{"
incr i
continue
} elseif {[string equal $inside "\{"]} {
incr dontstop
}
}
"\}" {
if {[string equal $inside "\{"]} {
incr dontstop -1
if {$dontstop == 0} {
set inside {}
append token "\}"
incr i
continue
}
}
}
\$ {
if {![string equal $inside "\{"]} {
if {![string equal [string index $text [expr {$i+1}]] $]} {
set res [LctSubstVar $text i]
append token "$$res"
continue
}
}
}
\[ {
if {![string equal $inside "\{"]} {
set res [LctSubstCmd $text i]
append token "\[$res\]"
continue
}
}
}
append token [string index $text $i]
incr i
}
return [set state TOK]
}
}
# Actually does not really substitute commands, but
# exapands macros inside.
proc LctSubstCmd {text indexvar} {
upvar $indexvar i
set go 1
set cmd {}
incr i
while {$go} {
switch -exact -- [string index $text $i] {
{} break
\[ {incr go}
\] {incr go -1}
}
append cmd [string index $text $i]
incr i
}
set cmd [string range $cmd 0 end-1]
return [::sugar::expand $cmd]
}
# Get the control when a '$' (not followed by $) is encountered,
# extract the name of the variable, and return it.
proc LctSubstVar {text indexvar} {
upvar $indexvar i
set dontstop 0
set varname {}
incr i
while {1} {
switch -exact -- [string index $text $i] {
\[ -
\] -
"\t" -
"\n" -
"\"" -
\; -
\{ -
\} -
\$ -
( -
) -
{ } -
"\\" -
{} {
if {!$dontstop} {
break
}
}
( {incr dontstop}
) {incr dontstop -1}
default {
append varname [string index $text $i]
}
}
incr i
}
return $varname
}
# Return the number of lines in a string
proc countlines {string} {
llength [split $string "\n"]
}
# interleave SPACE and EOL tokens in a Tcl list $tokens
# representing a command. Also every token is
# converted to the two-elements list representation
# with type TOK.
#
# The $origargv list is the output of the parser
# for that command, and is used by interleaveSpaces
# to make the indentation of the expanded macro as
# similar as possible to what the used typed in the source
# code.
proc sugar::interleaveSpaces {tokens origargv} {
set newargv {}
for {set j 0} {$j < [llength $tokens]} {incr j} {
lappend newargv [list TOK [lindex $tokens $j]]
set idx [::sugar::indexbytype $origargv SPACE $j]
if {$idx == -1} {
lappend newargv [list SPACE " "]
} else {
# If possible, try to use the same argument
# separator as the user typed it.
lappend newargv [lindex $origargv $idx]
}
}
# Use the same EOL string. That's always possible
if {![llength $newargv]} {
set newargv [list ";"]
}
lset newargv end [lindex $origargv end]
return $newargv
}
# Transform a script to a list of lists, where every list is
# a command, and every element of the list is an argument,
# and is itself a two elements of list. The first element
# is the token type, the second the toke value. The following
# toke types are defined.
#
# SPACE - Spaces, non significative for the execution, just separate arguments.
# TOK - Any significative token. The first element of type TOK is
# the command name.
# EOL - End of line.
#
# This function is intended to be used directly or indirectly by macro,
# that will do the processing, and then call listToScript to convert
# it back in script.
#
# Macros may want to call sugar::tokens for every command to work
# more comfortably with it, and than reconvert to the
# original format with sugar::interleaveSpaces.
#
# ----------------------------------------------------------------------
# In theory sugar::expand should be modified to directly use this
# instead of a local copy of almost the same code. They are actually
# a bit different because sugar::expand does the processing for every
# command, not in the entire script at once.
proc sugar::scriptToList script {
set i 0
set result {}
::sugar::parserInitState parserState
set eof 0
while 1 {
set command {}
while 1 {
set type [::sugar::parser $script token i parserState]
switch $type {
EOF {lappend command [list EOL {}]; set eof 1; break}
default {
lappend command [list $type $token]
if {$type == {EOL}} break
}
}
}
lappend result $command
if {$eof} break
}
return $result
}
# That's really trivial ;)
# The macro specification should guarantee that the list
# is transformed into the source code by simple concatenation
# of all the tokens.
proc sugar::listToScript list {
set result {}
foreach c $list {
foreach t $c {
append result [lindex $t 1]
}
}
return $result
}
# Return true if the named macro exists, and store in macroName var
# the fully qualified name of the procedure in charge to do expansion for it.
proc sugar::lookupMacro {macroname procnameVar} {
upvar 1 $procnameVar procname
if {[catch {info args ::sugar::macro::__macroproc__$macroname}]} {
return 0
}
set procname ::sugar::macro::__macroproc__$macroname
return 1
}
# Macro expansion. It trys to take indentation unmodified.
proc sugar::expand script {
while 1 {
set eof 0
set i 0
set result {}
::sugar::parserInitState parserState
while {!$eof} {
set argv {}
set argc 0
# Collect a command in $argv. Every token is a two-elements
# List with the token type and value, as returned by expr.
# Significative tokens are interleaved with space tokens:
# syntax macros will have a way to know how arguments where
# separated.
while 1 {
set type [::sugar::parser $script token i parserState]
if {[string equal $type EOF]} {
set eof 1
}
switch $type {
EOF {lappend argv [list EOL {}]; break}
default {
if {$type == {SPACE} && $argc == 0} {
append result $token
} else {
lappend argv [list $type $token]
incr argc
if {$type == {EOL}} break
}
}
}
}
# Call macros for this statement
if {[lindex $argv 0 0] != {EOL}} {
# Check if there is a macro defined with that name
set cmdname [lindex $argv 0 1]
# Call the macro associated with that command name, if any.
if {[sugar::lookupMacro $cmdname expander]} {
#puts "executing macro for $cmdname in procedure [::sugar::currentProcName]"
if {[catch {set tokens [eval $expander [::sugar::tokens $argv]]} errstr]} {
error "Macro '$cmdname' expansion error in procedure '$::sugar::currentprocedure': $errstr" $::errorInfo
}
set argv [::sugar::interleaveSpaces $tokens $argv]
}
# Call all the syntax macros. For now in random order.
foreach syntaxmacro [info command ::sugar::syntaxmacro::__macroproc__*] {
set argv [::sugar::interleaveSpaces [eval $syntaxmacro [::sugar::tokens $argv]] $argv]
}
}
foreach arg $argv {
append result "[lindex $arg 1]"
}
}
# Call all the transformer macros. For now in random order.
# TODO: consider if it's better to move this as first
# transformation.
foreach trmacro [info command ::sugar::transformermacro::__macroproc__*] {
set list [::sugar::scriptToList $result]
set list [$trmacro $list]
set result [::sugar::listToScript $list]
}
# Reiterate if needed, otherwise exit.
if {[string equal $script $result]} break
#puts "AFTER: '$script'"
#puts "BEFORE: '$result'"
#puts "---"
set script $result
}
return $result
}
# Return the index of the $num-Th element of type $type in a list
# of tokens.
proc ::sugar::indexbytype {argv type num} {
set idx 0
foreach a $argv {
foreach {t _} $a break
if {$type == $t} {
if {!$num} {
return $idx
}
incr num -1
}
incr idx
}
return -1
}
# Wrapper for [proc] that expands macro in the body
# TODO: add a switch -nomacro to avoid macro expansion
# for the given procedure.
proc sugar::proc {name arglist body} {
# Get the fully qualified name of the proc
set ns [uplevel [list namespace current]]
# If the proc call did not happen at the global context and it did not
# have an absolute namespace qualifier, we have to prepend the current
# namespace to the command name
if { ![string equal $ns "::"] } {
if { ![string match "::*" $name] } {
set name "${ns}::${name}"
}
}
if { ![string match "::*" $name] } {
set name "::$name"
}
set oldprocedure $::sugar::currentprocedure
set ::sugar::currentprocedure $name
# puts "+ $name"
set body [::sugar::expand $body]
# Call the real [proc] command.
uplevel 1 [list ::proc $name $arglist $body]
set ::sugar::currentprocedure $oldprocedure
return
}
# Number of tokens of type TOK. Useful for arity checking in macros.
proc sugar::tokensnum argv {
set c 0
foreach a $argv {
if {[lindex $a 0] == {TOK}} {
incr c
}
}
return $c
}
# Return values of all the tokens of type TOK as a list.
proc sugar::tokens argv {
set tokens {}
foreach a $argv {
if {[lindex $a 0] == {TOK}} {
lappend tokens [lindex $a 1]
}
}
return $tokens
}
# Define a new macro
proc sugar::macro {names arglist body} {
foreach name $names {
uplevel 1 [list ::proc ::sugar::macro::__macroproc__$name $arglist $body]
}
}
# Define a new syntax macro
proc sugar::syntaxmacro {name arglist body} {
uplevel 1 [list ::proc ::sugar::syntaxmacro::__macroproc__$name $arglist $body]
}
# Define a new transformer macro
proc sugar::transformermacro {name arglist body} {
uplevel 1 [list ::proc ::sugar::transformermacro::__macroproc__$name $arglist $body]
}
# That's used to create macros that expands arguments that are
# scripts. This kind of macros are used for [while], [for], [if],
# and so on.
proc sugar::expandScriptToken tok {
set t [lindex $tok 0]
set res [::sugar::expand $t]
if {[string equal $t $res]} {
return $tok
} else {
list $res
}
}
# Macro substitution. Like [subst] but for macros.
proc sugar::dosubst string {
sugar::parserInitState state
set idx 0
sugar::parser $string result idx state 1
return $result
}
# Expand Expr's expressions. Try to don't mess with quoting.
proc sugar::expandExprToken tok {
set quoted 0
if {[string index $tok 0] == "\{" && [string index $tok end] == "\}"} {
set quoted 1
set tok [string range $tok 1 end-1]
}
set tok [sugar::dosubst $tok]
if {$quoted} {
set tok "{$tok}"
}
return $tok
}
# Get the N-th element with type $type from the list of tokens.
proc sugar::gettoken {argv type n} {
set idx [::sugar::indexbytype $argv $type $n]
if {$idx == -1} {
error "bad index for gettoken (wrong number of args for macro?)"
}
lindex $argv $idx 1
}
# Set the N-th $type element in the list of tokens to the new $value.
proc sugar::settoken {argvVar type n value} {
upvar $argvVar argv
set idx [::sugar::indexbytype $argv $type $n]
if {$idx == -1} {
error "bad index for gettoken (wrong number of args for macro?)"
}
lset argv $idx 1 $value
}
################################################################################
# Macros to allow macros inside conditionals, loops and other Tcl commands
# that accept scripts or [expr] expressions as arguments.
################################################################################
sugar::macro while args {
lset args 1 [sugar::expandExprToken [lindex $args 1]]
lset args 2 [sugar::expandScriptToken [lindex $args 2]]
}
sugar::macro foreach args {
lset args end [sugar::expandScriptToken [lindex $args end]]
}
sugar::macro time args {
lset args 1 [sugar::expandScriptToken [lindex $args 1]]
}
sugar::macro if args {
lappend newargs [lindex $args 0]
lappend newargs [sugar::expandExprToken [lindex $args 1]]
set args [lrange $args 2 end]
foreach a $args {
switch -- $a {
else - elseif {
lappend newargs $a
}
default {
lappend newargs [sugar::expandScriptToken $a]
}
}
}
return $newargs
}
sugar::macro for args {
lset args 1 [sugar::expandScriptToken [lindex $args 1]]
lset args 3 [sugar::expandScriptToken [lindex $args 3]]
lset args 4 [sugar::expandScriptToken [lindex $args 4]]
return $args
}
# That's still not perfect because messes with indentation.
# Should use new scriptToList API to do it better.
sugar::macro switch args {
lappend result [lindex $args 0]
set idx 0
set isquoted 0
while 1 {
incr idx
set arg [lindex $args $idx]
if {$arg == {--}} {
lappend result $arg
incr idx
break
}
if {[string index $arg 0] != {-}} break
lappend result $arg
}
lappend result [lindex $args $idx]
incr idx
# Handle the two forms in two different ways
if {[llength $args]-$idx == 1} {
set l [lindex $args $idx 0]
set isquoted 1
} else {
set l [lrange $args $idx end]
}
# Expand scripts inside
set temp {}
foreach {pattern body} $l {
if {$body != {-}} {
if {$isquoted} {
set body [lindex [sugar::expandScriptToken [list $body]] 0]
} else {
set body [sugar::expandScriptToken $body]
}
}
lappend temp $pattern $body
}
# Requote it if needed.
if {$isquoted} {
return [concat $result [list [list $temp]]]
} else {
return [concat $result $temp]
}
}
################################################################################
# Transformers included in sugar
################################################################################
################ a macro for tail recursion ##############
# TODO: give a name to this kind of macros, and maybe provide
# a function to 'encapsulate' the common part of this
# kind of macros involving the redefinition of proc.
proc sugar::tailrecproc {name arglist body} {
# Convert the script into a Tcl list
set l [sugar::scriptToList $body]
# Convert tail calls
set l [sugar::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 sugar::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] == {SPACE}} {
set space [lindex $cmd 0 1]
} else {
set space " "
}
if {$cmdname == $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 == {if}} {
#puts "IF CALL"
for {set j 0} {$j < [llength $cmd]} {incr j} {
if {[lindex $cmd $j 0] != {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 [sugar::tailrec_convert_calls $name $arglist $scriptcode]
lset code $lastidx $j 1 [list [sugar::listToScript $converted]]
}
}
}
}
return $code
}See Also: sugar, Snit under Tcl/Tk 8.3

