Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

CaseMatches
case 5one value
case 0, 1, 2any in a list
case 60 to 89a range, ends included
case is < 0an open comparison
case elseeverything remaining

Cases are tested in order and the first match wins; there is no fall-through and nothing to break out of.

Note

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.

Note

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 times
  • do until c … loop — the same, inverted
  • do … loop while c and do … 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.

Pitfall

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-line if takes no end if.
  • select case matches values, lists, ranges and open comparisons, first match wins, no fall-through. as const builds a jump table and takes only constants.
  • A comparison yields an integer, not a boolean — wrap with cbool before comparing one against true.
  • for … next with step; do … loop with the test at either end; exit and continue.
  • scope … end scope bounds a lifetime deliberately.
  • defer runs on the way out of its block, in reverse order, on every path — including the end of each loop pass, and including an if you wrapped it in.

Exercises

  1. Write FizzBuzz for 1 to 15. Which reads better here, if/elseif or select case, and why? {solution="ex06-01"}
  2. Write a procedure that acquires something inside an if and 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"}
  3. Put a defer in a loop body that prints the loop counter. Before running it, predict how many times it fires and with which values.
  4. Rewrite a do … loop until as a do while … loop that behaves identically, including when the condition is already true at the start. Which needed more care?