[Description] [Contact]
[Description]

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:

  • Write in a single, plain text file, a complete BASIC application composed of different pieces of code, graphics and data; i.e., separate global variables, data and graphics from the main code while still keeping them in just one file. This allows the programmer to write clearer, cleaner and easier to maintain ZX programs.
  • Produce some parts of the tape that may be very inefficient to compute by the ZX Spectrum (e.g., data) before running the program, generating them at PC speed.
  • Include externally generated -PC- data as part of the final tape, just by using LOAD in the corresponding sections of the .tab file.
  • Relieve the main BASIC program from the responsibility of generating data or variables during execution, thus gaining program memory space.
  • Optimize the main BASIC program before running; e.g., both ZXBasicus and the Nutria-reboot emulator can delete from the .tab BASIC program parts all empty and REM statements, rename numeric variables to shorten their names automatically, and many more (see example below), all of this seamlessly to the programmer.

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:

  • Emulated at PC speed with:
    zxbasicus -r -i test_tabfile.tab --stopatend
  • Converted to conventional .tap with:
    zxbasicus -f --tab2tap -i test_tabfile.tab
  • Examined as a series of items with:
    zxbasicus -f --showtab -i test_tabfile.tab
  • Directly loaded as any other tape into the Nutria-reboot emulator.

!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
[Contact]

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.