# Locomotive BASIC

Locomotive Basic is a proprietary dialect of the BASIC programming language written by Locomotive Software.
It was modified (many custom features to support the platform) for use on the Amstrad CPC as "Amstrad BASIC" (where it was built-in on ROM).
Later Locomotive BASIC-2 was produced for the IBM PC compatibles platform as a GEM application on the Amstrad PC1512 and 1640 and was a descendant of Mallard BASIC, the interpreter for CP/M supplied with the Amstrad PCW.
There are two versions of Amstrad; BASIC 1.0 which only came with the CPC464 (and had a buggy DEC$ function), and BASIC 1.1 which corrected this and shipped with all other CPCs. BASIC 1.1 was also included in the Amstrad CPC Plus series machines, as part of the included game cartridge.

## Development

Development was based on existing work recently undertaken writing Mallard BASIC for Acorn Computers Z80 addon for the BBC Micro. It is reported to have taken around 12 weeks to enhance the existing code, and was "very influenced" by BBC BASIC, though adding additional functions to do things that would have required assembly language on the BBC.
The "ARNOLD" platform (the CPC464 development codename) was designed to be solid enough to attract the "serious" users Amstrad wanted. Locomotive were engaged to produce the firmware and BASIC for the platform. The mix produced a comprehensive result with technical capabilities almost unheard of in any one contemporary platform - the tidy approach to firmware access with fixed jump blocks, the hypothecation of the firmware into modules, the clever hardware allowing expandability, with multiple pages of RAM and ROM all occupying the same address-space, simultaneously accessible, re-locatable display memory etc. many of which appeared only partially, or not at all on rival platforms.

## Features of the CPC platform

If a programmer is concerned only with writing in a rich BASIC dialect they have an excellent machine on which to ply their skills and there is no reliance or expectation the programmer will ultimately have to resort to machine code or study the function of the hardware to produce good quality software. However, to fully comprehend the ethos behind Amstrad BASIC and push the machine to its limits, it is necessary to understand, at least superficially, the design approach to the machine as a whole.
Fundamentally, the CPC platform consists of clever hardware with a rich firmware to exploit it; compartmentalised into sub-systems with standardised jump tables to access functionality. Amstrad BASIC was written to expose as much of this platform and firmware as possible to the programmer.

### Firmware jump blocks

At a machine level on the CPC platform, application software need not be concerned with code moving about in the actual firmware (e.g. for different versions and re-assembly) so long as it uses the documented jump block addresses. For example, the firmware call TXT_PRINT_CHAR is ubiquitous - but note from below the firmware vectors are different in BASIC 1.0 and BASIC 1.1 ($9400 vs $93FE). In many machines, re-assemblies of the firmware and subsequent moving of routines can cause major headaches, but with the CPC platform so long as software uses the published jump block calls (i.e. $BB5A), software will run on all CPC platforms with no issues.
The firmware jumps have changed between versions but the address for TXT_PRINT_CHAR remains $BB5A.

### The firmware modules

Amstrad BASIC leveraged and exposed the platform being written to take full advantage of the machine, bundling up BASIC code into the requisite calls into the firmware, providing specific commands for functionality rather than reliance on generic *FX or PEEK & POKE statements required to access features on competitor platforms.

## BASIC and version differences

Internal version numbers of the BASIC ROM differ from the accepted, published versions discussed here.
Although not unique, the platform was unusual among contemporaries in that it used the ASCII character set rather than its own idiosyncratic form - an arguable strength as it instantly made the CPC platform a more serious offering, somewhat underpinning Amstrad's marketing claims of it being capable of professional business use.
In common with many other dialects, multiple program statements were separated on a single line using the colon :.
WHILE/WEND is the only loop construct other than FOR/NEXT.
Reserved words (functions, commands, statements etc.) are capitalized in a program by virtue of being tokenized. This reduces program size and accelerates execution. Listing a program then decodes the token to its reserved word which are all in upper case in the token table (it was this method which revealed the presence of DEC$ in BASIC 1.0 - see below). When saving a basic program, the fully tokenized form is saved in a binary format. Alternatively, saving with option "A" creates a fully text compatible ascii file e.g. SAVE "MYPROG",A

### BASIC 1.0

