17

On pages 150 to 154 of William Tang's (1982) Spectrum Machine Language for the Absolute Beginner, there are these lines of code. (Note GOTO 9000 is the first non-REM statement in the program).

9000 REM 
9010 REM initialisation
9020 LET ze= PI - PI: LET on = PI / PI: LET tw = on+on: LET tr = on+tw: LET fr = tw+tw: LET qk = 256: LET mr = 2020 : LET ln = 200

So following through the first five variables, ze represents 0 (zero), on 1 (one), tw 2 (two), tr 3 (three) and fr 4 (four). These variables are then (mostly consistently) used throughout the program where the numbers one to four are needed, though at least once on+tw is used for 3.

Note that 0 and 1 are derived by calculations based on π!

My question is: Why did the programmer choose to do this? I can think of two possible reasons, though there are almost certainly more.

  1. Obfuscation in the interests of copyright protection.
  2. Making sure a novice programmer types in the whole program from beginning to end before any part of it works. (As I said above, line 9000 is called straight away, and the program finishes at line 9180).

To avoid complete speculation, but aware that we are unlikely to get the author's express opinion, it would be good to hear about common practice / people who did explain this practice in the era of typed program listings, or to see a provable performance benefit from code like this.

  • 3
    I don't have a reference to make this a proper answer, but here's my vague recollection of something I read once: it's a speed thing. PI is a keyword, so it is tokenized when the program is entered. At runtime, the execution of that token consists of making a copy of a floating point constant that's somewhere in the BASIC ROM. Numeric literals like 0 or 1 have no tokenization, so they have to be converted from their source code representation to floating point values at runtime, which makes them slower. (Slower even than a subtraction or a division? I don't know.) – user5152 Mar 10 '18 at 0:04
  • Oh, here's probably where I read that: retrocomputing.stackexchange.com/questions/4808/… – user5152 Mar 10 '18 at 0:06
  • @WumpusQ.Wumbley You beat me by seconds! – Leo B. Mar 10 '18 at 0:06
  • Need someone to confirm that the C64 answer also applies to Spectrum. And surely the division must slow it down quite a bit (does the divide operator check first for special case like numerator = denominator?) – user5152 Mar 10 '18 at 0:11
  • Thats all right and true - for Microsoft BASIC. Sinclair BASIC is already a one step ahead by storing the FP representation of a constant in addition to it's ASCII representation. The trick with Sinclair BASIC isn't about speed but code size. – Raffzahn Mar 10 '18 at 0:39
36

These tricks are usually done to increase speed or reduce space. For most (especially Microsoft) BASIC, constants are stored within a tokenized line as ASCII (as entered), and converted to a floating point number every time they are evaluated. This is a time consuming process. Assigning the number once to a variable to be used thereafter will skip this part and save quite some time.

Sinclair (Floating Point) BASIC is better than that. While it also includes the number the same way (*1), it is followed by the token 0x0E (*2), and the next 5 bytes storing the constant in floating point format. Sinclair BASIC does now only need to read past the ASCII until the 0x0E and take the FP without the need of conversion - so speed is not an issue.

But looking at the encoding shows that every time a constant is used it will take at least 7 bytes of program memory:

  • 1 Byte for the ASCII representation (more for numbers outside the integers 0..9)

  • 1 byte 0x0E

  • 5 byte floating point representation

A variable name is stored within the program as its ASCII representation. So a two letter name (like on, tw, ...) occupies just two bytes (*3). Thus any occurrence of on is just two bytes instead of seven. That's five bytes saved each time on is used.

Of course, this comes with an offset of assigning the variable, which is 5 bytes plus the expression used. So with LET ze=0 that would be 12 bytes, meaning that already using the variable three times will save space.

Using ze=PI-PI saves even further, as this takes only 3 bytes, so the second usages of ze will yield saved program space. Similar for all the other calculations. It's all about avoiding constants and replacing them by shorter encoded calculations.

In fact, these optimizations also offer some hints about the age of a program, or at least the time its programmer learned the tricks. PI-PI was eventually the first discovered and used a lot - until someone came up with the idea of NOT PI which also results in zero, but needs one byte less. SGN PI gives one. Two is a bit more complex, but on+on is as good as SGN PI+SGN PI (both 5 bytes) - later INT EXP SGN PI with only 4 bytes was discovered. Three again is just INT PI, and so on.

The origin of these very Sinclair BASIC specific tricks starts with the ZX81 (the same as the ZX80, but with Floating Point BASIC). Here program space was scarily rare, thus every byte saved was worth the extra effort - much more than later on with the 'huge' RAM the Spectrum offered (*4).

Use of these tricks is a good hint that a programmer started out on a ZX81.

So I'd say that guy did start on a ZX81, and the above program was written early on - and just moved to the Spectrum later.


*1 - Well, not as entered, but as if it had been listed after being converted to float, and back to printable.

*2 - While this structure was introduced with the ZX80 FP BASIC/ZX81, 0x7E was used there. When creating Spectrum BASIC it was changed to 0x0E to allow its character set to be (mostly) ASCII compatible.

*3 - It's a bit more complex, so dig into BASIC tokenization and variable list handling if you really want to know the fine print.

*4 - Then again, we all know that there is never sufficient RAM.

  • 1
    There is an even shorter version for 0: Instead of LET ze=NOT PI, you can do LET ze=BIN – mcleod_ideafix Mar 10 '18 at 0:56
  • Then please, fix the 0x7E. It's 0x0E – mcleod_ideafix Mar 10 '18 at 0:58
  • 1
    Very sure about that – mcleod_ideafix Mar 10 '18 at 1:01
  • @mcleod_ideafix Ok, done, edit if you find more problems. I'm deleting the comments related to our edit clash – Raffzahn Mar 10 '18 at 1:16
  • 2
    I think the fact that the code was in a book that was published in October 1982 is a good hint that it started its life on a ZX81. Book publishing is a notoriously slow process - even for technical publishers - and writing a book including development and testing its code, editing, proofreading, printing and distributing it within 5 months seems unlikely, so the code in the book (or at least the majority of it) was almost certainly written prior to the spectrum's release date and then just had small fixes applied afterwards. – Jules Mar 10 '18 at 1:52
15

The answer by Raffzahn is very good, except that I disagree that ZX80/81 background is all that important and I also feel he missed one important trick.

I personally know most of these tricks from studying BASIC loaders for ZX Spectrum games. You see, yes, Spectrum has more memory, but when the machine code program is loaded, it was absolutely not uncommon to set up the stack at 24576-1 or 24320-1 or even 24064-1 (this was especially true with Multiface snapshots repackaged as hacked versions of games). With BASIC memory on unexpanded Spectrum 48K starting at 23755, this leaves you with no more RAM to Spectrum BASIC than the amount of RAM you have on ZX81.

So, many loaders started with a code like this:

10 INK NOT PI: PAPER NOT PI: BORDER NOT PI: CLEAR VAL "24063"

Raffzahn already explained NOT PI. VAL "number" is another very common approach. Indeed, by default a number with X digits is stored in ZX BASIC using X+1+5 bytes. Number 24063 would normally occupy 5+1+5=11 bytes, whereas seemingly inefficient VAL "24063" only takes 3+5=8 bytes.

VAL works great for single digit numbers too. VAL "2" is also 4 bytes and much clearer than ugly INT EXP SGN PI. There is absolutely no need to use any combinations longer than 4 bytes (so SGN PI + SGN PI is interesting, but ultimately useless).

One last bit of probably irrelevant trivia. One can also create binary-edited basic programs, which work but do not confirm to what can be inputted using BASIC line editor. Using this, any number (floating point included) can be stored as exactly 1+1+5 bytes, i.e. there is technically no need to spend more than 7 bytes per constant. So, even though CLEAR VAL "24063" is easier to type in, the most size-efficient loaders would typically have instead command CLEAR 0, stored as

DB #FD,'0',#0E,0,0,#FF,#5D,0 ; the number occupies 7 bytes only

This works because the actual ASCII representation of a number is only used by the editor; the interpreter only cares about binary representation after #0E.

Update

In fact, even a cursory look through the book indicates that the authors are working with 16K ZX Spectrum in mind, which actually makes a lot of sense for 1982. For example, on p.9 they discuss "Nevertheless, such assemblers typically require 6K of memory, and are therefore of limited use on 16K machine. The Spectrum display takes up 7K of memory, and after loading the assembler you may have only 4K of memory left for you assembly language program."

Even more relevant to the programme "EZ-Code" listed on pages 150 to 154 (which is a kind of machine code editor in BASIC) is the discussion on p.145 of the assumed memory map. "Your program module must not be greater than 800 bytes or more than 200 instructions. You cannot load the final program below memory 31499 (in order not to wipe-off the EZ-Code program)". Clearly, the authors are working under assumption here that there may be no memory above 32768.

Thus, the memory is quite tight because they are assuming 16K Spectrum. Hence, the likely need to economize space in their BASIC code.

  • Please note, I never said that a programmer using these tricks must have come from a ZX80/81. It's just a good hint, as RAM was way more a premoutm than on later machines. Also it wasn't ment as a list of all tricks. For example, there are at least 4 different ways to express One with just two bytes. Similar for Zero. – Raffzahn Mar 11 '18 at 22:11
  • Yes, I overstated your point in my first sentence. Will edit now to fix it. As for the list of tricks, it is not so much the total coverage, it is the lack of VAL "n" and simultaneous discussion of things like SGN PO+SGN PI that I tried to address. – introspec Mar 13 '18 at 0:32
  • Sure, but again, it's not a complete list, just a 1, 2, 3 example to touch the issue. I could have added like a dozend other encodings I remember, but that's not the point. It's not about making a guide about whats best or how to do it, but answer why such strange code was used. After all, we should strive to answer question, not essays about certain computers - that's something for free form forum tak, isn't it? – Raffzahn Mar 13 '18 at 1:06
  • "it would be good to hear about common practice". So I tried. – introspec Mar 13 '18 at 1:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.