Skip to content
VLSI Mentor

Tcl Part I · Module 11 · Scope and Namespaces

uplevel: Running a Script in Another Scope

upvar borrows one variable from another frame. uplevel borrows the whole frame: you hand it a script and it runs there, as though the caller had written those lines itself. That is how you write a command that behaves like a control structure — one that takes a body and runs it in the caller's context, the way foreach and if do. It is the most powerful thing in this module, it is how several of Tcl's own facilities are built, and it is the one command here that makes a script genuinely harder to read. All three of those deserve saying, so this chapter says all three.

Intermediate11 min readTclPart IScopeuplevelControl structures

Part I · Module 11 · Chapter 11.5 · uplevel: Running a Script in Another Scope

1. Running a script somewhere else

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc setit {} {
    uplevel 1 {set made_by_uplevel 42}
}

setit
puts "the caller's variable is now $made_by_uplevel"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the caller's variable is now 42

The set did not run in setit's frame. It ran in the caller's, which is why the variable exists out here afterwards.

The level argument works exactly as Chapter 11.4 §2 described: 1 is the caller (and the default), #0 is the global frame, and larger numbers count outward.

2. uplevel versus eval

The clearest way to see what uplevel does is beside eval, which runs a script in the current frame:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc via_eval {} {
    eval {set made 1}
    return "inside, made exists? [info exists made]"
}

proc via_uplevel {} {
    uplevel 1 {set made 1}
    return "inside, made exists? [info exists made]"
}

puts "eval    : [via_eval]"
puts "uplevel : [via_uplevel]"
puts "out here, made exists? [info exists made]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
eval    : inside, made exists? 1
uplevel : inside, made exists? 0
out here, made exists? 1

Read the middle line: after uplevel, the variable does not exist inside the procedure. It was created one frame up — which the last line confirms.

3. It concatenates its arguments

This is the trap, and it is worth meeting before you need it. uplevel joins everything after the level into one script:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc two_args {} {
    uplevel 1 set cc 7
}
two_args
puts "cc is $cc"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cc is 7

That worked — set, cc and 7 were concatenated into set cc 7. Now try it with a value containing a space:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc spaced {} {
    uplevel 1 set dd "two words"
}
spaced
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrong # args: should be "set varName ?newValue?"

The concatenation produced set dd two words — four words — because the quoting that made "two words" one argument to uplevel is gone by the time the script is assembled. Build the script with list and it survives:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc spaced_ok {} {
    uplevel 1 [list set ee "two words"]
}
spaced_ok
puts "ee is '$ee'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ee is 'two words'

4. Building something that behaves like a control structure

This is what uplevel is for. A command that takes a body and runs it in the caller's frame is indistinguishable from a built-in loop:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc repeat {n body} {
    for {set i 0} {$i < $n} {incr i} {
        uplevel 1 $body
    }
}

set hits 0
repeat 3 {
    incr hits
}
puts "hits = $hits"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hits = 3

incr hits worked on the caller's hits because the body ran in the caller's frame. Had repeat used eval, it would have incremented a local inside repeat three times and the caller's hits would still be 0.

Another small one — run a body only if a condition holds, with the condition evaluated in the caller's context too:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc unless {cond body} {
    if {![uplevel 1 [list expr $cond]]} {
        uplevel 1 $body
    }
}

set count 0
unless {$count > 0} {
    puts "count is not positive"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count is not positive

unless reads like a built-in at the call site, and it is an ordinary procedure.

5. uplevel returns the script's result

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc compute_there {} {
    return [uplevel 1 {expr {6 * 7}}]
}
puts [compute_there]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
42

Which means uplevel composes like anything else, and a wrapper can run something in the caller's frame and still hand back what it produced.

6. Why it makes code harder to follow

The registry asks for this explicitly, and it is fair. Three specific costs.

The reader cannot tell where a line runs. In repeat 3 { incr hits }, nothing at the call site says incr hits will touch the caller's variable rather than one inside repeat. You have to read repeat's source to know.

A name in the body can collide with a name in the caller. The body is written by one person and executed in someone else's frame:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc repeat {n body} {
    for {set i 0} {$i < $n} {incr i} {
        uplevel 1 $body
    }
}