Supplied with the Amstrad CPC464, it was a good release with only a single notable bug, being the DEC$ function (it provides PRINT USING style output but as a string rather than screen output). No time was available to fix this before the software had to go for ROM chip production. Consequently, the function was simply left un-documented and pre-fixed to force a syntax error when encountered. The curious programmer found the word dec$ capitalised in their code indicating it was a recognised reserved word - inexplicable at the time, until the history of the bug and disassemblies of the BASIC ROM became available.
Exploration of the BASIC 1.0 ROM can find the remnants of the function and its crippled token table entry.

### BASIC 1.1

Supplied with the CPC664/6128, besides fixing DEC$, it introduced (among others) the following additional keywords:
COPYCHR$, CURSOR, FILL, FRAME, GRAPHICS PAPER, GRAPHICS PEN and MASK
It also introduced a clumsy but adequate method of trapping disc errors with the DERR function. Errors still had to be trapped in the usual way with ON ERROR GOTO ... but the single "Broken In" error 32 could now be expanded to provide more detail. Examining DERR gives up to ten further indicators. Values returned are the AMSDOS error number plus 128, e.g. 14 is reported as a DERR value of 142 (14+128). Possible errors included the usual file not found, disk full etc. as well as the more specific "142 - The stream is not in a suitable state" and "149 - Disc changed while files were open". DERR is a function of BASIC 1.1 and so was not available on the FDI1 expanded CPC464, with its combination of AMSDOS and BASIC 1.0. Disc errors on the CPC464 were obtainable but relied on PEEKing into the AMSDOS system area, such program being incompatible with CPC664/6128 due to the AMSDOS variable addresses having moved.

## Variables and functions

Variable names must begin with a letter but can have up to 40 character long names made up of A-Z, the digits 0-9 and the . symbol. Names are case-insensitive, but case-as-typed is maintained in program listings.
Three data-types are supported signified by an identifier suffix on the variable name and can be selectively presumed by use of the DEFINT, DEFREAL and DEFSTR directives:
* Strings with type marker $, e.g.MyString$, up to 255 bytes in length
* 2-byte signed integers with type marker %, e.g.My.Integer%, in the range -32768 to +32767
* 5-byte real (floating point) with type marker !, e.g.My99Real!, in the approximate range +/-10E+38 to +/-4E-39 with nine significant digits. Type real is the default for un-typed variable names. All Floating point mathematics is the result of Amstrad BASIC exposing the MATHS_PACK of the firmware (very complete and entirely usable from machine code).
Unlike some dialects, variable names are unique down to type, e.g. S% and S! are separate variables.
Dimensioned variables (arrays) have 0 as their lowest element number(s), and can use square brackets around their elements which helps distinguish them from other parentheses in complex expressions in program code. e.g. A(3) can be written as A[3].
Named single-line functions are supported and follow the type and name convention of variables but pre-fixed with FN. e.g. FNMy.Func!
The address of a variable in memory can be determined with the @ directive, e.g. PRINT @A$. This replaces the VARPTR function found in other dialects of BASIC and is mostly used to pass a variable pointer to machine code routines allowing bi-directional data transfer.
Integer and Real variable values are stored in the variable table (see memory map below). String variable contents are stored on the heap. This allows the aggregation of fragmented memory via Garbage Collection. Consequently, for strings, the variable table held a three byte descriptor made up of the length of the variable and the address in the heap where the actual contents are to be found.

### Two missed opportunities?

BBC Basic uses a clever method for handling some integer variables, which despite claims of its heavy influence on Locomotive BASIC (and by extension Amstrad BASIC), was not replicated in the latter. BBC BASIC has a 54-byte table reserved for the values of the 27 single letter integers @%, A%, B% ... Z% so they do not occupy nor require lookup in the variable table - a simple left-shift and addition is all that is required to determine the value of a given variable. This makes access to the subject variables very fast and considering their ubiquitous use, it is perhaps, surprising this mechanism was not used in Amstrad BASIC.
For strings, the descriptor is three bytes in the variable table, made up of a length descriptor and a further (little-endian) address of the actual payload. Had the value returned by @ been the two byte address in the heap (and the length prefix moved to the heap also) this would have substantially reduced access time by replacing the two address lookups with just one.
Variable table structure for strings.

