[ BASIC Tips & Tricks ]


Introduction

 

    A few years back I had the idea to jot down all of the little noteworthy things I'd found out about BBC BASIC. Pretty soon I had a flaky set of notes, written on bits of torn off paper and scribbled down hastily in notebooks.

    Some time later, whilst searching for useful things to fill up my web space with, I realised that these notes would be of value to others as well as myself and decided to turn them all into a web page, which you find here.

    These tips and tricks are intended for the more experienced BASIC coder - if you are new to programming, or unfamiliar with BBC BASIC V, then you could find that you will confuse yourself by reading these pages.

    Sometimes most of the help you need on a BASIC keyword can be found out from the HELP command, which provides some simple help on most of BASIC's keywords.

    Although this page is not intended to replace Acorn's own BASIC reference manual, there will be overlap at some point.


The DIM Statement
Correct use of the DIM statement

 

The DIM statement is often used incorrectly, for example consider 'DIM b% 256', for which BASIC allocates 260 bytes. The parameter passed to DIM is indexed at 0, so if you want to allocate an n byte block you should specify n-1 bytes in the DIM statement. BASIC always allocates blocks in multiples of four bytes, so non-word-aligned values will always be rounded up to a word boundary, hence in the above example, the DIM statement tells BASIC to allocate 257 bytes and BASIC rounds this up to 260.

Remember that the first element of an array or DIMmed block is at zero, not one.

Negative DIM statement

 

Using a statement such as 'DIM b% -1' will set aside no space for b%, but will set b% to the address of the start of the next free location. This value is the last byte+1 of the last block allocated. This makes it easy to set aside space for assembly and assign the L% range-check value simultaneously. e.g. 'DIM code% 255, L% -1'. As BASIC always allocates blocks as multiples of four bytes, the block will always be word aligned.

Other negative values in DIM statements will generate an error.

Avoid DIMs

 

Speaks for itself really. For any serious work, you're often better off just writing some simple memory management routines.


Useful Statements
The TRACE statement

 

TRACE is a very useful statement. It is used as follows:

TRACE [STEP] ON|OFF|PROC|<number>
Trace [in single step mode] on or off or procedure or function calls or lines below the number.
TRACE TO <string>
Send all output to stream <string>.
TRACE CLOSE
Close stream output.
Expression: TRACE
Gives handle of the stream.

By using BPUT#TRACE,"string" then "string" will be sent to the trace file. Typically you would write IF TRACE THEN BPUT#TRACE,"string".

Formatting using LISTO

 

LIST [<line number>][,[<line number>]][IF<pattern>]. list section [if pattern]
LISTO <option number>. Bits mean:-
0: space before line
1: indent structure
2: split lines at ":"
3: don't list line number
4: list tokens in lower case

DATA

 

DATA statements tend to be intertwined with line numbers, which we don't really want to have to think about anymore. RESTORE [+][number] allows the data pointer to be set to the first DATA in the program - 'RESTORE' - the next set of data ahead of the statement - 'RESTORE +' - or the data number lines ahead - 'RESTORE +number'.

You can save the current data pointer by using 'LOCAL DATA' and restore it to its previous value by using 'RESTORE DATA'. RESTORE DATA is performed automatically if you use LOCAL DATA in a function or procedure.

SUMLEN

 

The SUMLEN function returns the sum of the lengths of all the strings in a string array. For example:

PRINT "There are ";SUMLEN(strings$());" characters in the array."

MOD

 

In addition to its usual function of returning the remainder of a division, the MOD function will return the modulus (square root of the sum of the squares of each element) of a numeric array. For example:

normalisedvector() = vector() / MOD vector()

QUIT

 

As a function, QUIT will return TRUE if BASIC was invoked with the -quit command line parameter.

CRUNCH

 

CRUNCH is a simple BASIC compressor built into the interpreter itself. The reason that it is simple, is that even the 'clever' BASIC compressors can't always compress a program and retain its original meaning (i.e. errors creep into the expressions within the code).

