ted
ted

Reputation: 4985

Redefining TCL foreach

I would like to redefine TCL's foreach, since multiple applications of foreach break some code.

Detailed problem description

The actual case where the code breaks happens in Vivado, a program using TCL for scripting. They supply an API that specifies get_<someObject> methods, which returns a list of objects (actually something that behaves like a list, but is implemented by xilinx to handle caching/printing a bit differently). To handle these lists I use foreach. The first time I use foreach on the list, I get <objects> in the loop variable.

However, if I apply foreach on a single object, the object is converted into a string representing its name. The problem is, that the API function get_property expects an object.

While applying foreach twice, does not seem to make sense, this might happen if you write two functions that take a list of objects and operate on them.

i.e.

proc a {obj} {
    puts "logging [llength $obj] objects:"
    foreach o $obj {
        puts "$o has column index [get_property COLUMN_INDEX $o]"
    }
}


proc b {obj} {
    foreach o $obj {
        a $o
        puts "working on $o"
        get_property COLUMN_INDEX $o
    }
}

If we now call these functions as follows

a [get_clock_regions X0Y0]   # ok (the list is turned into single objects in foreach)
b [get_clock_regions X0Y0]   # crashes inside a (the list from get_clock_regions is
                             # turned into objects by the foreach in b
                             # a is then called with a single object,
                             # the foreach now turns the object into a string
                             # representing the name, but get_property
                             # does not work on strings => error

I asked if the behaviour can be fixed in this question on the Xilinx Forums, however I am looking for a workaround for the meantime.

What I am trying to do

I would like to implement a workaround for the meantime. While I can do

a [list $o]

inside b, I think it would be nicer if I can redefine foreach to not break the above case. This way, I can simply drop my redefinition of foreach if Xilinx can fix the behaviour. (I am expecting foreach to work the same regardless if I have a list with one element or a single element, to my understanding this is the same in TCL).

My issues with redefining foreach are:

  1. How do I know if foreach is called as
    • foreach {varA varB} {valueList} {...}
    • foreach {varA varB} {valueList1 valueList2} {...}
  2. Is there a way to test if I have an object or a list containing one object? The Idea is two detect if it is just an object, if so wrap it into a list, which then can be unwrapped back to object by the normal foreach, however I have no clue how to detect this case.

Outline code I would like to write:

proc safeForeach {varnames valueLists body} {
    if { thereAreMultiple valueLists } {            # this issue no 1
        foreach valueList $valueLists {
            if {wouldDecayToString $valueLists} {   # this is issue no 2
                set valueLists [list $valueLists]
            }
        }
    } else {
        if {wouldDecayToString $valueLists} {       # this is issue no 2 again
            set valueLists [list $valueLists]
        }
    }
    #the next line should be wraped in `uplevel`
    foreach $varnames $valueLists $body
}

Upvotes: 1

Views: 558

Answers (2)

Donal Fellows
Donal Fellows

Reputation: 137787

Ultimately, the issue is that you don't want to treat simple values as lists. One of the ways of dealing with that is indeed to use [list $a] to make a list out of the value that shouldn't be mistreated, but another is to change the a procedure to take multiple arguments so that you can treat them as a list internally while having quoting automatically applied:

# The args argument variable is special
proc a {args} {
    puts "logging [llength $args] objects:"
    foreach o $args {
        puts "$o has column index [get_property COLUMN_INDEX $o]"
    }
}

Then you can call it like this:

a $o

When passing in a list to a procedure like this, you want to use expansion syntax:

a {*}[get_clock_regions X0Y0]

That leading {*} is a pseudo-operator in Tcl, which means to interpret the rest of the argument as a list and to pass the words in the list as their own arguments.

Upvotes: 2

andy mango
andy mango

Reputation: 1551

The root cause of the problem is (using your example) invoking proc a, which expects a list, with only a single scalar value when it is called in proc b. Your "workaround" of invoking a as, a [list $o], is the solution. It converts a single value into a list of one element. A list containing one element is not the same as a single value. Since lists in Tcl are just specially formatted strings, if the argument to proc a contains whitespace it will be treated as a list, split up into the whitespace separated components. Although Tcl is flexible enough to allow you to radically redefine the language, I think this is a case of "just because you can, doesn't mean you should." I just don't think this case is compelling enough since some code refactoring would make the problem disappear.

Upvotes: 3

Related Questions