## Graphics and colour

Handling of graphics and colour was straightforward with commands such as DRAW, PLOT, INK, PAPER, BORDER etc.
With Amstrad BASIC V1.1 (CPC664 onwards), the FILL command permitted painting complex-shaped, bounded areas of the screen and an eight bit mask could be applied DRAW enabling dotted lines etc.
A table giving the numeric codes for the 27 system colors was printed on the casing of the built-in 3" disk drive on the 664 and later machines.

## Text handling

Text moved through the system using a concept of "streams" numbered 0–9, appropriate commands use the hash # symbol to pre-fix the stream number.
Streams 0-7 are text windows. The TEXT_VDU firmware pack allows for the creation of multiple text windows each with their own cursor, dimensions, content, colours etc. An individual window is addressed using its stream number, e.g. PRINT#3,"Hello World!". Omitting this defaults to stream 0 - PRINT"Hello World!" being equivalent to PRINT#0,"Hello World!". Text windows are simple screen areas and can overlap but there is no built in provision to preserve the contents of windows "under" others, thus careful attention is required to avoid corruption. Later, machine code routines became available to intercept the firmware calls (by patching the jump block) and preserve the content of a window just before another was drawn over it, restoring it when the new window closed. This gave the much more useful "Windows" look to which we have grown accustomed - being mindful of the times; "pop-ups" were a novel concept in 1984. When using the text windows, screen scrolling relies on software (as only part of the screen content is moved) rather than using the hardware to scroll the entire screen by adjusting the screen-base offset of the CRT controller in RAM. Two SCREEN_PACK firmware calls are provided for the two forms; $BC4D SCR_HW_ROLL (Scrolls the entire screen up or down by eight pixel rows i.e. one character line) and $BC50 SCR_SW_ROLL (Scrolls part of the screen up or down by eight pixel lines). The latter can result in a pronounced "ripple" when scrolling large areas.
Stream 8 is the centronics parallel port. To send a line of text to the printer one could use the command PRINT#8,"Hello World!". Creating a printed listing was simply a matter of LISTing the program to stream 8, e.g. LIST#8. This flexible method dispenses with the legacy BASIC commands LPRINT and LLIST. When later, serial interfaces became available and opened up access to a greater range of printers and other devices, #8 could be diverted by patching $BD2B MC_PRINT_CHAR in the MACHINE_PACK jumpblock.
Stream 9 is the currently open file (either cassette or disc, input or output) and relevant commands are context sensitive; e.g. it is possible to have both input and output files open simultaneously with PRINT#9 and INPUT#9 routing text accordingly.

## Advanced features

A stand-out feature among almost every other BASIC of the time was a timer-based software interrupt mechanism at 50 ticks per second using the EVERY, AFTER and REMAIN commands (leveraging the KERNEL for its software interrupts). Four timers were available; 0–3, with associated reducing priority - a BASIC imposition not a platform limitation and allowed the programmer to run sections of their BASIC program, asynchronously, after a given delay, repeating if required, e.g. EVERY 50,0 GOSUB <line> produces a repeating one-second call with no further work. When answering an EVERY or AFTER, there is an implicit disabling (see DI below) of all lower-priority interrupts which is automatically cleared by the corresponding RETURN. As with all Interrupt Service Routines (ISR), such GOSUBs should be kept as short as possible, rather setting a flag for further processing of large chunks of program than trying to complete it inside the ISR.
The interrupt ability requires some method to limit its impact when unwanted. Two commands; DI and EI Disable and Enable BASIC interrupts respectively. DI should be used sparingly as it will interfere with all BASIC interrupt driven functionality: timers, colour flashing, keyboard scanning, sound output etc., thus program enclosed by DI and EI should be considered to have the same requirements as ISRs to avoid stunted program flow.
Amstrad BASIC granted a relatively high level of control over the sound chip, an AY-3-8912 with 3 tone channels and 1 noise channel and interrupt driven sound generation with comprehensive co-ordination of the three audio channels and associated BASIC commands: SQ (Sound Queue), ON SQ <channel> GOSUB <line> etc. The same chip was also used on late-model ZX Spectrums, as well as the Atari ST and MSX computers, but none of those had such a complete built-in SOUND command. Many things, from selecting a particular channel or a combination of channels, setting envelopes, volume, pitch, noise, and so on could be done with a single SOUND command, with up to 7 parameters. Granted, especially complex and/or low-level techniques could not be done with BASIC due to their requiring more precise or direct access to the hardware, e.g. especially complex music from trackers (including simulated chords using arpeggios, etc.), the playback of digitally sampled sounds as in the game RoboCop for example, and so on.
All disc, tape and file management managed by BASIC is through the firmware and were adequate for simple file management with commands such as SAVE, LOAD, MERGE, RUN", CAT etc. During this time, it was common for desktop computers to be "boot to BASIC" and that supplied with most low-cost home computers also acted as a simple operating system, managing files and moving program code and data between the machine and storage medium.
Also available was a parametric LOAD command, allowing, for example, to load a file containing "raw" picture data into video memory, causing it to be displayed, with a couple of BASIC instructions. Adding a memory address as parameter to the commands LOAD or SAVE would allow easy loading of raw uncompressed 16 KB screen pictures.

