Files
gyehoek-hs/doc/make-vm-stackier.org
T
msyds 7085ef0685
build / build (push) Failing after 1m26s
return, pushcall
2026-08-29 07:25:06 -06:00

3.1 KiB

rationale?

previously, the VM's stack was used for storing local variables across blocks; a Scheme procedure was split into several low-level routines (one for the procedure itself and one for each continuation), and the stack was used as a communication channel for these separate routines. in contrast, registers were local to each routine. this aligns with Wasm's model of functions pretty well, with Wasm locals acting as the VM's registers, and a global mutable stack serving as fallback.

this worked quite well until it became time to implement call/cc.

we are considering making the following alterations to the VM:

  • explicitly segment the stack into frames.
  • passing procedures and return addresses on the stack.
  • new instructions:

    • (tail-call /n/)
    • (call /n/)
    • (load /r/ /n/)
    • (return /n/)

scratchpad

  (letrec ((fac (λ (n)
                (if (zero? n)
                    1
                    (* n (fac (- n 1)))))))
  (fac 3))
  (λ (ktail0)
  (letrec ((fac
            (λ (n ktail1)
              (zero?
               n
               (κ (x0)
                  (if x0
                      (continue ktail1 1)
                      (- n 1
                         (κ (x1)
                            (fac x1
                                 (κ (x2)
                                    (* n x2 ktail1)))))))))))
    (fac 3)))
n ktail1
| |
| | x0
| | |
| | ^
| |
| | x1
| | |
| | ^
| | 
| | x2
| | |
^ ^ ^
  (define $fac-c0
    (pop! %x0 0) ; [ x0 $fac-c0 n $fac ktail1 ]
    (if %x0 ; [ $fac-c0 n $fac ktail1 ]
        ;; every variable but `ktail1' is dead so we pop them all.
        ;; this probably means that `if' should take two continuations
        ;; rather than two blocks.
        (then (push! 1) ; [ $fac-c0 n $fac ktail1 ]
              (return 1)) ; [ 1 $fac-c0 n $fac ktail1 ]
        (else (load %n 1) ; [ $fac-c0 n $fac ktail1 ]
              (prim %x1 (- %n 1)) ; [ $fac-c0 n $fac ktail1 ]
              (push! $fac-c1) ; [ $fac-c0 n $fac ktail1 ]
              (push! $fac) ; [ $fac-c1 $fac-c0 n $fac ktail1 ]
              (push! %x1) ; [ $fac $fac-c1 $fac-c0 n $fac ktail1 ]
              (call 1) ; [ x1 $fac $fac-c1 $fac-c0 n $fac ktail1 ]
              )))

  (define $fac-c1
    (pop! %x2) ; [ x2 $fac-c1 $fac-c0 n $fac ktail1 ]
    (load %n 3) ; [ $fac-c1 $fac-c0 n $fac ktail1 ]
    (prim %x3 (* %n %x2))
    (push! %x3) ; [ $fac-c1 $fac-c0 n $fac ktail1 ]
    (return 1) ; [ x3 $fac-c1 $fac-c0 n $fac ktail1 ]
    )

  (define $fac
    (load %ktail1 2) ; [ n $fac ktail1 ]
    (load %n 0) ; [ n $fac ktail1 ]
    (push! $fac-c0) ; [ n $fac ktail1 ]
    (push! $zero?) ; [ $fac-c0 n $fac ktail1 ]
    (push! %n) ; [ $zero? $fac-c0 n $fac ktail1 ]
    (call 1) ; [ n $zero? $fac-c0 n $fac ktail1 ]
    )

  (define $start
    (push! $fac) ; [ $start ktail0 ]
    (push! 3) ; [ $fac $start ktail0 ]
    (tail-call 1) ; [ 3 $fac $start ktail0 ]
    ;; ↑ `tail-call' knows how to dispose of the caller's stack frame.
    )