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.