The Procedural Tape (.tab) is a ZX Spectrum tape format that completely specifies the content of a tape by coding all its parts (program, code, screens, and array variables) in a single Sinclair BASIC source file. In this way, they generalize plain text BASIC source files (e.g., .bas, that can only contain the code for one program) to a series of sections all written in plain text BASIC source code, each one in charge of generating one item of a tape.
Some of the sections will be tokenized in order to be executed later on, when the tape is loaded, in an emulator or a real ZX, while others will be executed directly in the PC to produce the corresponding parts of the final tape. You can use BASIC code in these latter parts that will be run at the PC speed to produce the resulting tape item, or load binary data from the PC to form those items.
A specific software tool (such as ZXBasicus) can convert the .tab into a conventional .tap by processing all parts; also, the original .tab file can be directly loaded in an emulator that can process the format on the fly, as it is the case with Nutria-reboot.
The .tab format is a powerful way to:
The following video illustrates the main possibilities of procedural tapes to write complete BASIC applications for the ZX Spectrum with the aid of a simple text editor and an emulator that supports .tab files, in this case Nutria-reboot:
The .tab format is very easy to understand to anyone that has worked with Sinclair BASIC and also to parse by any tool. Below this you can see a completely documented example (download it from here) that can be either:
zxbasicus -r -i test_tabfile.tab --stopatend
zxbasicus -f --tab2tap -i test_tabfile.tab
zxbasicus -f --showtab -i test_tabfile.tab
!comment[]
This is a procedural tape (a .tab file). It contains plain text source code
(BASIC) that is processed to generate a conventional tape (.tap file) with
a number of items, one per section of the .tab file.
With the ZXBasicus tool (https://jafma.net/software/zxbasicus) you can list
the content of .tab files, analyze their main BASIC program, convert them
to .tap and even running them at the speed of the PC. For running the
BASIC code of the sections, ZXBasicus will use a 'full' interpreter, as
described in its own web page, but working silently, in the sense that no
graphical display will be shown to the user nor user input be possible.
With the Nutria emulator (https://jafma.net/software/nutria) you can load
.tab files like any conventional .tap or .tzx file, and their content will
be processed at loading time in the same 'full'-silent fashion.
Every section opening/closing keyword must start with `!` and
placed at the beginning (column 0) of a line; otherwise it will not be
recognized.
All sections begin with a keyword and some parameters enclosed within `[`
and `]`, with no spaces between keyword and brackets and only commas (again,
no spaces) to separate the parameters.
Comment sections like this one have no parameters. Comment sections do not
produce any item in the tape.
!!end
!comment[]
All sections body except comments use Sinclair BASIC, either to generate
the tokens of the main BASIC program (!prog[] sections) or to generate
main program variables (!progvars[]), array variables to load from the tape
(!dimvar[]), binary data in memory (!code[]) or screens (!screen[]).
Within the body of any section except in !prog[], you can use
'LOAD "filename_without_limitations_of_zx" CODE' to load binary files from
the current directory of the PC. You can use 'SAVE "..." CODE' as well if
you wish to save some data to the PC, or even LPRINT to write to the PC
console while the procedural tape is being processed. The files saved with
SAVE will have binary content and you must explicit the start and length of
the code you are loading/saving. You cannot use MERGE or do tape operations
of kind DATA.
There is a poweful feature related to LOAD in section bodies (with the
exception of !prog[]): it allows you to load .PNG graphics into memory
in that sections. For that, you must use a special syntax in the string name
of the file to load:
LOAD "screen:x0:y0:filename.png" SCREEN$ ->
Load a PNG file, convert it to
ZX graphic from top-left corner
(X0,Y0) chars for the size of a
ZX screen and put it onto the ZX
display memory. X0 is the
horizontal coord (width) and Y0
the vertical one (height).
If X0:Y0 are not present, take
from (0,0).
The conversion tries to preserve
the bitmap and colors as much
as possible within the
limitations of the ZX display.
LOAD "bitmap:x0:y0:w:h:nw:nh:filename.png" CODE start,length ->
Load non-coloured bitmap blocks
from the given file starting at
top-left corner X0,Y0 chars
(8x8 pixel cells in png file;
at 0,0 if not present).
Each bitmap block has W x H
chars, and there is a NW x NH
char region to read from file.
During reading, pixels with
gray level >= 127 are "1".
The result is loaded into
START until all blocks are
loaded (no error is issued if
LENGTH does not coincide with
the total of blocks).
It loads data linearly on the
blocks, i.e., from left to
right, from top to bottom,
encoding into each target byte
8 bits of the bitmap.
LOAD "attrmap:x0:y0:w:h:nw:nh:filename.png" CODE start,length ->
Analogous to the previous one
but takes the attributes only.
!!end
!comment[]
The prog[] sections are BASIC source programs that are tokenized to tape,
not executed in any way.
The delimiting keywords `!prog[name,line]` ... `!!end` are optional. The
parameters in `!prog[]` are:
- `name` a name enclosed within double quotes (can have escaped chars)
with 10 characters or less to use as the name of the item in the tape
for that program.
- `line` a natural number indicating the line to start the program.
- `` a number of one-letter, case-sensitive flags indicating
optimizations to be done on the code (made in the same order as written
in the flags). This option must exist; the possible flags are:
`0` -> no optimization (no other flag may be included in the
parameter)
`a` -> the same as `A` but without th `#` optimization.
`A` -> apply all optimizations below in this pre-defined order:
`c` -> substitute all uses of numeric variables that are assigned
only once with LET in this very section by their assigned
values.
`x` -> evaluate all parts of expressions that can be evaluated
before runtime. For better results, group all the constants
in one end of each expression, separating them as much as
possible from variables.
[NOTE: if any of c,x is present, the ones that are indicated
are repeated until no change is produced in the code]
`w` -> delete all unused variables
`v` -> shorten all numeric scalar variables that have long names
`r` -> delete all REM statements
`e` -> delete all empty statements
`u` -> delete all unreachable statements
`s` -> delete all unuseful statements (currently, just IF without
anything after THEN)
`f` -> delete all unused user functions (FN)
`l` -> merge lines to reduce the cost of searching for statements
`#` -> not an optimization but a tokenization mode instead:
obfuscate all literal numbers for reducing their size in
the binary tape and, in case they are negative, to
accelerate their evaluation as well through the
embedding of the negation operation in the very number.
In the case that no delimiting keywords exist, the name of the program will
be blank and the start line will be 0.
The escaped characters must be in the following format (case-sensitive):
'\\' (the escape mark, escaped), '\n' (kNL), '\p' (kPound),
or '\c' (kCopy).
'\(command[ args])' -> print control codes, where command is 'left',
'right','up','down','del','ink','paper','flash', 'bright',
'inverse','over','at','tab','number' and may have args separated
by commas, e.g., '\(left)', '\(at 2,3)', '\(tab 7,0)',
'\(number -2.3)' [this inserts a hidden number].
'\gph(**...)' -> graphic blocks, where '**' identifies each block
square with '.', ''', ':' or ' ').
'\udg(*...)' -> UDGs, where each '*' is an uppercase letter from
'A' to 'U'.
'\_****_' -> a token with uppercase name ****, e.g., '\_PRINT_'. No
inner space can appear within the token, i.e., it must be
'\_GOTO_', never '\_GO TO_'.
'\[**...]' -> hexadecimal block, where each '**' is an
hexadecimal code (2 digits even when the code needs only 1).
'\**' -> hexadecimal byte (2 digits even when the byte needs only
1).
!!end
!prog["\(paper 4)sin\(paper 7)",10,A]
10 load "" data a$()
15 rem *** Draw a sine slowly
20 border 1: print "slow version" : ink 0
30 let constant1 = 2 : let constant2 = pi
31 let constant3 = constant1 * constant2
32 let ind = 1 : poke 23674,0 : poke 23673,0 : poke 23672, 0
33 let t0 = peek 23672 + 256 * peek 23673 + 65536 * peek 23674
34 for f = 0 to constant3 step 0.01
35 plot int(f/constant3*255.0), int(sin(f) * 175.0 / 2.0 + 175.0 / 2.0)
36 let ind = ind + 1: next f
37 let t1 = peek 23672 + 256 * peek 23673 + 65536 * peek 23674
38 print "Time: "; (t1-t0)*0.020; " secs"
39 : : rem *** Draw a sine fast
40 beep 0.5,1 : print ink 2; "fast version"
50 poke 23674,0 : poke 23673,0 : poke 23672, 0
51 let t0 = peek 23672 + 256 * peek 23673 + 65536 * peek 23674 : over 1
52 for f = 1 to kSizeOfArray : plot s(1,f), s(2,f) : next f
53 let t1 = peek 23672 + 256 * peek 23673 + 65536 * peek 23674
54 print "Time: "; (t1-t0)*0.020; " secs"
55 rem *** draw again fast
60 beep 0.5,2
70 over 0 : for f = 1 to kSizeOfArray : plot s(1,f), s(2,f) : next f
75 rem *** Show characters in loaded matrix
80 for f = 32 to 255 : print f,a$(f-31) : next f
85 rem *** Load a generated screen
90 load "" screen$
95 rem *** Load generated UDGs
100 load "" code
101 for f = code("\udg(A)") to code("\udg(U)"): print chr$(f); : next f
!!end
!comment[]
The progvars[] sections define variables that are appended to the
tokenized program of a previous program section, i.e., both form just one
item in the target .tap file.
They are within mandatory delimiting keywords `!progvars[mode,list]` ...
`!!end`.
Their content is assumed to be BASIC code that is run in the PC to create
the vars.
The parameters in `!progvars[]` are:
- `mode` mode of inclusion of vars generated by the section into the
previous BASIC program; one of these:
- `all` to include all vars. No more arguments in `!progvars`.
- `include` to include the list of vars written after the mode.
- `exclude` to include all but the list of vars after the mode.
- `list` a list of var names separated by commas referring to the vars
that will be considered by modes `include` and `exclude`.
This section must follow a prog[] section after discarding any comment[]
sections in between.
!!end
!progvars[include,kSizeOfArray,s()]
10 let inc = 0.01: let ini = 0 : let fin = 2 * PI
11 let kSizeOfArray = int((fin - ini) / inc) + 1
20 dim s(2,kSizeOfArray) : let ind = 1
30 for f = ini to fin step inc
40 let s(1,ind) = int(f/(2*PI)*255.0)
41 let s(2,ind) = int(sin(f) * 175.0 / 2.0 + 175.0 / 2.0)
42 let ind = ind + 1
50 next f
!!end
!comment[]
The dimvar[] section define a table to be loaded with 'LOAD "" DATA'.
This section must be enclosed within mandatory delimiting keywords
`!dimvar[name,varname()]` ... `!!end`, where `name` is the name of the
item in the tape and `varname` is the one-letter name of the variable, that
must be created in BASIC when the body of this section is executed.
!!end
!dimvar["mydimvar",a$()]
10 dim a$(256-32,16)
20 for f = 32 to 255: let a$(f-31) = chr$(f) : next f
!!end
!comment[]
The screen[] section defines a screen, within mandatory delimiting keywords
`!screen[name]` ... `!!end`, where `name` is the name of the screen item
in the tape.
The screen must be created in the screen memory of the ZX by the BASIC code
in the body.
!!end
!screen["myscreen"]
10 load "screen:pantalla_escalada.png" screen$
12 rem for f = 16384 to 22527
20 rem poke f, f - (int(f/32) * 32)
30 rem next f
35 rem rem load "attrmap:0:0:32:24:1:1:pantalla_escalada.png" code 22528,768
40 rem for f = 22528 to 23295
50 rem poke f, f - (int(f/32) * 32) + (255-32)
60 rem next f
!!end
!comment[]
The binary[] sections define binary code, within mandatory delimiting
keywords `!binary[name,start,len]` ... `!!end`, where `name` is the name
of the code item in the tape, `start` the starting address in ZX memory and
`len` the number of bytes.
The code must be filled in the ZX memory by the BASIC code in the body.
Note that you can use 'LOAD "filename_without_limitations_of_zx" CODE' to
load binary files from the current directory. You can use 'SAVE "..." CODE'
as well; these files will have binary content and you must explicit the
start and length of the code you are loading/saving. You cannot use MERGE
or do tape operations of kind DATA or program.
!!end
!binary["myudgs",65368,168]
1 load "bitmap:0:2:1:1:21:1:commodorePET_font.png" CODE 65368,168
5 rem rem load "these_random_udgs.bin" code 65368,168 : stop
8 rem rem TO GENERATE THE BINARY DATA FILE NEEDED IN LINE 5:
10 rem for f = 65368 to 65535 : poke f, int(rnd * 256) : next f
20 rem save "these_random_udgs.bin" code 65368,168
!!end
!comment[]
The append[] sections take existing .tap files and append their elements
to the procedural tape.
They cannot contain any code.
!!end
!append["LaIslaDeRuth.tap"]
!!end
!comment[]
You can also use the !include[""] section to include other
procedural tapes. That section must be empty and has no other parameter.
!!end
The development of the Procedural Tape format and its support in ZXBasicus and Nutria-reboot has been done in Oct-Dec 2022 by me (Juan-Antonio Fernández-Madrigal).
If you are interested in this work, you can contact me in "software" (remove quotes) at jafma.net.