LD A, (data) ADD A, C ADC A, B LD (data), A RET data: .DB 35Notice that a variable is just a label with some numbers after it, which is pretty much identical to any other label you have in the program. Therefore, any label can be treated as a variable, even if it was never intended to be one.
LD A, 26 LD (Blegh), A RET Blegh: CP 7 RETWhat happens here? The LD (Blegh), A will change the instruction CP 7 to something else entirely. Taking a lower-level perspective, FE07C9 is changed to 1A07C9. This new op code sequence represents
LD A, (DE) ; $1A RLCA ; $07 RET ; $C9The entire operation of the program has been totally changed! The code has modified itself, or less reflexively, self-modifying code.
An arbitrary example like this shows SMC's primary disadvantage: really hard to interpret code. The main purpose of SMC is to extend the processor's limited capabilities. Example: When using an index register, the offset is a constant number. With SMC, that offset can be altered.
IX_Offset .EQU $+2 INC (IX + 0) ; $DD $34 $00Or you can change the target of a JR or DJNZ.
LD A, (IX_Offset) ADD A, 3 LD (IX_Offset), A
LD A, B ADD A, A LD (Jump), A Jump .EQU $+1 JR $00 INC IX ; JR $00 INC IX ; JR $02 INC IX ; JR $04 INC IX ; JR $06 INC IX ; JR $08 INC IX ; JR $0AIt can also be used to save a register faster than the stack:
LD (save_a), A LD (save_hl), HL ; Do some math on A and HL ... save_a .EQU $+1 LD A, $00 save_hl .EQU $+1 LD HL, $0000
Where would this be useful? Assume a scenario wherein a main module jumps to one of about twenty routines. Once any one
of these routines is finished, they should return to a specific address in the main module referenced by the label
Start. This would have to be done by placing a JP
Start at the end of each routine, which would come out to 60 bytes of code.
On the other hand, if the main module were to push the value of Start before jumping to a routine, then the routine would only need a RET to return. This would save 40 bytes of code.
Start: LD HL, Start PUSH HL . . . JP Z, Routine01 . . . JP Z, Routine02 . . . JP C, Routine03 . . . ; This is for exiting the program POP AF ; Remove Start from the stack RET Routine01: ; Do stuff RET ; Go back to Start Routine02: ; Do stuff RET ; Go back to Start Routine03: ; Do stuff RET ; Go back to Start
To construct a LUT, first identify the function's domain (the range of possible input values). For each domain value, calculate the result of the function, format it accordingly, and enter it into the LUT.
Example: a LUT used to calculate sin(x), 0° <= x < 90° (in 8.8 fixed-point format):
LD H, 0 LD L, A LD DE, sine_table ADD HL, DE LD A, (HL) INC HL LD H, (HL) LD L, A RET sine_table: ; The lookup table .DW $0000, $0004, $0009, $000D, $0012, $0016, $001B, $001F, $0024 .DW $0028, $002C, $0031, $0035, $003A, $003E, $0042, $0047, $004B .DW $004F, $0053, $0058, $005C, $0060, $0064, $0068, $006C, $0070 .DW $0074, $0078, $007C, $0080, $0084, $0088, $008B, $008F, $0093 .DW $0096, $009A, $009E, $00A1, $00A5, $00A8, $00AB, $00AF, $00B2 .DW $00B5, $00B8, $00BB, $00BE, $00C1, $00C4, $00C7, $00CA, $00CC .DW $00CF, $00D2, $00D4, $00D7, $00D9, $00DB, $00DE, $00E0, $00E2 .DW $00E4, $00E6, $00E8, $00EA, $00EC, $00ED, $00EF, $00F1, $00F2 .DW $00F3, $00F5, $00F6, $00F7, $00F8, $00F9, $00FA, $00FB, $00FC .DW $00FD, $00FE, $00FE, $00FF, $00FF, $00FF, $0100, $0100, $0100The most prohibitive drawback to using lookup tables is their giant size, but there's nothing that can be done about that (actually, in the case of trigonometry, you could use the symmetry of the sine function to have a LUT with only the entries for one-quarter of a circle).
Here is an example vector table:
VectTbl: .DW ClearScreen .DW PutSprite .DW DrawLine .DW EndPrgm .DW BlackScreenThe elements of the vector table are accessed just as a lookup table
LD H, 0 LD L, A LD HL, HL LD DE, VectTbl ADD HL, DE LD A, (HL) INC HL LD H, (HL) LD L, A JP (HL)
JumpTbl: JP ClearScreen JP PutSprite JP DrawLine JP EndPrgm JP BlackScreenTo call or jump to a routine in the jump table, you use an address of
JumpTbl + 3 * n
CALL JumpTbl + 3 * 2
CALL stratocumulus_routines + 3 * 3This will call $8009 which will then jump to $9436. But, one month later you release StratocumulusOS v1.1 which, owing to a truly brilliant optimization on your part, has a much smaller library size; as well, you store the routines somewhere else (but the jump table is still stored at $8000):
If you don't mind the size difference, you can also use a jump table as a replacement for a vector table. In this case, you have to multiply A by three (since each jump is three bytes in size).
LD A, 40 Loop: INC (HL) INC HL DEC A JP NZ, LoopSay that this piece of code were to be relocated to $9900. The problem is that when assembled, TASM will determine the value of Loop in the JP as relative to $9D95, when it should be relative to $9900. You have to handle situations like this. It's easy: first you subtract $9D95 from the value of Loop, giving the offset from $9D95. Then you add the offset to $9900 to get the correct address. This is the fixed code:
LD A, 40 Loop: INC (HL) INC HL DEC A JP NZ, Loop - $9D95 + $9900If there need to be a lot of relocations you would do well to make some macros.