Version 13 of procstep

Updated 2015-11-15 10:02:14 by pooryorick

PYK 2015-11-08: procstep is a drop-in replacement for proc that evaluates one command of a procedure body at at time.

Description

procstep provides one hook to inspect and manipulate the command before it is evaluated, and another to react to return codes. It requires scriptSplit and nocomments. These commands are also available as ycl proc tcl step.

::tcl::proc proc {name args body} {
    set newbody {}
    ::foreach command [nocomments [scriptSplit $body]] {
        ::append newbody [string map [list {${command}} [list $command]] {
            set ::tcl::ccode [
                ::catch {if 1 [::cmdhandler [namespace current] \
                     ${command}]} ::tcl::cres ::tcl::copts]
              ::if {$::tcl::ccode} {
                  if 1 [
                      ::errorhandler [namespace current] \
                         $::tcl::ccode $::tcl::cres $::tcl::copts]
              } else {
                  ::lindex $::tcl::cres
              }
        }]
    }
    ::uplevel [::list ::tcl::proc $name $args $newbody]
}

Here is a variant that improves runtime performance quite a bit by rewriting each command of the body when the procedure is created. This leaves no private space for catch to create its output variables in, so it stuffs them into $::tcl::cres and ::tcl::copts instead instead:

::tcl::proc proc {name args body} {
    set newbody {}
    ::foreach command [nocomments [scriptSplit $body]] {
        ::append newbody [string map [list {${command}} [list $command]] {
            set ::tcl::ccode [
                ::catch {if 1 {{*}[::cmdhandler [namespace current] \
                     ${command}]}} ::tcl::cres ::tcl::copts]
              ::if {$::tcl::ccode} {
                  if 1 [
                      ::errorhandler [namespace current] \
                         $::tcl::ccode $::tcl::cres $::tcl::copts]
              } else {
                  ::lindex $::tcl::cres
              }
        }]
    }
    ::uplevel [::list ::tcl::proc $name $args $newbody]
}

Here are some example handlers that don't do much of anything:

::tcl::proc cmdhandler {namespace command} {
   return $command
}

::tcl::proc errorhandler {namespace code cres copts}  {
    if {$code == 2} {
        tailcall return {*}$copts $cres
    }
    tailcall return {*}$copts -code $code $cres
}

Here is a monitor that disallows lindex:

::tcl::proc cmdhandler {namespace command} {
    if {[namespace eval $namespace [
        list namespace which [lindex $command 0]]] eq {::lindex}} {
        return {error "lindex not allowed"}
    }
    ::puts stderr [::list {now executing} $command]
    return $command
}

This monitor doesn't catch commands in command substitutions, so it wouldn't catch something like list lindex .... ycl proc step, however, does.

ycl proc step

ycl proc step is a more fully-featured variant of stepproc. It could be used as the engine for the Sugar macro system, with the difference that it sould behave in a more LISPY way, expanding each command macro at the beginning of the command, rather that all at once when the procedured is defined. This defeats one of the stated objects of Sugar, namely performance, but brings the advantage of being more dynamic, allowing macros to consider the current state of execution. In contrast, ycl proc step would not be useful as an engine to drive knit because in knit macro procedures are explicitly defined and called directly as commands.

A macro system build on ycl proc step would, like Sugar, want to provide an interface for integrating it with commands that evaluate some of their arguments as scripts.

Another potential application for ycl proc step would be a condition system like that of LISP that allowed restarts. This was actually the motivation for ycl proc step. An interesting feature of ycl proc step is that it would allow restarts even in command substitutions.

I think that a tailcall for Tcl versions prior to 8.6 could also be written usiing ycl proc step. to be robust, though, all commands that evaluate some of their arguments as scripts or expressions would have to be wrapped.