TechNiche banner

NextBASIC Profile device driver

The Stochastic Profiler tells you which lines of a NextBASIC program are taking the most time, without requiring any modifications to the program. It is implemented as a Next device driver so it can use the processor hardware interrupt in a system-friendly way, and access banks of memory to keep track of every program line without using any memory allocated to BASIC. Six new DRIVER commands and variants allow the profile data to be read or reset, in bulk or line by line, as the program runs or afterwards.

This page explains how to use the profiler, how it works, what it can teach you about the performance of short and long programs, and how to develop similar utilities on your Next. Latest update: 28th May 2021. Follow the link at the end of this page if you wish to provide feedback, or contact Simon via the Spectrum Next dev and BASIC pages on Facebook.

Downloads

Profile.drv - the driver
ProfileDemo.bas - simple example
The only file you NEED to use the driver to profile your NextBASIC programs is Profile.drv, the first one above. The demo is a short example, discussed later in this document, which you can use to try out the driver and compare its measurements on your Next with my own (made with NextZXOS 2.06, the official release in May 2021). If you're impatient, skip over to the installation heading below.

If you want to know how the driver works, modify it or use it as a basis for writing a new driver that can be entirely built on Next itself, download the following files:

Driver source and the current Z80 version of Zeus

Profile.god - source for Zeus
Profile.bas - the Driver compiler
ZEUSx.bin - Patched native Zeus assembler

Profile.god is the assembler source, tokenised to save memory and speed assembly. You won't need ZEUSx.bin, the latest Next-friendlier version of the assembler bundled with NextZXOS in the Tools/Dev/Zeus folder, if you've already got that version. The originally-bundled Zeus.bin is buggy unless used in 48K personality. Profile.bas includes the simple IDE for Zeus (ZEUSy.bas) modified for this particular project, and a compiler (also customised) which builds the driver initialisation structure around the assembler output and writes out the complete driver as a headerless file ready to be installed.

ASCII versions of the tokenised sources

Profile.txt - compiler listing
ProfileDemo.txt - example listing
Profile.asm - ASCII Z80 source

The .txt versions are plain-text listings of the corresponding .bas files. Profile.asm corresponds to the .god source for Zeus, and can be loaded into any other Z80 assembler that supports ASCII source files. It uses standard Z80 mnemonics but you may need to tweak some of the directives like ENT and ORG to suit eccentric assemblers. For more about Zeus on Next, visit my Zeus-support page.

All these files are freely available via unencrypted http so they can be downloaded directly from my server to your Next using Remy Sharp's .http command! You can download that from GitHub.


Installation

The driver can be added to the system with the usual command:

.install profile.drv

Beware that the current release version of this dot command may crash NextZXOS if the driver file is not found or BASIC memory is almost full; 2K free is safe, PRINT 65536-USR 7962 as usual to check how many bytes are free. If you mistakenly try to install the driver twice, an explanatory message appears.

To remove the driver, to load another or a fresh version, type:

.uninstall profile.drv

This will release the memory banks and remove the interrupt. NextZXOS currently supports up to four drivers at any one time.

Overhead

There is a slight, barely measurable, overhead of running the driver. Between 40 and 50 instructions are executed by the driver each frame while a BASIC program is running, but only four or eight when it's stopped or running a direct command. To put these two thousand instructions a second in context, the Z80n can execute between one and five million (for a byte memory copy or byte addition respectively) when running at 28 MHz.

Caveats

 • Drivers interrupt the BASIC program at the same point in a display frame - the interval while the screen image is being generated, from top to bottom, tens of times per second - so if your software is frame-locked and very fast (looping every frame, without fail) it may always and only count the line of the PAUSE 1 or SPRITE CONTINUE statement! This is a consequence of the way ZX interrupts are tied to display refresh timing. Temporarily remove the frame lock for fair proportional counts for every line.

 • The driver counts lines not statements. Split lines if you want to know which particular statements take the most time.

 • The profile counts are not exact and may vary between runs. Large counts are statistically reliable, small one might vary by a few units either way. Often an unexpectedly low count for one line will be balanced by a higher count for the one after, e.g. for a GO SUB/RETURN pair or the start and end of a loop. Run the test several times for greater confidence, or let the driver add up the times for several runs. If you add lines or variables to the program this will affect the results. They are also likely to vary whenever NextBASIC is updated - that's one reason this tool will remain useful even if certain aspects are sped-up in future.

 • Commands that disable interrupts, like BEEP and LOAD, will not record the time they spend monopolising the processor.

 • If a program ends with STOP, that line will remain 'current' for the interpreter but the driver will stop counting till it's running again. But if BASIC halts with an error, the driver may infer that the error-line is still running and accumulate an ever-increasing count for that line, until a new valid line is seen when the program is CONTINUEd or re-started.

 • The profiler tracks only line numbers, not bank or statement numbers (which would take a lot more memory). A version that profiles only code in a specific bank has been requested by Matt Langley and may follow if people pester me (politely) about it.


