wip: stack frames

This commit is contained in:
2026-08-28 11:39:37 -06:00
parent 49292d5d01
commit c0a44c89b4
6 changed files with 349 additions and 117 deletions
+88
View File
@@ -0,0 +1,88 @@
* example
#+begin_src scheme
(letrec ((fac (λ (n)
(if (zero? n)
1
(* n (fac (- n 1)))))))
(fac 3))
#+end_src
#+begin_src scheme
(λ (ktail0)
(letrec ((fac
(λ (n ktail1)
(zero?
n
(κ (x0)
(if x0
(continue ktail1 1)
(- n 1
(κ (x1)
(fac x1
(κ (x2)
(* n x2 ktail1)))))))))))
(fac 3)))
#+end_src
#+begin_example
n ktail1
| |
| | x0
| | |
| | ^
| |
| | x1
| | |
| | ^
| |
| | x2
| | |
^ ^ ^
#+end_example
#+begin_src scheme
(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 (pop! %_) ; [ $fac-c0 n $fac ktail1 ]
(pop! %_) ; [ n $fac ktail1 ]
(pop! %_) ; [ $fac ktail1 ]
(push! 1) ; [ ktail1 ]
(return 1)) ; [ 1 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.
)
#+end_src
+68
View File
@@ -0,0 +1,68 @@
* 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.
* scratchpad
** Scheme source
#+begin_src scheme
(* 2 (call/cc
(λ (cc)
(begin (cc 6)
3))))
#+end_src
** CPS
#+begin_src scheme
(λ (ktail0)
(letrec ((with-cc
(λ (cc ktail1)
(letrec ((k0 (κ (_)
(continue ktail1 3))))
(cc 6 k0)))))
(prim (call/cc with-cc)
(κ (x1)
(prim (* 2 x1)
(κ (x2) (continue ktail0 x2)))))))
#+end_src
** stack VM
#+begin_src scheme
;; (call n) expects `n' values on the stack as arguments. then the
;; procedure is expected at index `n', and the return continuation
;; should be at `n+1'.
(define $k0
(pop! %_) ; [ _ ret ]
(push! 3) ; [ ret ]
(tail-call 1) ; [ 3 ret ]
)
(define $with-cc
(pop! %cc) ; [ cc ret ]
(push! $k0) ; [ ret ]
(push! %cc) ; [ $k0 ret ]
(push! 6) ; [ cc $k0 ret ]
;; call a procedure with one argument.
(call 1) ; [ 6 cc $k0 ret ]
)
(define $main
(pop! %ktail0) ; [ ret ]
(prim %x1 (call/cc $with-cc)) ; []
(prim %x2 (* 2 %x1)) ; []
(push! %ktail0) ; []
(push! %x2) ; [ ret ]
(tail-call 1) ; [ %x2 ret ]
)
#+end_src