Functions in TI-BASIC: Challenge accepted
At high school, I loved to toy around with my TI-84 Plus calculator. It was the most techy thing you were allowed to use during school hours. It featured a very stupid (read: challenging) programming language: TI-BASIC. I made a wild host of programs on it, like a program to count down to the end of class, and a text-to-morse converter.
In this article, I want to focus on one hurdle, and how I solved it: functions.
ClrHome
0->K
1->S
1->N
1->J
{0,0,0,0,0,0}->lCOUNT
While K!=22 and K!=45
0->C
For(I,1,N)
C+lCOUNT(I)->C
If I=J and S=0
Then
Output(I,1,">")
Else
Output(I,1," ")
End
Output(I,2,"COUNT:")
Output(I,9," ")
Output(I,9,lCOUNT(I))
End
Output(7,1,"AVG:")
Output(7,9," ")
Output(7,9,round(C/N,2))
If S=1
Then
Output(8,1,"COUNTERS:")
Output(8,11,N)
Else
Output(8,1," ")
End
0->K
While K=0
getKey->K
End
If S=1
Then
If K=95
Then
min(N+1,dim(lCOUNT))->N
0->lCOUNT(N)
0->K
End
If K=85
Then
max(1,N-1)->N
min(N,J)->J
ClrHome
0->K
End
If K=21
Then
0->S
0->K
End
End
If K=21
Then
1->S
End
If K=102
Then
0->lCOUNT(J)
End
If K=95 or K=85
Then
lCOUNT(J)-1+2*(K=95)->lCOUNT(J)
End
If K=34
Then
remainder(J,N)+1->J
End
If K=25
Then
remainder(J+N-2,N)+1->J
End
End
ClrHome
In this post:
- Functions: The need
- Solution 0: Everything in a separate program?
- Solution 1: A state machine
- Solution 2: A redirection center
- Solution 3: Abusing the call stack
#
Functions: The needFunctions are pieces of code that can be called from anywhere in the program. For example, to redraw the screen after having processed a keystroke. They are the bread and butter of all popular programming languages, but in TI-BASIC, they don't exist. Sure, there are of course the primitive functions to instruct the calculator to do things like clear the screen, but no ability to define a function yourself. From worst to best, what are the solutions?
#
Solution 0: Everything in a separate program?From within a program, you can call other programs. Even before getting to know any caviats, for me there's a giant dealbreaker: a huge amount of programs. Even with my self-contained programs, I have a long list of programs, and if each would be split up in say four parts, that would be very unmanagable. Also, if you need to distribute your program, that isn't very user friendly either. A self-contained program is definitely my go-to.
#
Solution 1: A state machineIn this method, you create a while-loop spanning the whole program. Suppose you
want to enter a 'setup' menu, then we change a variable 'S' from '0' to '1'.
When a key is pressed, you subsequently check if the mode is '0' (normal) or '1'
(setup), and handle the press accordingly. Do we want to leave 'setup' mode?
Then set S=0
and the next iteration of the while-loop takes care of it.
I've added a simple demonstration program below.
Setup
BeginLoop
DrawCounts
DrawAvg
DrawSetupUI
AwaitKey
HandleKey_Setup
HandleKey_Normal
EndLoop
This demonstrates a simple program where we have a number of counters. Each counter can be incremented by pressing the '+'-button (key 95, line 90). We can also reset a counter by pressing '0' (key 102, line 85). There are multiple counters, and we can navigate between them using the arrows (line 97).
Additionally, there is a 'setup' mode, in which we can increase and decrease the amount of counters. This mode is entered and leaved by pressing '2nd' (key 21).
#
Solution 2: A redirection centerWe don't need to do one big while-loop. TI-BASIC features a Goto
and Lbl
statement. A Lbl
is succeeded with one or two characters, e.g. Lbl A9
. Then,
from anywhere in your program (before or after), use a Goto
statement with
the same pair, e.g. Goto A9
. The program jumps to the Goto
line and runs
the lines following it.
#
The codeSure, this program is longer and a bit slower than its Solution 1 equivalent, but the investment has a return: we have a clear separation of the different parts in our code, and can call between them however we like. I've gone to the extremes by even giving each segment a name by ways of a loose string. [1]
The segmentation fulfills an extra role when programming on your calculator: moving code around is very difficult. If instead, you only need to reorder the function calls, in my opinion, this is a win. The extra colons are also discarded, they are just for code clarity.
[1]: Simply evaluating an expression without using its value is valid. It's
value is stored in Ans
. If it's the last statement of your program, the value
will be printed as a sort of 'result' of your program.
#
Function callTake a look at the "NORMAL"
segment. It's code is basically
Lbl 10
0→S
11→R
15→T
Goto RE
Lbl 11
12→R
01→T
Goto RE
Lbl 12
(Key handling)
Goto 10
Note the Lbl 10
and Goto 10
themselves generate a while-loop. The S=0
is
necessary for the "DRAW"
-segment, which will now be called. The following
four non-empty lines are the 'function call' of this solution. I set 'T' to the
label where we want to go, and set 'R' to where we need to come back after.
Setting T=15
, means we will call the "DRAW"
-segment. When the draw is done,
we need to return to our calling code. For this we place a label Lbl 11
at the
end of our function call, and with R=11
we need to indicate this. Next, we
jump to RE
. This is what I call 'the redirection center'. And that's our
function call complete! We repeat the same again to call "AWAITKEY"
.
Yes, I agree, this is bulky. But hey, when coding in TI-BASIC, bulkiness is the
name of the game. In return, we are now able to clearly split up our code, and
call it from multiple places, like the "AWAITKEY"
-segment. After the call is
completed, we just resume where we left of.
#
Dynamic Goto'sOk, so what's the idea of a redirection center? The unfortunate thing of
Goto
's is its destination has to be hardcoded. Hence for each possible return
destination we have a Goto
, and with If
-statements, we select the right one.
Lines 178 through 195 do this. We do this in one place, which I've been calling
the 'redirection center'. In the previous example, we had set R=11
, and upon
returning from the call, we use Goto RE
to jump to the redirection center, and
it will execute the Goto 11
line.
With this, the essence is
11→R
Goto 15
Lbl 11
Disp "B"
Stop
:::
Lbl 15
Disp "C"
Goto RE
:::
Lbl RE
If R=11
Goto 11
If R=12
Goto 12
Disp "E"
This outputs ACDB
. Note we aren't using any Lbl 12
yet, so Goto 12
isn't
valid. But as long as we don't set R=12
, the program won't notice it.
#
NestingThe code fragment from Dynamic Goto's doesn't allow function nesting. In the section Function call we showed a slightly different function call code which does allow for nesting, and that also requires some extra code in the redirection center.
Lbl RE
If R=-Â1
Then
⌊RED(dim(⌊RED))→T
dim(⌊RED)-1→dim(⌊RED)
Else
R→⌊RED(dim(⌊RED)+1)
Â-1→R
End
(Goto statements)
We use a new list RED
to keep track of the places we will to return to. At
the start of your program set {Â-1}→⌊RED
to create the list.
If we get R
and T
specified, we add R
to the list. We then set R=-1
so
if we consequently jump to RE
for the end of the function, still R=-1
, and
we know we are leaving a function instead of entering one. The goto statements
will then select the segment T
refers to.
If we haven't set R
and T
before jumping to RE
, then R=-1
. In that case,
we need to return to the last caller return. For this, we set the target T
to
the last element of the list, and shrink the list so the last element is
removed. The goto statements then jump to that old caller.
#
Solution 3: Abusing the call stack#
The trickLet me tell you something: when you don't close a scope with End
, you won't
get any complaints: it's as if those are added at the very end of your program
implicitly. Ok, something for when you're very lazy, right? Well, not quite.
Without it, our program in Solution 2 would have gotten those errors when trying
to quit.
And there is even an obscure way that could let our program crash. An example which could do the crash more easily:
Lbl 1A
startTmr→T
If remainder(T,2)=0
Then
Goto 1B
Else
Goto 1A
End
Lbl 1B
Output(1,1,T)
Goto 1A
A little program which displays even times on screen. The output is done in the
1B
segment. But this unsuspecting program crashes with error ERR:MEMORY. In
fact, it's a stackoverflow. It might be more instructive to imagine a
While
-loop instead of an If
-block. If I write
While 1
Output(1,1,startTmr)
End
then at each encounter of End
, it needs to jump back to the start of the
While
-loop. Hence, the calculator remembers what block we are in. However,
when we use Goto
, this stack is preserved! In the main fragment, it is to the
calculator as if we are opening an infinite amount of If
blocks, without ever
having closed one!
This finding is our superhero in the quest for functions in TI-BASIC. Because,
if a While
-, For
- or Repeat
-loop is open, encountering an End
can return
execution to the start of the loop. This is done if the loops condition allows
a next iteration.
The strategy: we use Goto
while a loop is open, the called segment finishes
with an extra End
, which is matched against the open loop. Then, the loop
needs to be run another time, but of course without the Goto
being called
a second time. This translates to the following scheme:
For(H,-Â1,0
If H:Goto 15
End:-Â1→H
Disp "B"
Stop
Lbl 15
Disp "C"
:End
ClrHome
Disp "A"
For(H,-1,0
If H:Goto 15
End:-1->H
Disp "B"
Stop
Lbl 15
Disp "C"
:End
This outputs ACB
. The variable H
is chosen as it is a 'hook' in some way.
At the first iteration, H=-1
, and If
evaluates for any non-zero value. At
the second iteration, H=0
, and the Goto
won't be evaluated. Next H=1
, and
this breaks the For
-loop. Finally, we need to set H=-1
again, because of
nesting: if we now encounter the End
to return to a call from higher up,
the H=1
will be incremented to H=2
, but that is higher than the 0
the
For
-loop requires. Hence it will not iterate that one, and our whole scheme
is broken. Therefore, you should always set H=-1
again after a function call
has been performed.
Colons mean a separation of statements, that's also why the calculator renders
each new line with a preceding colon. You can add extra colons, doing so adds an
empty statement (like an empty line), so in most places this can be done purely
aesthetically. This is what I do to indicate the 'special' End
's; the :End
doesn't have a matching block begin above, it is there to return to our call
hook.
#
The codeNow our program can be written as
ClrHome
"MIT 2021 VINCENT KUHLMANN"
0->K
1->N
1->J
{0,0,0,0,0,0}->lCOUNT
Goto 20
:::::::::
"AWAITKEY"
Lbl 01
0->K
While K=0
getKey->K
End
If K=22 or K=45
Goto 99
:End
:::::::::
"NORMAL"
Lbl 10
0->S
For(H,-1,0
If H:Goto 15
End:-1->H
For(H,-1,0
If H:Goto 01
End:-1->H
If K=21
Goto 20
If K=102
Then
0->lCOUNT(J)
End
If K=95 or K=85
Then
lCOUNT(J)-1+2*(K=95)->lCOUNT(J)
End
If K=34
Then
remainder(J,N)+1->J
End
If K=25
Then
remainder(J+N-2,N)+1->J
End
Goto 10
:::::::::
"DRAW"
Lbl 15
0->C
For(I,1,N)
C+lCOUNT(I)->C
If I=J and S=0
Then
Output(I,1,">")
Else
Output(I,1," ")
End
Output(I,2,"COUNT:")
Output(I,9," ")
Output(I,9,lCOUNT(I))
End
Output(7,1,"AVG:")
Output(7,9," ")
Output(7,9,round(C/N,2))
If S=1
Then
Output(8,1,"COUNTERS:")
Output(8,11,N)
Else
Output(8,1," ")
End
:End
:::::::::
"SETUP"
Lbl 20
1->S
For(H,-1,0
If H:Goto 15
End:-1->H
For(H,-1,0)
If H:Goto 01
End:-1->H
If K=95
Then
min(N+1,dim(lCOUNT))->N
0->lCOUNT(N)
0->K
End
If K=85
Then
max(1,N-1)->N
min(N,J)->J
ClrHome
0->K
End
If K=21
Goto 10
Goto 20
:::::::::
Lbl 99
ClrHome