Implementation

The Profile device driver is an example for Spectrum Next system developers as much as it is a utility for NextBASIC programmers. This part of the documentation is for them. Read on to 'Driver Commands' if you're currently only interested in profiling your own BASIC programs.

Crucially for those who value the immediacy of native development, this example shows how to build a NextZXOS driver without needing another computer. Source included for the native Zeus assembler (in Tools/Dev/Zeus on the distribution SD) plus a NextBASIC 'Driver compiler' which acts as a simple IDE for Zeus, removing the need for a second assembler source file by converting the relocatable .DRY PLUS3DOS file output from Zeus into a .DRV configured to bind to the system, accessing interrupts and extra memory. It provides a standard and consistent way to index allocated RAM banks in Next or DivMMC address space, beyond the default 64K.

The Profile driver allocates three 8K banks as it needs 19,998 bytes to count how many times each of the possible 9,999 program line numbers is active when the Frame interrupt occurs (48 to 60 times a second depending on display settings). Less than half of the third bank is used. The last 4.5K is cleared but then untouched. Extra pages can be added, e.g. to store statement numbers, bigger counts or higher line numbers. If line numbers up to 16,383 were to be permitted it's only necessary to replace 3 with 4 in the source and increase the constant 9999 used to validate line numbers passed as the third DRIVER parameter.

Each count uses two bytes and is clamped to a maximum of 65,535, which would be reached in about 20 minutes if the same line was running all that time. This is the highest value supported by the DRIVER command (which uses Z80 register pairs to pass parameters in and out).

Driver Commands

Three driver commands are supported, 0, 1 and 2. Command 0 has three variants. The driver uses CODE "%" as its identifying character, allocated to it by Garry Lancaster. The relevance of percentages is obvious. You can put 37 in place of the constant CODE "%", and if you know ASCII off by heart I hardly needed to tell you that. I had to look it up, so I've used the character representation in examples here.

DRIVER CODE "%",0,0 clears the counts for all line numbers (1..9999)

DRIVER CODE "%",0,N clears the count for line N only (useful if it reaches the limit 65535 in a long soak test or after an error).

DRIVER CODE "%",0,N TO C reads the count for line N to C, then resets that count for line N to zero. This is useful when collating profiles on the fly. C is 0 if the line has not been running when the frame interrupt occurs since the last DRIVER 0 command.

DRIVER CODE "%",1,N TO C reads the count for line N without changing it. This is the usual way to read one count for a specific line number as a program runs. To convert it to a percentage, divide it into the total count for all lines and multiply by 100.

DRIVER CODE "%",2,S TO B returns the 8K bank number allocated to one of the three slots in which the profiler stashes line counts. Slots are numbered from 1 to 3. Slot 1 contains counts, as consecutive 16-bit words, for line numbers 8192 upwards. Slot 2 tracks lines 4096 to 8191, and Slot 3 records counts for lower line numbers, 1..4095, to suit 8K Next MMU bank addressing. The first word in slot 3, which would correspond to the invalid line number 0, is reserved to keep the addressing simple.

If you are in a hurry, load ProfileDemo.bas next. This auto-runs a short benchmark program and then displays the number of times each line was executing when the profiler cut in. Run it a few times, from line 4000, to see how the counts vary between runs, depending on exactly when BASIC is interrupted.

Add lines or variables at the start to see how this affects results. The speed of BASIC depends more on context than the source lines themselves. You'll have to read the rest of this page to fully understand why, and the consequences. One of them is the need for, and existence of, this driver.

There are some other examples and useful commands in the Profile.bas and ProfileDemo.bas sources. PROC ClearCounts() is a wrapper for DRIVER CODE "%",0,0. PROC ShowCounts(%m) displays the non-zero counts and corresponding line numbers for lines up to the parameter value, so PROC ShowCounts(4999) will exclude lines from 5000 upwards from its summary. Since the fastest lines are at the start of any program, they're the ones we're often most interested in. Alter the range of the FOR loop you can home in on any contiguous group of lines, such as those of a particular PROC. Add a STEP if you've renumbered your program with LINE.