set i "my own i"
repeat 2 {
    append collected "x"
}
puts "collected : $collected"
puts "my i      : $i"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
collected : xx
my i      : my own i

That one is fine because repeat's i is a local in repeat's own frame, not the caller's. But a body that itself sets i would be fighting for the caller's i, and nothing warns either party.

It defeats the guarantee from Chapter 11.1. The reason a Tcl procedure is safe to call is that it cannot touch your variables. A procedure using uplevel can touch all of them, and its signature does not say so.

7. Where you have already met it

Three places in this corpus used uplevel before this chapter explained it, which is worth connecting:

  • The lmap compatibility shim in Chapter 9.8 §7 runs a generated foreach in the caller's frame, so the loop variables land where the caller expects them.
  • The proc wrapper in Chapter 10.9 §7 is the same shape — a replacement command forwarding to the original.
  • Several verification harnesses in this module's own examples take a script and run it one level up.

All three are the same pattern: a command that accepts a script and must run it as though the caller had written it.

8. A worked example

A timing wrapper that runs a block in the caller's frame and reports how long it took:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc timed {label body} {
    set start [clock clicks -milliseconds]
    set result [uplevel 1 $body]
    set elapsed [expr {[clock clicks -milliseconds] - $start}]
    puts "  $label: finished in ${elapsed}ms (result '$result')"
    return $result
}

set total 0
timed "accumulate" {
    foreach v {1 2 3 4 5} { incr total $v }
    set total
}
puts "total is $total"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  accumulate: finished in 0ms (result '15')
total is 15

The body used the caller's total and its result came back through uplevel. The elapsed time will differ on your machine; everything else is fixed.

9. Exercises

Exercise 1. What is the difference between eval {set x 1} and uplevel 1 {set x 1} inside a procedure?

Exercise 2. Why does uplevel 1 set dd "two words" fail, and what are the two safe forms?

Exercise 3. Write twice that runs a body twice in the caller's frame, and show it incrementing a caller's counter.

Exercise 4. In repeat 3 {incr hits}, what would happen if repeat used eval instead of uplevel 1?

Exercise 5. Name the three reasons uplevel makes code harder to follow.

Exercise 6. When should you reach for upvar instead?

Answers

1. eval runs the script in the procedure's own frame, so x becomes a local that disappears on return. uplevel 1 runs it in the caller's frame, so x is created there and outlives the call.

2. Because uplevel concatenates everything after the level into one script, so the quoting is lost and it assembles set dd two words — four words, which set rejects. The safe forms are a braced literal script, uplevel 1 {set dd "two words"}, or a constructed one, uplevel 1 [list set dd $value].

3.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc twice {body} {
    uplevel 1 $body
    uplevel 1 $body
}

set n 0
twice { incr n }
puts $n
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2

4. The body would run in repeat's own frame, creating and incrementing a local hits three times. The caller's hits would be untouched — and because incr creates a missing variable, there would be no error, as Chapter 11.1 §7 showed.

5. The reader cannot tell from the call site which frame a line runs in; names in the body can collide with names in the caller, with no warning to either; and it removes the guarantee that calling a procedure cannot touch your variables.

6. When you need one specific variable rather than a whole script. upvar names what it touches in a single visible line, which uplevel cannot do — so it is the better choice for a counter, an accumulator, or an array.

10. Remember

  • uplevel ?level? script runs a script in another frame; level 1 is the caller and is the default, #0 is global.
  • eval runs in the current frame; uplevel 1 runs in the caller's. That is the whole distinction.
  • It concatenates its arguments, so quoting is lost — always pass one braced script or build it with list.
  • It returns the script's result, so it composes normally.
  • It is how you write a command that behaves like a control structure — one that takes a body and runs it in the caller's context.
  • With eval instead, a body would silently operate on locals inside your procedure, often with no error.
  • It makes code harder to follow: the call site does not say where lines run, body names can collide with caller names, and it breaks the usual guarantee that a procedure cannot touch your variables.
  • Prefer upvar for one specific variable, and plain arguments and return values for everything else.

Where this fits

Part of the Tcl curriculum.