Skip to content
VLSI Mentor

Tcl Part I · Module 4 · Operators and Expressions

Logical Operators and Short-Circuit Evaluation

Three operators combine conditions, and they come with a behaviour that is easy to describe and easy to underestimate. When the left-hand side of an and already settles the answer, the right-hand side is never evaluated at all. That is usually introduced as a performance detail. It is not — it is what allows one test to protect another, so that a check which would fail on missing data simply never runs. This chapter proves the behaviour with a side effect you can see, then builds the guard pattern that depends on it.

Foundation12 min readTclPart IOperatorsLogicGuards

Part I · Module 4 · Chapter 4.5 · Logical Operators and Short-Circuit Evaluation

1. The Three Operators

Azvya Education Pvt. Ltd.VLSI Mentor
basic.tcl
puts "1 && 1 = [expr {1 && 1}]"
puts "1 && 0 = [expr {1 && 0}]"
puts "0 || 1 = [expr {0 || 1}]"
puts "0 || 0 = [expr {0 || 0}]"
puts "!0     = [expr {!0}]"
puts "!5     = [expr {!5}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 && 1 = 1
1 && 0 = 0
0 || 1 = 1
0 || 0 = 0
!0     = 1
!5     = 0

&& is and, || is or, ! is not. Their operands are read as truth values, which from Chapter 3.6 means anything on the accepted list — and anything else is an error, not false.

2. Combining Comparisons

The normal use:

Azvya Education Pvt. Ltd.VLSI Mentor
combining.tcl
set count 5
set limit 10
set mode "fast"

puts "in range:      [expr {$count > 0 && $count < $limit}]"
puts "either:        [expr {$count > 100 || $mode eq "fast"}]"
puts "not fast:      [expr {!($mode eq "fast")}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
in range:      1
either:        1
not fast:      0

Comparisons bind tighter than && and || — Chapter 4.2 — so $count > 0 && $count < $limit groups the way it reads.

The third line is better written as $mode ne "fast". Negating a comparison with ! is usually a sign there is a direct operator for what you meant.

3. The Rule That Matters

&& stops as soon as the left side is false. || stops as soon as the left side is true.

If the answer is already settled, the right-hand side is never evaluated.

Two parallel left-to-right flows for the and operator. When the left operand is false the answer is already settled, so the right operand is skipped and the result is zero. When the left operand is true the right operand is evaluated and its truth value becomes the result.Left is false0 && anythingAlready settledright side skippedResult: 0nothing else ranLeft is true1 && somethingNot settledright side evaluatedResultthe right side decides12
Figure 1 — what && does with each of its two cases. A false left operand settles the answer immediately, so the right operand is never evaluated at all. A true left operand does not settle it, so the right operand runs and decides the result. This asymmetry is what lets the left operand protect the right.

|| is the mirror image: a true left operand settles it, and the right side is skipped.

4. Proving It

You cannot show this with return values alone, because 0 && x is 0 whether or not x ran. You need a right-hand side that leaves a trace:

Azvya Education Pvt. Ltd.VLSI Mentor
proof.tcl
proc noisy {} {
    puts "    (the right side ran)"
    return 1
}

puts "0 && noisy  ->  [expr {0 && [noisy]}]"
puts "1 && noisy  ->  [expr {1 && [noisy]}]"
puts "1 || noisy  ->  [expr {1 || [noisy]}]"
puts "0 || noisy  ->  [expr {0 || [noisy]}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0 && noisy  ->  0
1 && noisy  ->      (the right side ran)
1
1 || noisy  ->  1
0 || noisy  ->      (the right side ran)
1

Two of the four printed the message and two did not.

  • 0 && … — false already, right side skipped.
  • 1 && … — not settled, right side ran.
  • 1 || … — true already, right side skipped.
  • 0 || … — not settled, right side ran.

The message appears before the result because the inner command runs first — Chapter 2.5.

