Control Flow, Scope and defer
Programs choose and repeat. tiko's constructs for both will look familiar, and the reason they
get a chapter is the third thing here: defer, which arranges for something to happen on the way
out of a block — and which has one rule that catches everybody once.
Choosing
'' branching.tiko -- if, and select case for more than two ways.
dim as int32 score = 72
if score >= 90 then
print "excellent"
elseif score >= 60 then
print "a pass"
else
print "not yet"
end if
'' A whole if on one line needs no 'end if'.
if score > 0 then print "there is a score"
'' select case compares one value against ranges, lists and open tests.
select case score
case 90 to 100
print "band: top"
case 60 to 89
print "band: middle"
case 0, 1, 2
print "band: barely started"
case is < 0
print "band: impossible"
case else
print "band: low"
end select
a pass
there is a score
band: middle
if / elseif / else / end if needs no parentheses around the condition. A complete if can
sit on one line, and then it takes no end if — useful for a guard, and confusing if you try to
extend it later, so keep those short.
select case compares one value against many possibilities, and the cases are more expressive
than a C switch:
| Case | Matches |
|---|---|
case 5 | one value |
case 0, 1, 2 | any in a list |
case 60 to 89 | a range, ends included |
case is < 0 | an open comparison |
case else | everything remaining |
Cases are tested in order and the first match wins; there is no fall-through and nothing to
break out of.
select case as const compiles to a jump table, which is faster for a dense set of integer
cases — and it accepts only constant values, so case is < 0 inside one is TKO0419.
select case true runs the first case whose test is true. case ( n mod 3 ) = 0 works
there: the comparison's −1 or 0 is converted to the boolean subject, as every case value is.
When each branch asks a different question, though, if / elseif is usually the clearer shape.
Repeating
'' loops.tiko -- counting, repeating, and leaving early.
for i as int32 = 1 to 3
print "for " & i
next
for i as int32 = 10 to 0 step -5
print "down " & i
next
dim as int32 n = 0
do while n < 2
print "while " & n
n += 1
loop
n = 0
do
print "until " & n
n += 1
loop until n = 2
'' exit leaves the loop; continue skips to the next pass.
for i as int32 = 1 to 6
if i = 2 then continue for
if i = 4 then exit for
print "picked " & i
next
for 1
for 2
for 3
down 10
down 5
down 0
while 0
while 1
until 0
until 1
picked 1
picked 3
for … next counts, and step sets the stride — negative to count down. Declaring the counter in
the loop, for i as int32 = 1 to 3, keeps it out of the surrounding scope, which is what you want
almost always.
do … loop carries its condition at either end, with either sense:
do while c … loop— test first, may run zero timesdo until c … loop— the same, inverteddo … loop while canddo … loop until c— test last, so the body always runs once
exit for and exit do leave a loop; continue for and continue do skip to the next pass.
Naming the loop kind makes nested loops readable.
An older form, while … wend, also exists and behaves as do while … loop. Existing code uses it;
new code has no reason to.
Blocks that exist to bound a lifetime
'' scopeblock.tiko -- a block that exists only to bound a lifetime.
dim as int32 outer = 1
scope
dim as int32 inner = 2
defer print " leaving the scope"
print " inside: outer " & outer & ", inner " & inner
end scope
'' 'inner' is gone here, and the defer has already run.
print "outside: outer " & outer
inside: outer 1, inner 2
leaving the scope
outside: outer 1
scope … end scope introduces a block with no condition and no loop — just a boundary. Variables
declared inside it end there. It is how you keep a temporary from leaking into the rest of a long
procedure, and, as the next section shows, how you control exactly when a defer fires.
defer
defer takes a statement and runs it when the enclosing block is left — however it is left:
'' deferorder.tiko -- defer runs on the way out, in reverse order.
sub Work( byval failEarly as boolean )
print " open A"
defer print " close A"
print " open B"
defer print " close B"
if failEarly then
print " leaving early"
exit sub
end if
print " doing the work"
end sub
print "normal path:"
Work( false )
print
print "early exit:"
Work( true )
normal path:
open A
open B
doing the work
close B
close A
early exit:
open A
open B
leaving early
close B
close A
Two things to read off that output. Deferred statements run in reverse order, so resources are
released in the opposite order to the one they were acquired in — B before A. And they run on
every path out, including the early exit sub, which is the entire point: the cleanup is
written next to the thing it cleans up, and cannot be skipped by a return you add later.
That makes defer the answer to the oldest bug in resource handling — the path that forgot to
close something:
open "data.txt" for input as #1
defer close #1
Now the rule that catches everyone.
defer belongs to the nearest enclosing block, not to the procedure. An if, a loop body and
a scope are all blocks.
'' defertrap.tiko -- defer belongs to the nearest enclosing BLOCK.
sub Guarded( )
print " start"
if true then
print " acquired"
defer print " released" '' runs at end if, not end sub
print " still inside the if"
end if
print " after the if -- already released"
end sub
sub EachIteration( )
for i as int32 = 1 to 3
defer print " end of pass " & i
print " pass " & i
next
end sub
print "a defer inside an if:"
Guarded( )
print
print "a defer inside a loop body:"
EachIteration( )
a defer inside an if:
start
acquired
still inside the if
released
after the if -- already released
a defer inside a loop body:
pass 1
end of pass 1
pass 2
end of pass 2
pass 3
end of pass 3
Read the first half: the release happens at end if, while the procedure is still running. A
guard written as acquire inside an if, then defer releases the resource immediately and leaves
the rest of the procedure using something already closed — and nothing in the code looks wrong.
The fix is to leave early instead of nesting: test the failure case, exit sub, and put the
acquire-and-defer pair in the procedure's own body. Exercise 2 shows both side by side.
In a loop body the same rule is exactly what you want — the deferred statement runs at the end of each pass, as the second half of the output shows.
defer needs a block to be on the way out of, so it cannot appear at module level:
'' modulelevel.tiko -- defer needs a scope to leave, and a module
'' body is not one. Does not build.
defer print "this never had a scope to leave"
print "hello"
modulelevel.tiko(3,1): error TKO0814: 'defer' is not allowed at module level; put it
in a procedure or a block
Labels and goto
tiko has goto and line labels. They exist for two reasons: compatibility with older code, and
on error goto, which Chapter 18 covers.
Nothing in this book uses goto for control flow. Every loop construct above, plus exit and
continue, covers what it was for — and a goto that jumps across a defer is refused
(TKO0827), because the compiler cannot say what should have run on the way.
Summary
if/elseif/else; a one-lineiftakes noend if.select casematches values, lists, ranges and open comparisons, first match wins, no fall-through.as constbuilds a jump table and takes only constants.- A comparison yields an integer, not a
boolean— wrap withcboolbefore comparing one againsttrue. for … nextwithstep;do … loopwith the test at either end;exitandcontinue.scope … end scopebounds a lifetime deliberately.deferruns on the way out of its block, in reverse order, on every path — including the end of each loop pass, and including anifyou wrapped it in.
Exercises
- Write FizzBuzz for 1 to 15. Which reads better here,
if/elseiforselect case, and why? {solution="ex06-01"} - Write a procedure that acquires something inside an
ifand defers its release, and a second that guards first and defers in the procedure body. Print from both and compare the order. {solution="ex06-02"} - Put a
deferin a loop body that prints the loop counter. Before running it, predict how many times it fires and with which values. - Rewrite a
do … loop untilas ado while … loopthat behaves identically, including when the condition is already true at the start. Which needed more care?