PROC TellCounts() in Profile.bas uses DRIVER CODE "%",2 to find the driver's data banks, each with room for thousands of counts. Thus it can scan all 9999 line counts, wherever they are banked in memory, and print them to the screen. It may take a few seconds, but illustrates a way to read thousands of values without multiple DRIVER calls. As it has to make the count pages accessible to BASIC and the Z80, it needs 8K above the CLEAR address.

TellCounts() normally uses the top 8K but can use any MMU bank above RAMTOP, conveniently if you already have graphics or similar data up there. By converting the 8K bank number into a 16K page the BANK DPEEK function could be used to read individual values or blocks of them without encroaching directly upon the precious 64K directly addressable by the Z80n.

Higher slot numbers could be used if more banks were allocated. Otherwise this command reads progressively earlier bytes of the 512 reserved in system memory for each driver, e.g. DRIVER CODE "%",2,512 TO B sets B to the first byte of the DRIVER's object code, usually a JR opcode. This illustrates, as a side effect of the necessary bank lookup, a way to PEEK within the 2K of protected memory reserved for drivers.

Processor registers are scarce resources on a Z80 and the driver saves register addresses in BC and previous MMU setup in DE, swapping the old DE into HL in PAGER, the routine which prepares for memory management. All routines that call PAGER must exit via the label DONE, which restores not only the bank mapping but the contents of the NextReg address register, in case the interrupt comes just after a program sets this and before it gets a chance to read the corresponding register value. DEFB is used to invoke the Z80n extra instruction NEXTREGA, which was added after Zeus tokens were all defined. The rest is all standard 1970s Z80 assembly language (in fact almost all of it would run on the earlier 8080).

Slot numbers cannot correspond directly to banks because the .install command takes account of banks already in use before finding fresh ones requested for the driver. The banks might be in any order, or non-contiguous. The table labelled HIGHSTREET (the once-traditional place to find banks), at the end of the driver binary object, is used in the FINDL Z80n subroutine to look up bank numbers by slot,  and access the required 8K page and offset within it.

This scheme can be extended to any number of banks; the current approach supports up to 128K, indexed via HL as 65,536 words; larger records or a 24-bit index allow more than 16 banks to be linearly addressed that way, e.g. a megabyte of 16-byte records in 128 banks. Use power-of-two record sizes, or allocate padding at the end of banks, to avoid records spanning multiple banks.

Tutorial

The following section shows how to use the driver to profile large and small programs, and the effects of context on the speed of BASIC. The exact counts on your Next will vary, depending on the display output setup (I used VGA 4) and CPU speed (I picked 28 MHz) and ZXOS version (2.06) but the proportions should correspond, unless you've added lines or variables to the test program.

ProfileDemo.bas starts with the source of Kilobaud/PCW Benchmark 7, commonly used to compare 1980s BASIC interpreters, often wildly misleading when extrapolated to 'real programs' rather than tiny contrived examples. The demo autostart at line 4000 and assumes the driver has already been loaded (it is unaffected by NEW or LOAD). To help get consistent results it clears the profile counts with PROC ClearCounts(), clears the variables with CLEAR (so only the ones used in the test affect the timings) then calls the test with GO SUB to the first line, as adding any lines before would also slow things down.

Benchmark 7 - no earlier lines or variables

Program line                   Profile count
100 REM Benchmark 7                  0
110 PRINT "S"                        0
120 LET K=0                          0
130 DIM M(5)                         0
140 LET K=K+1                       14
150 LET A=K/2*3+4-5                 68
160 GO SUB 230                      16
170 FOR L=1 TO 5                    55
180 LET M(L)=A                     124
190 NEXT L                         138
200 IF K<1000 THEN GO TO 140        30
210 PRINT "E"                        0
220 STOP                             0
230 RETURN                           3


The next thing I tried, without adding any lines or variables, was to convert the benchmark to use NextBASIC integer variables. These are preallocated to fixed memory locations so the lines that refer to variable names run faster, even in this small example, and calculations run faster because only positive whole numbers are considered. Here's Benchmark 7%:

Program line                     Count
100 REM Benchmark 7                0
110 PRINT "S"                      0
120 LET %K=0                       0
130 DIM M(5)                       0
140 LET %K=%K+1                   11
150 LET %A=%K/2*3+4-5             19
160 GO SUB %230                   19
170 FOR %L=1 TO 5                 30
180 LET %M(%L)=%A                 64
190 NEXT %L                       62
200 IF %K<1000 THEN GO TO %140    25
210 PRINT "E"                      0
220 STOP                           0
230 RETURN                         3

As expected, the arithmetic lines 150 and 160 run almost three times faster, as they have half the precision (16-bit rather than 32 for digits and 8 for the binary place) don't bother with decimals, exponents, overflow checking or even negative numbers. But if speed is of the essence and your values are whole numbers 0..65535, who cares?

Note that the result of line 150 will be very different for odd values of %K, as integer division discards the remainder.

The three statements of the FOR loop run twice as fast with integer variables - and far more so as the program grows in lines or number of variables assigned earlier - but the range, step and array size is limited (the DIM is ignored as %M is preallocated, but left in to avoid changing the line lookup overhead) and jumping out of the loop will cause a memory leak. Integers can't be used to slice strings either (in 2.06, at least) so there are many cases when the snags outweigh the benefits.

The other surprise is that the GO SUB and GO TO lines take more time in the integer version, even though the number of lines to search is unchanged. The more digits after % in the line number, the greater this overhead, but even single-digit line numbers 1..9 are retarded; without the per cent prefix the line number is preconverted to 16-bit binary, hidden in six invisible bytes within the stored program, much closer to the way BASIC needs it. In this case, the digits are just for humans to read, skipped over while the program runs.

Nextramon

Nextramon is one of the larger BASIC examples on the Next distribution, a symbolic disassembler for data and code in memory. I wrote this originally in 1982 while learning the Spectrum keyboard layout, and it runs to over 500 lines using a mixture of old and new BASIC features, and extensive string handling which standard benchmarks ignore.

The only change I made to profile Nextramon was to add the line DRIVER CODE "%",0,0 at the start, to reset all the line counters. I then let it run briefly, disassembling the first 22 lines of the ROM with default settings (no printer output, numbers in hex) then broke in and ran a loop of DRIVER CODE "%",1 commands to get the counts for all lines which had been profiled at least ten times:

FOR %i=40 to 7530 : DRIVER CODE "%",1,%i to %d
  IF %d>9 THEN PRINT %i;" ";%d : NEXT %i


The driver fingered a dozen lines: though many others had single-digit counts, I wanted to isolate the busiest ones. Five of them were in the initialisation routines, reading and storing the 704 Z80n instruction names or the shorter sets of floating-point calculator tokens and system variable offsets. I ignored those as they only run once at the start of the program.

The highest count by far, and the least interesting result, was for line 400. This is the INPUT waiting for the next command. There's no point speeding that up! The next top score was for line 600, which uses GO SUB sub to get a new line of code or data (sub is set to suit the information required) and PRINT l$ to display it. Regardless of line and variable search overheads, the PRINT was the bottleneck. Character output is slow regardless of LAYER, especially if it causes the screen to scroll.

The other hot lines were 1820, 2003, 4080, 5025 and 5030. All of these perform string handling, a known bottleneck; many of them use character operations to make strings which NextBASIC 2.07 is scheduled to handle efficiently with new functions for string editing and base conversions. 2003 discards trailing spaces at the end of a string, while 4080 builds up the output line from several smaller strings. The last two lines highlit are in PROC WORD(), which converts the low and high bytes (in lines 5025 and 5030 respectively) of a 16-bit value into a five-character hexadecimal string.

Pending a HEX$ function to do this for us, I listed the lines and analysed why they take so much time:

5025 LET ct= INT c/256: LET c$=h$(ct/16+.5): LET ct=ct- INT (ct/16)*16+.5: LET c$=c$+h$(ct)
5030 LET ct=c- INT (c/256)*256: LET c$=c$+h$(ct/16+.5): LET ct=ct- INT (ct/16)*16+.5: LET c$=c$+h$(ct)+"H": ENDPROC

These were counted 29 times in the brief test-run, 11 times for line 5025 and 18 for line 5030. The lines use standard ZX BASIC, but Next's integer extensions can do the same job more than twice as fast, even though they're not yet allowed in string expressions so they need copying back and forth to normal variables, which may still dominate the time taken.

5025 %z=c:w=%z>>12&15+1:x=%z>>8&15+1:y=%z>>4&15+1:
      z=%z&15+1:c$=h$(w)+h$(x)+h$(y)+h$(z)+"H":ENDPROC

