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.