Tiny BASIC is a dialect of the BASIC programming language that can fit into as little as 2 or 3 KB of memory. This small size made it invaluable in the early days of microcomputers (the mid-1970s), when typical memory size was only 4–8 KB. The prevalence of BASIC on the first generation of home computers is an outcome of Tiny BASIC.
uBasic is an integer Basic interpreter in the tradition of Tiny BASIC, with which it is largely compatible. This version is entirely written in 4tH, some bugs have been removed and several additional features have been added, like user stack support and structured programming:
PUSH
, POP()
and TOS()
can be used to emulate DATA
statements, pass parameters to subroutines or make recursive subroutines;DO
, LOOP
, UNTIL
, WHILE
, CONTINUE
and BREAK
supported;IF
..THEN
..ELSE
..ENDIF
supported;GOSUB
, RETURN
, TRY()
and FUNC()
extensions;PARAM()
and LOCAL()
;OPEN()
, CLOSE
, READ()
and WRITE
;TOK()
and SKIP
;TRY()
and RAISE
;LEN()
, VAL()
, STR()
and JOIN()
.uBasic has no "symbol table" in the strict sense of the word. There is a table of "labels" (line numbers) and jump locations, though. These labels are scanned before the actual execution starts. Non-numeric labels are hashed.
There are two internal stacks, one for the FOR..NEXT
and DO..LOOP
statements and one for the GOSUB..RETURN
. The FOR..NEXT
stack is the most interesting. It holds the jump location, the limit, the increment and the bound variable. A DO..LOOP
differs only from a FOR..NEXT
loop in that it has no limit, no increment and no bound variable.
The bound variable is a pointer, so that changing the variable itself has the expected effect. The addresses of variables, whether local or global, are derived directly from their ASCII value. Global variables have a fixed offset. Local variables are relative to current frame pointer.
A new frame is created every time GOSUB
is called and discarded when a RETURN
is encountered. The local variables themselves are created when LOCAL()
is invoked. LOCAL()
simply relocates the frame pointer, freeing up space for the local variables.
Jumps are usually made using the label table, so they are quite fast, but there are exceptions like FOR
, BREAK
(called in UNTIL
and WHILE
) and multi-line IF
statements. Here the source is scanned until the required keyword is found and hence slow. Single-line IF
statements simply skip the rest of the line, which requires no parsing.
Combining non-numeric labels, the data stack and local variables allows for some pretty complex constructs. E.g. This is a (recursive) factorial:
Print "Factorial of 10: ";
Push (10) : Gosub _Factorial
Print Pop()
End
_Factorial
If Tos() = 1 Then Return
Push (Tos() - 1) : Gosub _Factorial
Push (Pop() * Pop())
Return
Adding local variables into the mix, makes it even more versatile:
_SquareRoot ' This is an integer SQR subroutine.
Local (1) ' This will create A@
Push ((10^(Pop()*2)) * Pop()) ' Output is scaled by 10^(TOS()).
a@ = Tos()
Do
Push (a@ + (Tos()/a@))/2
If Abs(a@ - Tos()) < 2 Then
a@ = Pop()
If Pop() Then
Push a@
Break
EndIf
EndIf
a@ = Pop()
Loop
Return
Most Forth users are used to manipulate the stack so directly, but those coming from other languages aren't. However, uBasic has facilities that make the stack almost completely transparent to the average programmer.
GOSUB
takes an optional parameter list and transfers its items silently to the stack. PARAM
works a lot like LOCAL
, but it does not just create local variables, it also initializes them, using items from the stack. The last local variable created is initialized by the top item on the stack - just as an unsuspecting programmer could expect:
_Factorial Param (1)
If a@ = 1 Then
Return (a@)
Else
Return (Func (_Factorial (a@ - 1)) * a@)
Endif
RETURN
takes an optional expression - and transfers it to the stack. Again, just like any other C-like language. The cherry on the ice is FUNC()
, which is just like GOSUB
with one very important difference: it returns the top of the stack - which was probably left there by RETURN
:
x = 9
Print "Factorial of ";x;": "; Func (_Factorial (x))
End
So we can rewrite our square root subroutine like this:
Print Func (_SquareRoot (10, 4))
End
_SquareRoot Param (2) ' This is an integer SQR subroutine.
Local (1) ' A@ holds 10, B@ holds 4, now comes C@
b@ = (10^(b@*2)) * a@ ' Output is scaled by 10^B@).
a@ = b@
Do
c@ = (a@ + (b@ / a@))/2
Until (Abs(a@ - c@) < 2)
a@ = c@
Loop
Return (c@)
The FUNC()
function turns the _SquareRoot
subroutine into a true user defined function. It places the items 10 and 4 on the stack. PARAM
creates local variables A@ and B@ and initializes them with these stack items.
Then LOCAL
creates an additional, uninitialized local variable, named C@. Finally RETURN
places the contents of C@ on the stack, where it is taken off by FUNC()
, which in turn returns it to PRINT
.
So it's safe to say one can adopt a completely "Structured Programming" style if one wants to. Note "classic" BASIC is fully supported as well. Both styles can even be combined in the same program.