The statement is passed a single integer (or expression) which is a bitfield, constructed as follows:

  • 0 - Remove spaces before statements
  • 1 - Remove spaces in statements
  • 2 - Remove all but the first REM statement
  • 3 - Remove empty statements
  • 4 - Remove empty lines

BASIC will automatically CRUNCH programs if BASIC$Crunch is defined. I personally don't advise this as some programs object to being CRUNCHed and voice their objection by locking up the machine. :-(

Using the RETURN statement

 

The RETURN statement can be used in the declaration of procedures and functions to indicate that the variable passed is to have its value written back when the function exits. For example:

DEF PROCtimestwo(RETURN number%)
number% = number% * 2
ENDPROC

Any variable passed to this procedure would normally be protected from being changed. Once the RETURN is added, any changes to the variable is automatically reflected when the procedure or function exits.

A side effect of using this is that if you call a procedure or function which uses RETURN with an actual parameter - e.g. PROCtimestwo(55) - then BASIC will produce the error 'Invalid RETURN actual parameter'.

END

 

This expression alters the amount of memory allocated to BASIC, so you can actually change the wimpslot of your program without resorting to using SYS "Wimp_SlotSize"..., which is larger and trickier.

For example, the expression 'END = END + 4096' will set the slot of the program to be whatever it takes, plus 4K of free space.


Saving Space, Increasing Speed
Use Integers

 

I think of this as obvious, but I'll point it out anyway: Use Integer variables in preference to Real variables wherever possible - they're faster. Although in most cases, they use the same amount of memory - both use 12 bytes for a single-character variable name. (BASIC VI uses 16 bytes for a Real variable as its real variables are stored as actual floating-point numbers).

Bitfields

 

Using an integer as a set of flags is much more space efficient than using several variables, or variable arrays, to do the same job.

Omitting brackets

 

Where operator priority allows, you can often omit brackets. For example, the following code:

(a * b) << (c + d - e)

can be rewritten as the following:

a * b << c + d - e

which will still yeild the same result, as the numerical operators all have a higher priority than the shift (<<) operator.

Brackets can be omitted on most functions e.g. LEN(file$) becomes LENfile$

Bitfields

 

Using an integer as a set of flags is much more efficient than using several variables, or variable arrays, to do the same job.

Bit-clearing

 

Use '<value> AND NOT <bitmask>' to clear bits in Integer variables. You can also use statements such as '<value>AND-4' as -1 is equivalent to &FFFFFFFF.

Omitting <>0

 

<>0 can be omitted in most cases if it corresponds to a boolean state. For example, 'IF n<>0 THEN ...' can be replaced with 'IF n THEN ...'. This works as zero is interpreted to be FALSE and any non-zero is interpreted to be TRUE (its actual value is -1). All the IF statement does is check if the expression given to see if it is TRUE or FALSE.

Accessing memory

 

You access memory using the ! (word at), ? (byte at) and | (real at) operators. Instead of using '?(address+n)' you can use 'address?n'. 'address?0' is equivalent to '?address'.

Shift multiplication

 

Using the << shift command to replace multiplications by power-of-two numbers will yeild a speedup.

Being clever 8-)

 

Although you can write code that looks like this:

IF boolean-expression THEN variable=TRUE ELSE variable=FALSE

Use your brain! With a bit of thought you can see that an exact equivalent is:

variable=(boolean-expression)

There's usually plenty of code which can bereduced like this.

Memory use

 

Unlike certain other languages, BASIC cannot reallocate DIMmed memory blocks (except on a CLEAR statement). This means that you have to be very careful about when and where you allocate memory. Consider the following code fragment:

DEF PROCdosomething
LOCAL mem%
DIM mem% 255
...
ENDPROC

Each time this is called, 256 bytes will be allocated. Once the procedure is exited the memory will not be returned to the control of the interpreter (even though mem% is marked as LOCAL) so if this procedure is repeatedly called, you will eventually run out of memory.

Saying that, if you DIM string and integer arrays and they're marked as local, then you will get the memory back when the routine finishes.

