Passing Code Pointers as Data in Amstrad CPC BASIC

One of my secret coding pleasures is passing a function as a parameter to a subroutine. Most modern languages have what’s called ‘first class code’. That means that you can assign the address of a function to a variable, store it in an array, and pass it as a parameter to a function. This enables you to write a generic function which calls the passed-in function for some operation, and saves you from having to write multiple, similar, functions for each operation.

It may not be obvious that you can also do this in machine code, and that this strategy was used a number of times by the authors the Amstrad CPC BASIC interpreter. Indeed, the flexibility of assembler means that the code address can be passed to the subroutine in ways which would be impossible in a higher level language. In this article I’ll take a look some of the ways code pointers are used in CPC BASIC. In the second part I’ll look at how the techniques are used to create iterators which call a routine for every item in a data structure.

Utility Routines

To help with this they created a set of routines specifically to call an address in a register.

;; JP (HL)
JP_HL:               
        jp      (hl)      


;; JP (BC)
JP_BC:                   
        push    bc       
        ret            


;; JP (DE)
JP_DE:        
        push    de          
        ret                

Source: https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/Utilities.asm

Z80 assembler has a built in JP (HL) instruction which jumps to an address in the HL register (and would probably be more accurately named JP HL) but no instruction to call an address in a register. When any of these routines is called the return address from that call will act as the return address from the subroutine.

Address Passed in a Register

The first, and most common, technique is to simply put the address in a register. This is used, for example, within various graphics routines where commands use the same parameters but which call different firmware routines. You can see this in the code for PLOT, PLOTR, MOVE, MOVER, DRAW, DRAWR, each of which loads the address of a firmware routine into the BC register and jumps to a common piece of code to parse the parameters and call the firmware.

Here’s an extract showing the PLOT and PLOTR commands.

;; command PLOT
;PLOT <x coordinate>,<y coordinate>[,<masked ink>]

command_PLOT:  
        ld      bc,GRA_PLOT_ABSOLUTE ;firmware function: gra plot absolute
        jr      plotdraw_general_function 

;;==============================================
;; command PLOTR
;PLOTR <x offset>,<y offset>[,<masked ink>]

command_PLOTR: 
        ld      bc,GRA_PLOT_RELATIVE ;firmware function: gra plot relative

;;+-----------------------------------------------------------
;; plot/draw general function
;;reads parameters and calls the address in BC to do the actual function
plotdraw_general_function:  
        push    bc           
        call    eval_two_int_params
        call    next_token_if_prev_is_comma 
        jr      nc,_plotdraw_general_function_6
        cp      $2c               ;',' separating parameters
        call    nz,validate_and_set_graphics_pen 

_plotdraw_general_function_6: 
        call    next_token_if_prev_is_comma 
        jr      nc,_plotdraw_general_function_13
        ld      a,$04     
        call    check_byte_value_in_range ;check value is in range
        push    hl           
        call    SCR_ACCESS        ;firmware function: scr access 
        pop     hl        

_plotdraw_general_function_13:
        ex      (sp),hl   
        push    bc       
        ex      (sp),hl  
        pop     bc             
        call    JP_BC             ;JP (BC)
        pop     hl    
        ret  

Source: https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/Graphics.asm

ENT and ENV commands

A more advanced use of this is in the ENT and ENV commands. These create sound envelopes for tone and volume respectively and use complex but similar parameter lists which can be repeated multiple times.

Here is the parameter list for ENV,

;ENV <envelope number>[,<list of: <envelope section>>]
;Where <envelope section> is <step count>,<step count>,<pause time>
;                           or <hardware envelope>,<envelope period>
;There can be up to five envelope sections
;Envelope number is     1..15
;Step count is          0..127. If zero then set an absolute volume
;Step size is           -128..+127. If <step count> is zero this is the absolute volume setting
;Pause time is          0..255 in 1/100ths of a second where 0=256
;Hardware envelope is   value for register 13
;Envelope period is     value for registers 11 and 12

And that for ENT,

;ENT <envelope number>[,<list of: <envelope section>>]
;Where <envelope section> is <step count>,<step size>,<pause time>
;                         or =<tone period>,<pause time>
;There can be up to 5 envelope sections
;Envelope number is 1..15
;Step count is      0..239
;Step size is       -128..+127
;Pause time is      0..255 in 1/100ths of a second where 0=256
;Tone period is     0..4095

Each command defines a routine to a read a block of parameters. Each reads and processes the envelope number then loads the address of the callback routine into the DE register. They then call a common function which calls the callback routine multiple times until the end of the command or an error.

The full code is too long to publish in full so I’ll leave you to browse the source files at https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/Sound.asm

Data Following Call

An interesting way to pass a parameter to a subroutine is to place it immediately after the call to the subroutine. The subroutine then fetches the return address from the stack, retrieves the parameter, updates the return address and puts it back on the parameter.

Before we get to doing this with code I’ll look at an example of how this is done with data. This routine tests whether the next BASIC token matches the parameter and raises an error of not.

Here’s an example of how that routine is used, by the DEF command. The DEF command (token) must be followed by the FN token to create a DEF FN statement (which creates a user defined function). The first line calls the next_token_if_equals_inline_data_byte routine with the FN token ($e4) on the next line.1

