Close

Methods and Functions in Meta-Assembler: A Practical Guide

A project log for XiAleste

XiAleste Next is an 8-bit home computer, which is compatible with software for the Amstrad CPC6128 released on 13 June 1985

h2wh2w 02/22/2026 at 20:030 Comments

In previous posts, I discussed working with data containers. Now it's time to talk about methods and functions — the key elements of any program.

Method and Function Signatures

In the meta-assembler, every method and function has a strict signature that describes argument types and their passing registers. Let's look at a simple string copy procedure example:

lisp

(defproc test-copy-string ((dest string :reg de) (src string :reg hl))  
  (declare (once) 
  (asm-func none))  ;; Input: DE = dest, HL = src  
  (.label 'loop)  (.ld a (@ hl))      ;; Read character from source  
  (.ld (@ de) a)      ;; Write to destination  
  (.or a)             ;; Check for 0 (end of string)  
  (.ret 'z)           ;; If 0 — exit    
  (.inc hl)           ;; Next in source  
  (.inc de)           ;; Next in destination  
  (.jr 'loop))        ;; Repeat

As seen in the example, the procedure (a function without a class) declares:

The return value is currently declared in the (declare (asm-func none)) block.

Calling Conventions

The system follows these conventions:

Register
AF/HL, DE, BCObject pointerFor class methods
IYReturn valueFunction result

Call Primitives

Several specialized primitives exist for calling procedures and methods:

1. Direct Function Call (dcall)

lisp

(dcall vec3::new int16 int16 _type_)

This call invokes the new function from the vec3 namespace. The compiler checks argument type compatibility (two int16s) and expected result (_type_).

Call variants:

2. Binding Arguments to Registers (rlet)

The special form rlet (or function definition macro) binds argument names to types and registers:

lisp

(rlet ((x int16 hl)        
  (y int16 de)
  (z int16 bc))
  (dcall vec3::set x y z none))

The last line tells the compiler: call vec3::set with arguments x, y, z, which are already in their respective registers. The compiler only checks types and generates code:

lisp

0147:  CD 66 00    |     CALL $0166

3. Object Method Call

Calling an object method requires an object pointer in the IX register. Two approaches are possible:

Sequential call with method loading into IY:

lisp

(obj-method->iy ix vec3::len)  ;; Load method address into IY
(ycall vec3::len vec3 len)      ;; Call method via IY

Full method call cycle (with all overhead):

text

012B:  DD 21 00 80    |     LD IX, $8000      ; object address
012F:  E5             |     PUSH HL           ; save registers
0130:  C5             |     PUSH BC
0131:  DD 6E 00       |     LD L, (IX+0)      ; load vtable address
0134:  DD 66 01       |     LD H, (IX+1)
0137:  01 0F 00       |     LD BC, $000F      ; method offset
013A:  09             |     ADD HL, BC        ; get method address
013B:  E5             |     PUSH HL
013C:  FD E1          |     POP IY            ; transfer to IY
013E:  C1             |     POP BC            ; restore registers
013F:  E1             |     POP HL
0144:  FD E5          |     PUSH IY           ; method address on stack
0146:  C9             |     RET               ; call
0147:                 | [loc-exit]            ; return point

If execution needs to continue after the call, the return address (in this case loc-exit) is additionally pushed onto the stack.

Performance: The Bitter Truth

I cannot avoid mentioning the elephant in the room. The compiler is written in an interpreter and, being quite complex, shows rather low performance. This raises legitimate doubts about its suitability for large projects.

Time will tell whether the system can scale or will require optimization of critical sections. For now, it's a working tool for medium-sized projects where the flexibility of metaprogramming outweighs compilation costs.

To be continued. In following posts: memory management and critical code optimization.

Discussions