Operator priorities

 
Priority Operator
1 - (unary minus)
+ (unary plus)
NOT
FN
( )
?
!
$
|
2 ^
3 *
/
DIV
MOD
4 + (addition)
- (subtraction)
5
(Note that you may use only one priority 5 operator per bracketed function.)
=
< >
<
>
<=
>=
<<
>>
>>>
6 AND
7 OR
EOR

Constant variable reduction

 

This is a feature of the BasCompress and StrongBS BASIC compressors that I find very useful indeed.

The compressor notes all variables that don't change over the duration of the program - constant variables - and replaces all references to them in the output file with their equivalent constant value.

Because constant values tend to be faster than constant variables, this gives a speedup as well as (often) reducing the size of the program.

Using LIBRARY

 

LIBRARY "<file>" allows all the functions and procedures of the specified BASIC file to be used in your program.

It's usual for programmers to build up collections of their own generic routines which they amass into libraries. The LIBRARY statement allows these libraries to be loaded into BASIC's memory at run-time for use by your program.

Block array initialisation

 

Whole arrays (of any type) can be assigned in one go by supplying a set of parameters, rather than a single value.

For example:

transformation()=1, 0, 0, 0, 1, 0, 0, 0, 1

The parameter list does not need to have as many entries as the array that is being assigned.

You can also do block operations leading to very powerful statements such as:

coordinate() = coordinate() * transformation()

Note that the last subscript changes quickest, so, using a 3x3x3 array as an example, the following would happen:

(0, 0, 0) = first
(0, 0, 1) = second
(0, 0, 2) = third
(0, 1, 0) = fourth
(0, 1, 1) = fifth
(0, 1, 2) = sixth
(0, 2, 0) = seventh
  . . .   = n'th
(2, 2, 2) = twenty-seventh

@%

 

@% sets the format of the printed numerical output and, optionally, string output. It is an unusual variable as you can assign it both a number and a string, the string being an ANSI printf format string. PRINT @% will display a number. The number is a four-byte field and constructed as:

<&80 for "," separator, &00 for "."><0,1,2 for G,E,F><digits after '.'><digits before '.'>

The default format is "G10.9".


Avoiding Faulty Code
Compressing down multi-line THENs

 

Multiple-line THEN statements can often be compressed down to a single line to provide a useful saving of space. However, doing this can lead to some problems when the compressed structures don't behave as you would expect them to. For example, say you have a construct like this:

IF a THEN
  IF b THEN
    a-and-b
  ELSE
    a-and-not-b
  ENDIF
ENDIF

and you compress it down to:

IF a THEN IF b THEN a-and-b ELSE a-and-not-b

then BASIC will execute a-and-not-b when a or b is FALSE - the first matching ELSE found is executed - which obviously isn't equivalent to the multiple-line construct.

TIME

 

The TIME statement returns the number of centiseconds elasped since the machine was switched on. By default, it is equal to the value returned from OS_MonotonicTime. This value is used in some OS calls, such as Wimp_PollIdle to represent time delays.

TIME uses the OS_Word calls 1 and 2 to read and write the system clock - it is not private to each running BASIC program. This means that if you're using TIME in a multitasking program and another program issues TIME = <some value> (to change BASIC's idea of the time), then your program will be affected.

Some programs use code such as:

SYS "Wimp_PollIdle", mask%, block%, TIME+200 TO reason%

to return to their program after a specific time interval (in this example, in two seconds time). This code would not function as expected if another program issued the TIME = ... statement. In particular if TIME is reset to zero, then certain tasks might start polling continuously, absorbing a lot of desktop time.

You can avoid this mess by replacing TIME references with calls to a FNtime routine which returns the real monotonic time:

DEF FNtime
LOCAL time%
SYS "OS_ReadMonotonicTime" TO time%
= time%

Omitting THEN statements

 

BASIC allows you to omit the THEN statement on a single-line IF structure. If you want to make sure your program will be handled correctly when passed through a BASIC compressor program, you must always use the THEN statement. This is because expressions such as 'IF x<>0 E% = 1' can get compressed to 'IFx<>0E%=1'. This will confuse the interpreter, as it will think that the '0E' is a number.

