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.
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
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
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
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 ...
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
TOS here refers to top-of-stack. I.e. the return address of the call.
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 ...
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.
- 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,