;; command DEF
;DEF FN<function name>[(<formal parameters>)]=<expression>
command_DEF: 
        call    next_token_if_equals_inline_data_byte
        defb $e4                  ;inline token to test "FN"

        ex      de,hl   
        call    get_current_line_number
        ex      de,hl
        ld      a,$0c             ;Invalid direct command error
        jp      nc,raise_error 
        call    parse_and_find_or_create_an_FN 
        ex      de,hl    
        ld      (hl),e
        inc     hl  
        ld      (hl),d 
        ex      de,hl 
        jp      skip_to_end_of_statement ;DATA

Source: https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/DEFFN.asm

And here’s the start of the code itself,

;; next token if equals inline data byte
next_token_if_equals_inline_data_byte:
        ex      (sp),hl           ;get return address from top of stack/save HL
        ld      a,(hl)            ;get byte
        inc     hl                ;increment pointer
        ex      (sp),hl           ;put return address back to stack/restore HL

;;+----------------------------------------------------------
;; next token if value in A
;; A = char to check against
next_token_if_value_in_A:
        cp      (hl) 
        jr      nz,raise_syntax_error_D
...

Source: https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/Execution.asm

Note the use of EX (SP),HL to retrieve the return address. You might expect to see a POP being used, but this is a general utility routine and needs to preserve all the registers (except A and F which are used to return values).

EX (SP),HL swaps the value in the HL register with that on the top of the stack. In a call to this routine the HL contains the current BASIC execution pointer, so the first EX (SP),HL puts this value on the top-of-the stack. HL now contains the return address. The parameter is read into the A register and HL is incremented to get the new return address. EX (SP),HL is then used again to put this new return address back onto the stack and get the execution pointer back into HL.

The next two lines compare the parameter (in A) with the BASIC token pointed to by HL and raise an error if they don’t match.

I’ve left out the remainder of this routine which skips whitespace and sets some flags depending on the token.

Code Following Call

But, of course, we came here to talk about passing code as parameters. Hopefully the previous example has prepared you for this one which uses the code following the call as the parameter!

Several BASIC commands take a text stream number as an optional first parameter. An example of this is the PRINT command

PRINT "This goes to the current stream"
PRINT #1,"This goes to screen window 1"
PRINT #8,"This goes to a file"

We could easily write a subroutine to swap to a stream, and another to swap back. We can then place calls to these functions at the start and end of any commands which need this functionality. But a routine such as the PRINT command has multiple return paths. Using this method means we need jumps to the ‘swap back’ code which make the code longer and more complex.

Instead Locomotive Software chose to create a ‘wrapper’ function. This wrapper function reads and processes the stream number parameter, if present. It then fetches the return address from the stack and calls it. After this call execution will return to the wrapper function. When it returns the wrapper function can swap back to the original stream and do any other clean up necessary.

By now the original return address has been removed from the stack (and called) and the return address is now for the code which called the PRINT (or whatever) routine, so execution returns to this original caller.

Here’s the code for the wrapper function. There’s actually a number of entry points to this depending on what validation needs to be done for the stream number, i.e. whether the stream must be a screen window (used when, eg., setting colours) or can also be a file or printer stream. This code is the core of the functionality.

exec_TOS_on_evalled_stream_and_swap_back:
        call    eval_and_validate_stream_number_if_present
        cp      $08              ;Only streams #0..#7 allowed
        jr      nc,raise_Improper_Argument_error

exec_TOS_on_stream_and_swap_back:
        pop     bc               ;BC=return address

exec_BC_on_stream_and_swap_back:
        call    select_txt_stream
        push    af     
           ;Original stream number
        ld      a,(hl)            ;Next token to execute
        cp      $2c               ;',' - another parameter?
        call    JP_BC             ;JP (BC)
        pop     af                ;Swap back to original stream
        jp      select_txt_stream 

Source: https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/Streams.asm

TOS here refers to top-of-stack. I.e. the return address of the call.

The POP BC retrieves the return address, the call to JP_BC calls it. Note how the next token is read and compared to a comma to save the wrapped function having to test it itself.

The call to select_txt_stream selects the stream in the A register and returns the old stream number in the A register. This is preserved on the stack for the swap back at the end of the routine.

When this routine is invoked it’s operation is so subtle that it’s easy not to notice what’s happening. Here it is as the first line in the PRINT command.

command_PRINT: 
        call    exec_following_on_evalled_stream_and_swap_back

        call    is_next_02     
        jp      c,output_new_line ;No parameters
...

Source: https://github.com/Bread80/Amstrad-CPC-BASIC-Source/blob/main/TextOutput.asm

Summary

That’s the end of this first look into code as data. As mentioned in the intro next time we’ll be looking at how these techniques are used to create iterators which can walk data structures. There’s a number of them in the Amstrad BASIC ROM and they’re all fascinating. But I think I’ve fried your brain enough for one article.

Footnotes

  1. The last line of this routine calls skip_to_end_of_statement which is the routine called for the DATA statement. If the DATA statement is ‘executed’ it just skips over everthing until the next statement. In the DEF FN command, all it cares about is the function name – which it adds to a list of function names, similar to how a variable is initialised. It then skips the remainder of the function definition. The definition is only parsed when (if) it is invoked, which is handled by the FN command,

Leave a Reply

Your email address will not be published. Required fields are marked *