5. The Payoff: One Test Protecting Another

This is why the rule matters, and it is a correctness matter rather than a speed one.

Azvya Education Pvt. Ltd.VLSI Mentor
guard.tcl
set f "nope.txt"

puts "guarded: [expr {[file exists $f] && [file size $f] > 0}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
guarded: 0

file size on a file that does not exist raises an error. It was never called, because file exists returned false and settled the answer.

Reverse the order and the protection is gone:

Azvya Education Pvt. Ltd.VLSI Mentor
unguarded.tcl
set f "nope.txt"

puts [catch {expr {[file size $f] > 0 && [file exists $f]}} m]
puts $m
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1
could not read "nope.txt": no such file or directory

Same two tests, same operator, opposite order — and now it fails.

6. What They Return

A detail that catches people arriving from Python:

Azvya Education Pvt. Ltd.VLSI Mentor
returns.tcl
puts "5 && 3 = [expr {5 && 3}]"
puts "0 || 7 = [expr {0 || 7}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
5 && 3 = 1
0 || 7 = 1

Tcl returns 1 or 0, never the operand. In Python, 5 and 3 is 3 and 0 or 7 is 7, which is why the a or default idiom works there.

That idiom does not exist in Tcl. For a default value, use Chapter 3.4's info exists pattern or the conditional operator in Chapter 4.7.

7. Chaining and Grouping

&& binds tighter than ||:

Azvya Education Pvt. Ltd.VLSI Mentor
grouping.tcl
puts "1 || 0 && 0   = [expr {1 || 0 && 0}]"
puts "(1 || 0) && 0 = [expr {(1 || 0) && 0}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 || 0 && 0   = 1
(1 || 0) && 0 = 0

Different answers. The first is 1 || (0 && 0).

Per Chapter 4.2 §6: when && and || appear in the same expression, parenthesise. Nobody should have to recall the order to review your condition.

8. Common Mistakes

Putting the expensive or risky test first. §5. Order matters for correctness.

Expecting && to return the operand. §6.

Mixing && and || without parentheses. §7.

Using ! where a direct operator exists. !($a eq $b) is $a ne $b.

Assuming a non-boolean right side is safe because it is skipped. It is safe only when it is actually skipped — which depends on the left side at run time, not on what you expect.

Relying on a side effect in the right operand. If it must happen, do not put it in a condition.

9. Exercises

Exercise 1 — the truth tables. Produce all four combinations for && and for ||, and both cases for !.

Exercise 2 — see the skip. Write the §4 noisy proof yourself. Then add a fifth line, expr {0 && [noisy] && [noisy]}, and predict how many messages appear before running it.

Exercise 3 — build a guard. Write one condition that is true only when a variable limit exists and holds a number greater than zero. Test it with limit unset, set to abc, set to 0, and set to 5. It must not error in any case.

Exercise 4 — break it by reordering. Take your Exercise 3 answer and move the existence test to the end. Which inputs now fail, and why?

Exercise 5 — the division guard. Write a condition that safely tests whether $total / $count exceeds 10 when count might be zero. Confirm it does not error for count equal to 0.

Exercise 6 — the missing idiom. A colleague writes set name [expr {$input || "default"}] expecting Python behaviour. Explain what Tcl actually produces, then write the version that works.

10. Remember

  • &&, || and ! combine truth values, and their operands must be valid truth values.
  • && stops when the left side is false; || stops when the left side is true. The right side is then never evaluated.
  • That is a correctness feature. It lets one test protect another.
  • Order conditions cheapest and most protective first — [info exists x] && $x > 0, not the reverse.
  • They return 1 or 0, never the operand. The Python a or default idiom does not exist here.
  • && binds tighter than ||. Parenthesise when both appear.
  • Prefer a direct operator to negation: ne over !(... eq ...).

Next: the operators that work on the bits inside a number.

Where this fits

Part of the Tcl curriculum.