Program flow

Introduction

A script, like a computer program, starts at the beginning and finishes at the end. However there is a lot more to it than that. Consider:
PRINT "0"
PRINT "1"
PRINT "2"
PRINT "3"
PRINT "4"
PRINT "5"
PRINT "6"
PRINT "7"
PRINT "8"
PRINT "9"

That is one way to print the numbers from 0 to 9. It works, but can you imagine the code to print the alphabet? How much easier it would be to:
FOR num% = 0 TO 9
  PRINT num%
NEXT

What the second example does is to say: Right, we're going to go around and around with our 'num' variable going from zero to nine. This is called a loop, it is a bit like a loop of string, round and round. Each time we will PRINT what our num variable is.
In this way, ten lines of code becomes three. And, better yet, it is possible to output every single printable character - all the numbers, all the letter (upper and lower case), all the symbols, and the extra stuff and accented things - with a mere three lines of code (instead of 223 lines).

That is one aspect of program flow. The second is in making decisions. For example, what if we examine the identity of the currently tuned channel and it is not the channel ID we expect? Do we plough on regardless, or do we get the user to do something? To be able to have this sort of control requires the ability to make decisions; and this is what we shall look at first:

 

Making decisions

In BASIC, you can do things like:
IF ( (A = 1) AND (B = 2) AND (C = 3) ) THEN ...do something...

You can do this in script by knowing that the if() syntax is:

if( <var> <condition> <number> ) <command>

If the condition evaluates to be true, then the 'command' is executed. There is no reason why the 'command' cannot be another if() command.

So, first, the conditions:
  ! Not equal
  = Equal
  < Less than
  > Greater than
  [ Less than or equal to
  ] Greater than or equal to

All of the following examples evaluate to TRUE, so the command would be executed.

set A to 123
if(A = 123) ...
if(A ! 0) ...
if(A > 12) ...
if(A < 1973) ...
if(A [ 123) ...
if(A ] 123) ...

All of the following evaluate to FALSE. Remember, A is zero at script start.

if(A = 123) ...
if(A ! 0) ...
if(A > 12) ...

You get the idea.

Note that you can compare a float with an integer. 99.4 is less than 100, 103.2 is more...

So, that line of BASIC at the start could be written in our script as:

if(A = 1) if(B = 3) if(C = 3) ...do something...

Now let's assume you have read some data from a teletext frame, and you'd like to convert it. We shall invent an algorythm, where:
result = ( (((input << 3 ) / 4) >> 1) * 5 )
We only want to do this to numbers over 100, but we may be called with numbers less than 100. What we need is an 'if()' spanning several lines.

There are two ways to do this:

The wrong way...

if(Q ] 100) set R to Q
if(Q ] 100) lsl(R, 3)
if(Q ] 100) div(R, R, 4)
if(Q ] 100) lsr(R, 2)
if(Q ] 100) mul(R, R, 5)

The right way...

if(Q < 100) go("skipthisstuff")
  set R to Q
  lsl(R, 3)
  div(R, R, 4)
  lsr(R, 2)
  mul(R, R, 5)

 .skipthisstuff

This leads us on to...

 

Branching

You have the ability to branch. That means to instruct the script interpreter to jump to another location in the program.

Branches are defined as a period followed by the 'name' of the location. This is called a label, and looks like:

.branchpoint
or:
.mungecode
Labels can contain practically any printable character (except space) as they are treated as strings, however it is best to stick to upper and lower case letters and an underscore. By habit I write my labels as lower-case words run together, however you can use the style you prefer, such as:
.BranchPoint
or:
.munge_code

Under RISC OS, you can define up to sixteen labels. Under Windows, there is no limit.

All defined labels are read when the script is initialised, and subsequently you can use the go() command to jump to a label, to branch.
You can branch forwards or backwards, as all of the labels are already 'known' as the script begins execution.

 

Recursive code

This means code that loops around. We have already seen this in the FOR...NEXT example, and perhaps a good practical example is code to check we're tuned to the right channel:
message("Please set satellite receiver to \"TV5 FBS\" (channel FAV:46)")
.isittv5
  channelid(A)
  if (A ! &AF00) message("Please set satellite receiver to \"TV5 FBS\" (channel FAV:46)")
  if (A ! &AF00) go("isittv5")