This single line has a profile count of 11, the same as just the first original line and less than the second slow line, 5030, which is no longer needed. It runs 2.6 times faster, or (to put it another equivalent way) in 38% of the original time, and is called at least once for every line of output.

It's a useful optimisation and the profiler guided me past the other 500-odd lines to find this relatively low-hanging fruit. The speedup steps partly from the elimination of floating-point arithmetic and also from assembling the result in a single string expression (via four temporary variables) rather than four simpler ones. There's a substantial overhead every time a string variable value changes.

If integer variables were allowed in string expressions, the line could have been trimmed to just:

5025 %z=c:c$=h$(%z>>12&15+1)+h$(%z>>8&15+1)+
      h$(%z>>4&15+1)+h$(%z&15+1)+"H":ENDPROC


Depending on other variable-names in use, this would be substantially faster, though still not as quick as the QL SuperBASIC toolkit equivalent RETURN HEX$(c) - an equivalent of which has been pencilled in to the NextBASIC roadmap. When that's ready I'll revisit Nextramon.

Benchmarks later

To show how line lookups can dominate the execution time of longer programs, and why small synthetic benchmarks like the PCW/Kilobaud ones are highly misleading when applied to non-trivial programs, I renumbered this benchmark routine and copied it to the end of Spectramon, adding 529 lines to be scanned to resolve each GO or NEXT statement.

Program line                   Decimal Integer
8100 REM Benchmark 7              0       0
8110 PRINT "S"                    0       0
8120 LET K=0                      0       0
8130 DIM M(5)                     0       0
8140 LET K=K+1                   13      11
8150 LET A=K/2*3+4-5             71      21
8160 GO SUB 8230                172     170
8170 FOR L=1 TO 5                52      37
8180 LET M(L)=A                 129      59
8190 NEXT L                     762      63
8200 IF K<1000 THEN GO TO 8140  185     177
8210 PRINT "E"                    0       0
8220 STOP                         0       0
8230 RETURN                       2       3

Oddly, RETURN appears to have got a tick faster, but the figures are tiny in either case, too small to be significant, unlike those for GO TO and GO SUB lines which are larger in every case, and take six to eleven times longer (the IF line is doing more than just GO TO) whether integer or decimal arithmetic is used.

This explains why programmers are advised to put the main loop of BASIC games in the first few lines, and skip over them with RUN or GOTO to initialisation routines which run less often and are not time-critical. The Profiler shows how much difference this reordering can make. Note that the actual line number values are irrelevant (except that more digits use more memory or slow down conversion if prefixed by %) - it's purely the number of previous lines that matters, as each must be skipped-over to find any later line.

This is one of the reason multi-statement lines speed up program interpretation, unless you're editing them. The LINE MERGE command, tucked away on page 326 of the Next manual, can be used to join consecutive lines, but be wary of doing this if they have IF and especially ELSE statements in them, because merging those could change the program flow.

THEN and ELSE scan only the current line to work out where to continue from. This limits their usefulness and often forces extra GO TO statements not needed in languages with multiline block structure, like BetaBASIC, SAM BASIC, QL SuperBASIC etc. When NextBASIC gets ENDIF, we'll need to re-profile it (and rewrite a lot of messy code).

Variable declarations

Program lines are not the only things laboriously searched by BASIC, making programs slower as they grow longer. A similar issue applies to variable names and values, though its harder to see where they're defined and in what order.

BASIC keeps a simple list of names and values in the memory area labelled VARS in the system variables, and adds to it whenever it find a variable name it's not seen since the last RUN or CLEAR. Entries are not created when the line is typed in, only when it is executed, and in the order of execution.

Even when only a couple of dozen variables are previously declared, this makes a massive difference to the benchmark timing - as it does in most BASICs, key exceptions being Sinclair SuperBASIC, for the QL, and the BASIC for 8-bit Atari computers. Both of these convert user-defined names - such as variables on the Atari, PROCs and FNs too on a QL - into index numbers so the program can fine their value, or name, without any searching. The name and value tables are created as the program is entered, during syntax checking. On those machines the length of a name, or the number of coexisting variables, has no bearing on program speed. Sadly, ZX BASIC does not work that way; the three variable name references in line 8180 make it almost a third the speed if those names are later in the dictionary:

