Menu

uBasic and Tiny BASIC

thebeez

Introduction

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;
  • Multi line IF..THEN..ELSE..ENDIF supported;
  • Parameter passing supported by GOSUB, RETURN, TRY() and FUNC() extensions;
  • Local variables supported, both initialized and uninitialized, using PARAM() and LOCAL();
  • Alphanumeric labels supported;
  • "Structured Basic" commenting style;
  • Simple file I/O support, using OPEN(), CLOSE, READ() and WRITE;
  • Input parsing, using TOK() and SKIP;
  • Exception handling, using TRY() and RAISE;
  • String support, including familiar functions like LEN(), VAL(), STR() and JOIN().

Architecture

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.

Functions and procedures

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.


Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.