Labels & Subroutines
Implementing labels
So far our language is just a list of commands - the computer is yet to think for itself, or make decisions based on data we give it. That is to change in the next couple of parts. First of all, we'll give the interpreter the ability to jump around the source code we give it at will.
A label is a marker placed in source code. We can jump to this marker from a certain point in code. For example:
MSG Hello! JUMP testlabel MSG World! LBL testlabel
Would see the command "MSG World!" skipped completely as we jump to the label just below it.
First of all, we need a place to keep a list of all our labels. Add the following to your form declarations:
' Label lookup Dim Labels As New Collection
We can now write two new functions, on to add a label to our list of labels, and one to get the position of a label:
Function CreateLabel(LabelName As String, LabelPos As Integer) Labels.Add LabelPos, LabelName End Function Function FindLabel(LabelName As String) As Integer FindLabel = Labels(LabelName) End Function
Pretty harmless, eh? ;) If you remember in the previous part we had to empty the list of variables each time we ran the program, otherwise we would get "variable already exists" errors. Add a call to EmptyCollection in Init, this time for the label list:
'Initialises Sub Init() ' Clean var list EmptyCollection Vars ' Clean label list EmptyCollection Labels End Sub
Finally, we can add two commands. A command to declare a label, and a command to jump to a label. Here are the case statements, to go in the usual place:
' Declare a label
Case "lbl"
CreateLabel Token(), LineNum
' Jump to a label
Case "jump"
LineNum = FindLabel(Token())
Note that at the moment we can ONLY jump to labels we've declared in a previous line of code. Not very good, and means the only way we can test the program is by putting it in a never-ending loop. So SAVE RIGHT NOW then try this. You'll need to Ctrl-Alt-Del to close VB, so I suggest you save!
LBL begin MSG Hello JUMP begin
To fix this problem, we need to go through the code beforehand, finding all the labels, and declaring them then. So, to complement the sub MainPass, create a new one called LabelPass. Here's my code for it:
' Pass through code for labels
Sub LabelPass()
Dim LN As Integer
Dim CodeLine As String
LN = 0
' Loop thru lines looking for a label + declare
For Each CodeLine In Code
Tokenise (CodeLine)
If LCase(Token()) = "lbl" Then
CreateLabel Token(), LN
End If
LN = LN + 1
Next CodeLine
End Sub
Note that this sub also needs acces to the array Code() which is created in MainPass. Hence, we can move the line
' Load code into array in 1-line chunks Code = Split(txtCode.Text, vbCrLf)
Out of MainPass, and into Parse() like so. Note I've also added the call to LabelPass in here:
'Parses a complete program Sub Parse() ' Load code into array in 1-line chunks Code = Split(txtCode.Text, vbCrLf) Init LabelPass MainPass End Sub
Now try the following code:
jump end msg hello lbl end
You should end up with... nothing. If nothing happens, well done! If you get any errors don't worry - writing this section was complex and so a few errors might have crept into my code. However I can guarantee the downloadable project files work, so just grab the code out of there.
Subroutines
A subroutine is a section of code that can be called, and then at a specific point you return to exactly where you left off. As a few examples - look at the VB subs we've been using here! A section of code for example, I called Init, is called in Parse(), then at "End Sub" we return to where we left off.
It's easy to jump to a certain point in code - just use a label. But the returning? We need to have a way of saving our last line number, then skipping back to that when we hit a "return" command. That's easy, what is hard is coping with things like this:
SUB MySub1 GOSUB MySub2 RETURN SUB MySub2 GOSUB MySub3 RETURN SUB MySub3 GOSUB MySub4 RETURN SUB MySub4 GOSUB MySub5 RETURN
And so on. Confused yet? That sort of nesting happens a lot - and would be a devil to manage.
I give you, the stack.
Stacks
Imagine you have a few books, and you pile themn up. First you put a red book down, then a blue book down, and lastly you put a green book on top of the pile. You now have a pile of 3 books. A stack of books even.
Now, take the first one off the top. That the last one you put on. The take another one off, that's the blue one - the penultimate one you put on. Your final book off is the first one you put on. This is what a stack is - a first in, last out list.
In computing terms you can do two things with a stack - you can put data onto it (called a "push") or you can take some data off the top (called a "pop"). I'll be using a stack to keep a list of return line numbers for nested subroutines.
Here's how it will work:
- Encounter a "GOSUB" - jump to the label, and push the return position onto the stack.
- Continue to execute
- Encounter a "RETURN" - pop the first value off the top of the stack and go back to the line indicated by the stack value.
Here's how it would work in real life:
00: GOSUB MySub1 01: END. 02: SUB MySub1 03: GOSUB MySub2 04: RETURN 05: 06: SUB MySub2 07: MSG Hello 08: RETURN
- Line 0: Skip to line 2, pushing "0" onto the stack. (Stack now contains: 0)
- Line 3: Skip to line 6, pushing "3" onto the stack. (Stack: 3, 0)
- Line 7: Message "Hello"
- Line 8: Pop value off stack - "3"- and go back to line 3. (Stack: 0)
- Line 4: Pop value off stack - "0" - and go back to line 0.
- Line 1: End program.
Note that when jumping to a line number the current line number is immediately incremented again after the case statement - so when jumping to line 6, the next line to be executed is line 7 etc.
Since VB6 does not have a stack object built in, I use this class I found on the internet (credit to whoever's it is!)
Private colData As Collection
Private Sub Class_Initialize()
Set colData = New Collection
End Sub
Public Sub Clear()
Set colData = New Collection
End Sub
Public Function Count() As Long
Count = colData.Count
End Function
Public Sub Push(value As Variant)
colData.Add value
End Sub
Public Function Pop() As Variant
Pop = colData.Item(colData.Count)
colData.Remove colData.Count
End Function
Public Function Peek() As Variant
Peek = colData.Item(colData.Count)
End Function
Private Sub Class_Terminate()
Set colData = Nothing
End Sub
Copy this into a new class called Stack and save (I called the file "Stack.cls"). Now add this declaration at the top of the form:
' For nested subs Dim ReturnTargets As New Stack
And finally the two case statements for dealing with "gosub" and "return" commands.
' Go to a label as a subroutine
Case "gosub"
ReturnLine = LineNum + 1
' Add to the stack of return destinations
LabelPos = FindLabel(Token())
ReturnTargets.Push ReturnLine
' Go to line
LineNum = LabelPos
' Return from a sub
Case "return"
ReturnLine = Val(ReturnTargets.Pop)
LineNum = ReturnLine - 1
And that's it. Implementing subroutines in hardly any time at all. Once we have the basic label system working, lots of doors open for us. In the next part I will show you how to have Do, Loop, Repeat, Until, While and If commands. I'll also deal with boolean comparisions (if value = value), and how to make decisions based on whether a comparison was true or false.