NEM 2008-05-13: Recent conversations on the tcl-core mailing list, relating to Cloverfield and L have pointed out an interest in having a version of upvar that is simpler to use, clearer to read, and that captures errors early. (This wasn't the only focus of the conversation, but a side-issue). This is quite possible today, using a slight variation on the code at implicit upvar. The main construct in the package provided here is a checked proc that does implicit upvar on any parameters named in the form "&foo". When called, these procs also check whether the supplied variable name actually exists in the caller's scope, and if not, raise an intelligible error message. The package also provides a checked version of upvar itself, and a new construct called uplink which is used mainly in the implementation of the checked proc -- it is essentially a version of upvar whose arguments are more convenient when generating code. After this, it is trivial to define wrappers around various core built-ins, such as incr, lappend etc so that they raise errors if the variable supplied does not exist, rather than auto-creating it. This package could therefore be useful when quick detection and diagnosis of errors is more important than the full flexibility and convenience provided by various built-ins. The implementation is not the most efficient possible, as I opted for a clear implementation over a fast one (you could speed it up by inlining more of the code, but it's messier). It could also be back-ported to 8.4 with only a few changes.
# check.tcl --
#
# Provides "checked" versions of some Tcl constructs which catch errors
# earlier.
#
package require Tcl 8.5
package provide checked 0.1
namespace eval checked {
namespace export proc upvar uplink
# proc name params body --
#
# Works like the built-in proc command, except that it allows implicit
# upvar. Any parameter named like &foo will be treated as a variable
# reference and an implicit [upvar] inserted so that the local
# variable 'foo' will be linked to the variable whose name was passed
# in that argument. In addition, this upvar checks at call-time
# whether the argument passed refers to an existing variable or not,
# and throws an error if not. For instance, a version of [incr] that
# complains when the variable doesn't exist (as in Tcl 8.4) can be
# written simply as:
#
# checked proc myincr {&var {amount 1}} { incr var $amount }
#
::proc proc {name params body} {
set vars [list]
foreach p $params {
set p [lindex $p 0]
if {[string match &* $p]} {
::lappend vars $p [string range $p 1 end]
}
}
set body [list checked uplink 1 $vars $body]
uplevel 1 [list ::proc $name $params $body]
}
# uplink level vars body --
#
# This command is mostly a convenience proc for use in the [checked
# proc] implementation. The $vars argument should be a dictionary of
# parameter -> local var names. When called, it will examine the
# arguments passed to each specified parameter and then link a
# variable of that name from the caller's scope to the local var name
# specified. For instance:
#
# proc myincr {varName {amount 1}} {
# checked uplink 1 {varName var} {
# incr var $amount
# }
# }
#
::proc uplink {level vars body} {
set cmd [list checked upvar $level]
if {[string is integer -strict $level]} { ::incr level }
foreach {param local} $vars {
::upvar 1 $param p
if {![uplevel $level [list info exists $p]]} {
return -code error "no such variable \"$p\""
} else {
::lappend cmd $p $local
}
}
uplevel 1 $cmd
uplevel 1 $body
}
# upvar level ?otherVar localVar ...? --
#
#
# A replacement for the built-in [upvar] command, except that it
# complains immediately if any of the otherVars do not exist already
# at the given level. Also the $level argument is mandatory, and you
# can specify no variable links (as a convenience).
#
::proc upvar {level args} {
set cmd [list ::upvar $level]
if {[string is integer -strict $level]} { ::incr level }
foreach {varName local} $args {
if {![uplevel $level [list info exists $varName]]} {
return -code error "no such variable \"$p\""
} else {
::lappend cmd $varName $local
}
}
uplevel 1 $cmd
}
# Redefined various standard commands to be checked
foreach cmd {incr lappend append} {
# Add more as needed
namespace export $cmd
checked proc $cmd {&var args} "::$cmd var {*}\$args"
}
namespace ensemble create
}