This tactic may seem unusual, however if you think about it, more and more people will be receiving their broadcasts from a set-top box which is either digital terrestrial or digital satellite. These devices will output a multitude of channels on the one UHF 'channel' (or perhaps via a VCR if it can't output UHF by itself). Therefore it is prudent to check the channel ID and ask for a little bit of user interaction.
In any case, the above example will go around and around until the channel ID it reads is &AF00 - hence it is recursive code.

 

Implementing constructs - FOR...NEXT

In typical BASIC, a FOR...NEXT loop would look like:
FOR x% = 101 TO 109
  PROCget_page(x%)
NEXT

In our teletext script, we can achieve the same thing using recursive code and a loop. The only other complication is that the variable initialise and increment is not performed automatically.

set A to 101
.comebackhere
  getframes(A)
  A++
  if (A < 110) go("comebackhere")

 

Implementing constructs - DO...WHILE

In typical BASIC, a DO...WHILE loop is a lot like FOR loop, but instead of controlling a variable, we look for a specific event. Here's some fake BASIC:
PRINT "Please select TV5 FBS"
DO
  ch% = FNget_channel_id
WHILE (ch% <> &AF00)

We've already seen this before:

message("Please select TV5 FBS")
.isittv5
  channelid(A)
  if (A ! &AF00) message("Please select TV5 FBS")
  if (A ! &AF00) go("isittv5")

Note that, again, it boils down to recursive code and an IF test.

 

Implementing constructs - REPEAT...UNTIL

A REPEAT...UNTIL loop is very similar to a DO...WHILE. Indeed, the difference is mainly one of syntax. The DO...WHILE will loop while the clause is still true, like:
DO : Play Cricket : WHILE (It is still sunny)
while the REPEAT...UNTIL will loop until the clause is true, like:
REPEAT : Search for that film you haven't watched : UNTIL (You have found the DVD)

Because of this, there is no point providing an example. You are going to simply have to work out the mechanics of what you are trying to do, and then implement it using an if test and a branch. By doing this, you can implement all sorts of everything, from simple FOR...NEXT loops to SELECT CASE statements. Because of the ability to nest if statements inside other ones, you can arrange some quite complex control structures.

 

Call/Return

You can raise the bar one degree higher by adding 'procedural calls' to the mix. These look and feel a lot like a simple branch, however where you branched from is remembered so you can return back to that point.
In other words, you can write simple procedures.

By way of example, I have a script that reads the temperatures of today and tomorrow's weather and writes them to file as celsius and fahrenheit. This was performed as a demonstration of the maths system and procedural code because, to be honest, I don't know fahrenheit at all. Anyway, there were two possible ways to do this:

  1. Write linear code, starts at the beginning, finishes at the end, runs through instruction by instruction. This would require including the °C->F code twice.
  2. Write code with a subroutine to perform the °C->F conversion, and call that when required.

In order to perform a procedural call, you should use the command call() (instead of go()). What this does is jump to the specified label, after saving the return location on the call stack.
When you have finished with the subroutine, you can return to the line following the call() by using the return() command.

[...setup stuff...]

; read today's high and low
selectframe(201)
info("Generating report...")
readvalue(D, 14, 14)   ; D = today's high
readvalue(E, 18, 14)   ; E = today's low

filewritestring("Nantes today    : High = ")
filewritevar(D, 2)
filewritestring("°C  (")
set A to D
call("fahrenheit")
filewritevar(B, 2)
filewritestring("F)")
filewritebyte(13)
filewritebyte(10)
filewritestring("                  Low  = ")
filewritevar(E, 2)
filewritestring("°C  (")
set A to E
call("fahrenheit")
filewritevar(B, 2)
filewritestring("F)")
filewritebyte(13)
filewritebyte(10)
filewritebyte(13)
filewritebyte(10)

[...similar code for tomorrow's forecast...]

[...tidyup code...]
quit()


.fahrenheit
  ; takes a value in A, converts it to fahrenheit,
  ; places a rounded version of the result into B...
  ; CORRUPTS 'Z' and 'B'
  set Z to A
  add(Z, 40)    ; Celsuis + 40
  mul(Z, Z, 9)  ; * 9
  div(Z, Z, 5)  ; \ 5
  sub(Z, 40)    ; - 40      [isn't there a +/- 32 algorithm?]
  roundcast(B, Z)
  return()

 

The call stack

Every time you 'call()', the return location is placed on a "call stack". This is simply a list of return locations. When you 'return()', the most recent address is taken from the call stack and used. After being used, it is discarded - meaning for each 'call()' the call stack grows by one, and for each 'return()' the call stack shrinks by one.
A typical analogy is that of a stack of plates. Every 'call()' places a plate and every 'return()' removes the topmost plate.

There is no way to arbitrarily discard or otherwise skip entries in the call stack. Every 'call()' must have a matching 'return()'. There can be more than one return in code - i.e. in the middle of a function if it detects invalid input as well as the usual one at the end of the function - so long as programmatically only one is executed per call, depending on the conditions of trigger. For example:

set Q to 23
set R to 0
call("divide")
quit()


.divide
  set S to 0
  if (R = 0) return();
  div(S, Q, R)
  return();
What this does is to simply divide Q by R and place the result into S. However, if it detects that R is zero, it aborts early to save a division-by-zero error. So there are two possible 'return()'s, but given the circumstances, only one is ever valid. Either R is zero (hence the first one) or it isn't (hence the second one).

It is considered a failure to end a script with a call stack; this implies a mismatched call/return pair.

Under RISC OS, the call stack may be up to 16 entries. There is only one way to end a script under RISC OS (the terminate() command), so abandoning a script prematurely may result in warnings if a call stack is active.

Under Windows, there is no limit to the size of the call stack. Furthermore, the terminate() command is specifically intended for abandoning scripts, so you will not receive a warning if there is a call stack defined. The correct way to end a script (under Windows) is with the quit() command, and this will warn if there is a call stack.
The the lvar() command (under Windows) lists the call stack, the result looking like:

Call stack:
    4 --> +398   line  30
    3 --> +290   line  23
    2 --> +194   line  17
    1 --> +101   line   6
if there is a call stack active, or:
Call stack:
  -- no call stack --
otherwise.