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:
- Argument names - for code readability
- Argument types - for correctness checking
- Passing registers - where each argument is expected on input
The return value is currently declared in the (declare (asm-func none)) block.
Calling Conventions
The system follows these conventions:
| Register | ||
|---|---|---|
| AF/HL, DE, BC | Object pointer | For class methods |
| IY | Return value | Function 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:
- Via active table:
vec3::new— table can be anywhere - Via vtable in type:
vec3::vtable::new— explicit path specification - Direct call:
vec3::new— without a table, if the function is statically known
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.
h2w
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.