Correct use of shift operators (>>, << and >>>)

 

Using the >> operator to shift down the topmost bit (or something which includes the topmost bit) of a word will not work, as it is a signed operator. Instead use the >>> operator which will shift down unsigned. (There is no equivalent <<< operator.)

Trailing spaces

 

Beware of trailing spaces after THEN statements. For example:

IF a=5 THEN <-- trailing space here
  b=7
ENDIF

This will set the value of variable b to 7 irrespective of the value of a.

LOCAL variables

 

If you use a LOCAL statement in a procedure or function, then don't assume that the variables you specify will be created and initialised for you, because there's no guarantee that it will. This is true even if your variables exist before the procedure or function is called - you will still need to initialise them.


Cunning Stunts :-)
Assigning a variable dynamically

 

It's actually possible to assign a variable from a procedure by doing something like PROCassign("greeting$", "hello"). The trick is in judicious use of the EVAL and RETURN statements.

PROCassign("greeting$", "hello")
END

DEF PROCassign(a$,b$)
unused = EVAL("FNassign2(" + a$ + "," + CHR$34 + b$ + CHR$34 + ")")
ENDPROC

DEF FNassign2(RETURN a$, b$)
a$ = b$
= 0

Obviously, this version will only work for strings, but it can be adapted for other data types.

Dynamic STRING$() buffers

 

The STRING$(<number>,<string>) statement can be used to create temporary buffers on the fly, for example when calling a SWI which needs a string buffer for output. However, their maximum length is 255 which is one less than the 256 character buffer you usually need. For example:

DEF FNmessage(msg$)
LOCAL b%, l%
SYS "MessageTrans_Lookup", msg%, msg$, STRING$(255,"*"), 256 TO ,, b%, l%
b%?l% = 13%
= $b%

Note: This can be a bit tricky at times, you really have to read whatever has been put in the buffer immediately after the call, otherwise it may get overwritten.


Further on from BASIC
Use of ARM

 

The natural way to code a block memory movement procedure in BASIC is to write a FOR loop with a load/store pair inside. However, this is a slow way to do it. Using a small ARM code block copier, it will go zillions of times faster*.

Although assembly can be assembled at run-time. However, if the code is getting quite lengthy it's best to store it in a file and load it in when your program starts up.

* Approximate figure. ;-)

Note: assembler area limit checking, exported routines from CALL.

Detecting the OS version

 

The function INKEY(-256) returns the operating system version identifier - a unique value of which is returned for each release of the host operating system. This value is not related to the '3.11', '3.71', and so on, version numbers as it has to retain compatibility with the identifiers from other Acorn OS's.

This isn't specifically a BASIC feature, as all the interpreter really does is return the value available from calling SWI OS_Byte 129,0,&FF. The identifiers are:

Operating System Identifier
BBC Master MOS 3.2 &FD
Arthur 1.20 &A0
RISC OS 2.00 &A1
RISC OS 2.01 &A2
RISC OS 3.00 &A3
RISC OS 3.10 and 3.11 &A4
RISC OS 3.50 &A5
RISC OS 3.60 &A6
RISC OS 3.70 &A7
RISC OS 3.80 &A8

Using newer OS calls

 

Using the more recent versions of system calls is likely to provide performance increases and enhanced functionality. For example:

IF wimp%>320 THEN
  SYS "Wimp_ResizeIcon"...
ELSE
  SYS "Wimp_DeleteIcon"...
  misc%!4 = win% : misc%!16 = x% : ...
  SYS "Wimp_CreateIcon"...
ENDIF

Another example is the RISC OS 3.6 sprite plot calls which can now plot using a simple ordered dither, if you specify a bit in R0.


Thanks

  Thanks to those people who have made suggestions and provided information towards this page. If you have any feedback on this page, it would be gratefully accepted.

Index - Hello - New! - Software - BASIC
Notes - Good & Bad - Sticks - Links - Copyright
DaveSpace