Traditional Macros
I didn’t touch the VM and only modified the compiler. During macro expansion and definition, I added and referenced entries in the macro table. Although the namespace is different from top-level variables, I think this is fine… So now I can freely extend things like let and begin as follows. When I have time, I’d like to add other macro systems too.
(define-macro let
(lambda (binds . bodies)
(cons (append (list 'lambda (map (lambda (x) (car x)) binds))
bodies)
(map (lambda (x) (cadr x)) binds))))
(let ((a 10)) (+ a 1))
Making Bytecode Cooler
The previous bytecode was like the one below, with a structure that chains the next instruction. While it’s suitable for handling in scheme, I want to write the VM in C later, so I’d like it to be more linear.
(frame (halt) (constant 2 (argument (constant 1 (argument (close 0 (refer-local 1 (return 2)) (apply)))))))
I changed it to something like this. Now it should be easy to write an interface with C.
0 frame 9 Create a frame with return address 9
1 constant 2 Load constant 2 into accumulator
2 argument Push onto stack
3 constant 1 Load constant 1 into accumulator
4 argument Push onto stack
5 close 0 7 8 Load closure object with body from address 7 to 8 into accumulator
6 apply Apply that closure object
7 refer-local 1 Load the second local variable
8 return 2 Remove 2 local variables from register and restore
9 halt End
This work was quite difficult. The problem was closures. As long as closures aren’t bound to top-level variables, the bytecode corresponding to that closure exists, so you can just put #(start_address end_address) in the closure object. However, when bound to a top-level variable, even if you know the start and end addresses at call time, you can’t call it because the bytecode itself doesn’t exist. Let me think about various things
- Built-in function (+,-,…) closures need to have their instruction code placed elsewhere
- Also, need to be careful when defining closures with define
- If you dump all the instruction code of the body into RAM when generating a closure and just write the address, it should work
- No, moving closures that don’t need to be bound to top-level to RAM is costly
- Okay, let’s only move the necessary closures to RAM
- Necessary means when bound to top-level with set! or define
(However, I’m arbitrarily calling the area of instruction code emitted by the compiler ROM, and the area of instruction code added later by the VM itself RAM)
There’s still more to think about
- close, test, frame contain instruction addresses (branch destinations, return addresses, etc.), so those addresses need to be properly converted. - I was careless at first - This could be solved by writing recursively
- What about continuations? - Continuations are essentially closure objects containing stack information, so they should be fine - They were fine
I rewrote the conversion program and VM code. It’s good that we don’t waste RAM unnecessarily. The VM became simpler than before. It turned out like this.
scheme code -----> After macro expansion -----> 3imp VM code -----> Linear code -----> Execute on VM
macro.scm compile.scm linear.scm vm.scm
Macro table Top-level table