Program line                    Counts
8100 REM Benchmark 7               0
8110 PRINT "S"                     0
8120 LET K=0                       0
8130 DIM M(5)                      0
8140 LET K=K+1                    47
8150 LET A=K/2*3+4-5             101
8160 GO SUB 8230                 173
8170 FOR L=1 TO 5                 71
8180 LET M(L)=A                  350
8190 NEXT L                      842
8200 IF K<1000 THEN GO TO 8140   198
8210 PRINT "E"                     0
8220 STOP                          0
8230 RETURN                        2

After RUN or CLEAR (with or without a parameter) subsequent variable names are added to the VARS area in the order they are encountered. This determines the order in which they will be checked in subsequent searches.

Single-letter names use varying space to keep track of string, array, FOR index and simple variables, but BASIC skips over them one at a time till it finds a match. First come, first served, every time thereafter.

Multiple-character variable names can only be simple numeric variables in ZX BASIC, but are often used for labels to make line-references more readable, e.g. GO TO menu after LET menu=1000, or more flexible with LET menu=2000 - whatever their uses, they make all access to later names proportionately slower as they are checked, character by character, WHETHER THEY MATCH OR NOT!

Why? This is because ZX BASIC was designed for a 1K computer, and Sinclair Research later denied Stephen Vickers and John Grant's request to refactor this aspect of the ZX-81 version of Nine Tiles' BASIC for the 16/48K Spectrum and its larger ROM. They agreed on hires, colour, lower-case, UDGs and BEEP, though.

We've come a long way since, and NextBASIC is still in development. Whatever changes are made, the profile driver will let you know which lines are taking longest.

Integers later

Having a reputation for thoroughness, I repeated the benchmark test after pre-defining 35 other variables - a mixture of FOR loops, arrays, strings and scalars with long and short names - but without any earlier program lines. The following table shows the consequences for the profile counts:

Program line                    Counts
8100 REM Benchmark 7               0
8110 PRINT "S"                     0
8120 LET K=0                       0
8130 DIM M(5)                      0
8140 LET K=K+1                    60
8150 LET A=K/2*3+4-5             110
8160 GO SUB 8230                  15
8170 FOR L=1 TO 5                 74
8180 LET M(L)=A                  429
8190 NEXT L                      244
8200 IF K<1000 THEN GO TO 8140    49
8210 PRINT "E"                     0
8220 STOP                          0
8230 RETURN                        3

NEXT, GO SUB and GO TO are quick again, being at the start of the listing, but the arithmetic and array lookup lines are even slower than they were when merged with Nextramon, as there are now more predefined names. So the last thing we need to do to understand this fully is find out exactly what those names are, and their order.



Finding variables

ListVars example output If you're trying to make sure your key variables are defined first, you need a way to see all the names known to BASIC in the order in which they'll be searched. Fear not, I've written one. ListVars.bas contains a couple of NextBASIC PROCs to list variable names, array bounds and scalar values.

MERGE this with your own program and run PROC ListVars() whenever you want to know the order in which BASIC has stored variable names. The code is an implementation of the information summarised on pages 282 to 284 of the Next manual.

If short of program memory you may need to CLEAR before you MERGE, or LOAD ListVars, CLEAR and re-SAVE before merging it. This is because ZX BASIC programs contain variables, in order, with values, as well as lines. I've deliberately included a few kilobytes of arrays, loop and other variables in the example, to get you started. The image shows the first few of those. ListVars.txt is the untokenised program listing.

Notice how a FOR loop records five values, not just one. As well as the current value of the variable - shown first but not necessarily the starting value of the loop, which is not stored as it's only used once - it stores the END (after TO) value, STEP (even if defaulted to 1) and the line and statement number following the FOR, so that NEXT knows where to branch back to. After a loop has finished, as for c, m and j in the screenshot, the variable ends up set to the STEP + END value.

Scalar variable values are shown, whether their names are long or short, and string values up to a maximum length of 59 characters (set in line 9600). Array values are not shown, but their dimensions are displayed. It's easy - but verbose - to extend the program to read and show their values as well. PROC FLOAT(A) reads five bytes from address A, which could point to a scalar, array element or FOR-loop control value, and sets global variable I to the corresponding numeric value, either by decoding the floating-point or extracting the 17-bit integer indicated by a dummy decimal exponent of zero.

NextBASIC's integer variables are preallocated to fixed but hidden addresses inside BASIC private memory pages. They all have single-letter names which are indexed rather than found by searching, and therefore not shown.

Click here for Simon N Goodwin's home page and contact information.

Copyright notice

Article copyright © 2021 Simon N Goodwin, programs are released under the CC-BY-NC-SA version 4 Licence (https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode)