Version 1 of TclImplForNamedArguments

Updated 2017-02-23 16:27:24 by avl

This is a pure Tcl reference implementation for a variant of TIP 457.

As of now, it doesn't yet handle the specified -upvar <outNameVar> option. (may follow within a few days).

procx creates a procedure, but the procedure isn't yet meant to be called directly. By not modifying this stub procedure at every single call, it even allows the original body to benefit from any kinds of compilations that today's or future Tcl has to offer.

The second procedure needs to be used as a call-wrapper. A proper implementation in C wouldn't have this limitation. It would have been possible to create the defined procedure with a different name, and alias the wrapper onto the target procedure's name, but I kept it simpler for now.

Example usage:

procx foo {a {b 0} {c -default 0} {d -default 0 -name d -value 1} {e 0}} { list $a $b $c $d $e }

call foo A
call foo A B
call foo A B C
call foo A B C -d
call foo A B C E


procx process {
   {loglvl -default 3
           -name lvl
           -name q -value 0
           -name v -value 9 }
   {type -default "snafu"
           -name foo -value foo
           -name bar -value bar}
   args
} { list lvl $loglvl type $type args $args }

call process la la la
call process -foo
call process -lvl 42
call process -v
call process -q -- -bar goes in args


procx testrep {{opt -default 42 -name -o} val} { tcl::unsupported::representation $val }

call testrep [expr {-42}]
call testrep -- [expr {-42}]
call testrep -o 0 [expr {-42}]
call testrep -o 0 -- [expr {-42}]

Implementation:

proc procx {name params body} {
   upvar #0 tcl::proc_cache($name) cache; set cache {}; set allnames {}
   set group_args  [dict create]; # argnames with 1st option group
   set group_names [dict create]; # names with their specific options
   
   foreach argspec $params {
      # canonicalize argspec:
      if {[llength $argspec] % 2 == 0} {
         set argspec [linsert $argspec 1 "-default"]
      }
      set argspec [lassign $argspec argname]; # extract argname
      lappend allnames $argname; # add to internal list
      # initialize container for option groups. 1st group has empty key:
      set optiongroups [dict create {} {}]; set curname {}
      # sort out options into option groups:
      foreach {o v} $argspec {
         if {$o ne "-name"} {
            dict lappend optiongroups $curname $o $v
         } else {
            dict set optiongroups "-$v" [dict create {} $argname]
            set curname "-$v"
         }
      }
      set firstgroup [dict get $optiongroups {}]; # pick 1st group
      dict unset optiongroups {}; # remove the 1st group from dict
      # distinguish positional versus named arguments:
      if {[dict size $optiongroups] > 0} {;# named argument
         dict set group_args $argname $firstgroup
         # add the argname to each option group:
         dict for {n grp} $optiongroups {
            # TODO: check that $n didn't already exist in group_names
            dict set group_names $n [dict get $optiongroups $n]
         }
      } else {;# positional argument
         # finish group of named arguments, if any.
         if {[dict size $group_names]>0} {;# flush
            lappend cache $group_args $group_names
            set group_args [dict create]
            set group_names [dict create]
         }
         # add this positional argument to the cache
         lappend cache [list $argname $firstgroup] [dict create]
      }
   }
   # finish group of named arguments, if any.
   if {[dict size $group_names]>0} {;# flush
      lappend cache $group_args $group_names
   }
   # debug:
   # puts $allnames
   # puts $cache
   uplevel 1 [list proc $name $allnames $body]
}
proc call {name args} {
   set argnr 0; set allnames {}; set formargs [dict create]
   foreach {argopts nameopts} $tcl::proc_cache($name) {
      dict for {n _} $argopts { lappend allnames $n }; set done 0
      set arg [lindex $args $argnr]; set isnamed [dict size $nameopts]

      while {
         $argnr < [llength $args] &&
         ( !$isnamed || ( $arg ne "--" && [dict exists $nameopts $arg] ) )
      } {
         if {$isnamed} {; # assign value to named argument
            set aname [dict get $nameopts $arg {}]
            #puts "arg: $arg -- aname: $aname"
            if {[dict exists $nameopts $arg "-value"]} {
               dict set formargs $aname [dict get $nameopts $arg "-value"]
            } elseif {$argnr+1 < [llength $args]} {
               dict set formargs $aname [lindex $args [incr argnr]]
            } else {
               error "no value given to $aname"
            }; incr argnr; set arg [lindex $args $argnr]
         } else {
            set argname [lindex $argopts 0]
            if {$argname eq "args"} {
               dict set formargs "args" [lrange $args $argnr end]
            } else {
               dict set formargs $argname $arg
            }
            incr argnr; set arg [lindex $args $argnr]
            set done 1; break; # essentially break while + continue foreach
         }
      }
      if {!$done} {
         # no more args for this group or positional argument
         #puts "arg: >$arg< argnr: $argnr -- $argopts"
         if {$isnamed && $arg eq "--"} { incr argnr }
         dict for {n opts} $argopts {; # check if all have value or default
            if {![dict exists $formargs $n]} {
               #puts "checking $n in $opts"
               if {$n eq "args"} {
                  dict set formargs "args" {}
               } elseif {[dict exists $opts "-default"]} {
                  dict set formargs $n [dict get $opts "-default"]
               } else { 
                  #puts "formargs: [dict get $formargs]"
                  error "formal argument $n has not been given a value"
               }
            }
         }
      }
   }
   if {[lindex $allnames end] eq "args"} {
      set allnames [lrange $allnames 0 end-1]
      set aaa [dict get $formargs "args"]
   } else { set aaa {} }
   tailcall $name {*}[lmap x $allnames {dict get $formargs $x}] {*}$aaa
}