## Machine code support and RSXs

Machine code was well supported (but not to the impressive heights of the BBC Micro's, in-line assembler). There was an easy method of allocating "safe" memory (see below) and once loaded either from disc or cassette or POKEd in, Z80 machine code could be executed with CALL <addr>. CALL supported a method to pass parameters to the machine code using BASIC variables (including strings) using the form CALL <addr>,Q,@X$. Integer variables and integer immediates can be passed directly as their value (ByVAL). All variables, but specifically strings can be passed using the @ directive, which points to the address of the variable descriptor (ByREF). Thus, machine code can pass values directly back to a variable or manipulate its contents making transfer of data between a BASIC program and machine code trivial.
Another method, leveraging the firmware, allowed for named sections of machine code using a feature known as Resident System eXtensions (RSX). Although not a purely BASIC feature, Amstrad BASIC embraces this mechanism of RSXs and the names can be used in the program as if they were BASIC reserved keywords - each being prefixed with the bar | symbol, the remaining syntax being identical to CALL. e.g. |MYRSX,Q,@X$ (note a comma is required between the RSX name and any arguments). The code for the RSXs had a very specific header structure - the RSX names, their entry points and 4 bytes of scratchpad RAM for the firmware to link in and out of the list. A call was made using the KERNEL routine $BCD1 KL_LOG_EXT to initialise the names and they are then available to use throughout the system (by finding each named section using $BCD4 KL_FIND_COMMAND - BASIC does this for | commands). A major advantage of RSXs is not needing to know the address of a piece of machine code (CALL can only access the main 64KB address map, however configured) - the KERNEL returns the parameters necessary to execute the code in question no matter where it resides, in the entire memory map and in all ROM/RAM banks (see "bank switching" below). Code could simply call sideways by searching for the RSX name and from the link structure, the firmware immediately knows which ROM/RAM bank the code occupies. KL_SIDE_CALL, KL_SIDE_PCHL etc. configure the banks and run the machine code. Thus all memory is usable for machine code program. Use of RSXs ensured that machine code in sideways ROMs was fully integrated with BASIC and the disc commands |DIR, |A, |TAPEIN, |ERA etc. all resided in ROM and were used throughout BASIC program code. On the CPC464, disc commands were only added when the FDI1 (floppy disc interface, with its sideways ROM) was plugged in the rear expansion port.
In UK CPC circles at that time, is was popular to pun words that began with "bar" and name RSXs accordingly, e.g. |BQ, |BARASTREISAND, |STARD etc.

## Memory allocation and management

Generally speaking, BASIC programmers do not have to concern themselves too much with the inner workings of their chosen platform to get decent results. Should a programmer wish to ignore them and stick only to BASIC, they are well catered-for with a speedy, rich dialect. For those who want to go beyond and want more control, they are likewise accommodated.
In many contemporary systems, allocating a block of memory seems an after-thought with no "proper" method of achieving it. Innovative methods were adopted by programmers which resulted in some, often, highly cryptic and fiddly methods, such as creating REM statements with the required length and then POKEing the data into known addresses in the BASIC program line - the command LET L=USR 16514 was famous in ZX81 circles. 16514 being the address of the first byte after a REM statement, on the first line of a BASIC program - the only piece of memory guaranteed not to move around or be corrupted with added program lines, garbage collect etc.
Amstrad BASIC program space occupies RAM from $170 extending to somewhere around $A6F0 (varies with BASIC version and other considerations), just under the BASIC system area, AMSDOS area and the high jump blocks - approximately 41KB. Numeric variables and functions etc. are located immediately above the program with the remaining, free memory in between termed "the heap". The contents of string variables are stored in the heap near the very top, with just the string descriptors being stored in similar fashion to numerics in the variable table - just above the program (see Variables and functions above). BASIC program code grew up and strings grew down towards each other in RAM, gradually consuming the heap in between. Amstrad BASIC has a garbage collector which de-fragments the heap to maximise usable memory - as with most computer languages, as strings are created then sliced/deleted, chunks of previously used memory occur between used parts and the heap can become "porous". Notably the BBC Micro did not have garbage collect and manipulation of strings could result in "out of memory" errors simply because a requested block did not exist in one chunk. In common with other dialects, Amstrad BASIC provides the FRE function to return the amount of free heap. The function uses two forms: FRE(numeric), e.g. PRINT FRE(0), returns the size, in bytes, of the largest single block in the heap. The second form, FRE(""), forces a garbage collect and so, returns a true count of free heap. Used areas (mainly strings and temporary structures) are aggregated at the top of the heap with no gaps - data is moved and pointers updated, leaving available heap as a single chunk of memory extending down to the program and variables. This latter form can take some time to run on badly fragmented systems. Should an "Out of memory" error be imminent due to no free heap, Amstrad BASIC performs a garbage collection immediately prior to stopping the program in an attempt to rescue the situation. Consequently, it was not necessary for the programmer to concern themselves overly and FRE is more informational than essential.
Amstrad BASIC provides the function, HIMEM, which returns the last address used by BASIC i.e. the very last address before the system areas. The MEMORY command can be used to adjust this address and through a combination of the two, space can be reserved easily and controllably - a primitive form of malloc. For example, suppose a block of 1KB was required, space could be reserved (with full consent/awareness of BASIC) using MEMORY HIMEM-1024 - adjusting down the maximum extent of BASIC memory use. Machine code, say, could then be safely loaded into and called at HIMEM+1, safe from other processes and accidental relocation/corruption.
The CPC range made provision, through the SCREEN_PACK, for User Defined Graphics (UDG) whereby the 8x8 pixel matrix for a character could be redefined allowing for the creation of special characters. By default, the top 16 characters (128 bytes) were "soft" and if that was sufficient, no further adjustment was required. Following on the ethos of exposing the platform capabilities in BASIC, it passed forward this functionality and the memory for UDG was allocated from the heap (i.e. the more characters, the lower HIMEM). More characters (and thus more RAM) could be allocated with the SYMBOL AFTER <char> command - where char represents the character code from which UDG is available. e.g. SYMBOL AFTER 32 would allow the entire printable ASCII character set to be redefined, with the corresponding reduction in available program space (1784 bytes). The firmware copied the default character matrices from ROM into the newly allocated space, thus defining the character matrix was not essential for clarity. If UDG were not required, memory could be returned to BASIC (resulting in fixed character matrices) with the command SYMBOL AFTER 256.
The simplified default CPC memory map as it appears to BASIC.

## A notable omission - memory bank switching

Although as a BASIC implementation, Amstrad BASIC is fairly complete and goes to great pains to support the CPC platform, one notable omission in all versions is native access to the memory bank switching mechanism used to expand the Z80 memory space; re-using address ranges by overlaying sections of physical ROM or RAM.
Through an arrangement of the custom circuitry, switching pages into one of the four 16KB pages in the Z80 memory map ($0xxx, $4xxx, $8xxx, $Cxxx) was possible. For instance, the top page ($C000) is occupied by both the video RAM, the BASIC ROM and (on disc equipped machines) the AMSDOS ROM with the hardware and firmware providing the necessary control of access. The firmware ROM was bank-switched to the lower 16KB, extra RAM (that beyond the Z80 64KB e.g. the additional 64KB of the CPC6128) and sideways ROMs (containing user machine code or 3rd party software) are mapped to the top page ($C000) - RAM could actually be mapped to any bank. The CRT controller generally mapped to the 16KB video RAM at $C000 in the main Z80 address space with the hardware ensuring the video picture is not interrupted when other banks were switched into the same page. All CPC models supported this fairly advanced (for the time) bank-switching to increase the potential memory storage - even an un-expanded CPC464 uses fully 96KB of ROM and RAM mapped into the Z80 64KB address space.
Memory writes are always applied only to RAM in the main 64KB address space, regardless of switched state, e.g. if the lower ROM ($0-$3FFF) is switched into the memory map, POKE &800,0 will write into RAM regardless. Various configurations of where the switched banks occurred in the memory map were supported by the KERNEL.
The CPC464/664 hardware can address up to 256KB including up to eight 16KB sideways ROMs. One position is used by the BASIC ROM and both the CPC664 and CPC464 expanded with the FDI1, use a further slot for the AMSDOS ROM - leaving six slots. The CPC6128 hardware permitted bank switching up to 4MB with increased space for 16 (32 with a PCB hack) 16KB sideways ROMs. Again, two positions were occupied with BASIC and AMSDOS leaving 14 available for user or 3rd parties. The KERNEL provides several methods of accessing banked memory; some involve a pseudo 24bit address via a vector, extending the HL register pair using the C register and for ROMS only, encoding the high or low ROM switch in the top few bits of a 16-bit address. Still others can retrieve data from a bank as if it were in the mainstream Z80 address map or directly from the main 64KB of RAM regardless of the state of the banks. Kernel routines to access banked memory include KL_FAR_CALL, KL_FAR_JMP, KL_RAM_LAM among others. Prior to "calling sideways", i.e. switching in a bank and jumping to execute machine code at an address there, the kernel assembles structures on the Z80 stack to ensure the safe landing of inbound RETurns from banked memory, restoring the state of the banks and ensuring code execution continues without issue. It was quite normal to have calls between banked ROMs, indeed this happens seamlessly every time BASIC uses AMSDOS (disc commands) as both ROMs occupy the same physical address space.
Despite BASIC not providing support for this kernel functionality, use of banked memory in a BASIC program was certainly possible but required the use of machine code routines to do the switch and take advantage of the result. Banked memory could not be used for BASIC program code.
On the bundled CP/M 2.2 and CP/M+ (CPC6128) discs was a two-part program to add primitive-but-adequate bank-switching commands to BASIC. When run, BANKMAN.BAS reserves 1317 bytes of RAM into which; it loads BANKMAN.BIN, makes a call to that binary and then deletes itself. The latter file contains machine code to initialise a set of RSXs to enable RAM bank switching from BASIC programs. Functionality includes saving and loading screens from banks, copying between banks etc. It was not generally considered practical though as it is necessary to include both BANKMAN files with your own software products and run them at the command prompt before your own program can take advantage of bank switching RAM. To add further hindrance, although not copyrighted, BANKMAN.BAS is saved in protected mode (SAVE"<my_prog>",p) and so is not listable - meaning that you cannot easily determine the method to load the RSXs and so use them in your own programs - to eliminate the two-stage start up.
Besides using banked memory to store screen images and rapidly switch between them, some applications implemented "RAM Discs" but these were not supported under the cassette/AMSDOS firmware, being quite proprietary.

## Contemporary rivals

Unlike the Commodore 64's built in BASIC (Commodore BASIC), which had no dedicated commands for graphics or sound, Amstrad BASIC allowed doing pretty much anything that was within the standard capabilities of the machine. This was not unimportant, as some other machines of the era required programmers to use assembler in order to access the full sound and graphics capabilities of their system. MSX, Sinclair Spectrum and some others offered a similar, more or less complete command set for their sound and graphics capabilities. The only things going clearly beyond BASIC capabilities were the overscan modes used in games and demos, 27-color graphics modes, digital sound playback, and smooth scrolling.
Unlike Sinclair BASIC or Commodore 64 BASIC, which had various keyboard command shortcuts or specialized keys for choosing symbols or colors, Amstrad BASIC keywords were typed in full and the interpreter parsed, recognized and tokenised them. However, there were abbreviations like "?" for "PRINT" and a few shortcuts. Programs could be saved onto Compact Cassette or floppy disk and retrieved as binary or ASCII files.