YARV Frame Layout

This document is an introduction to what happens on the VM stack as the VM services calls. The code holds the ultimate truth for this subject, so beware that this document can become stale.

We’ll walk through the following program, with explanation at selected points in execution and abridged disassembly listings:

deffoo(x,y)z =x.casecmp(y)endfoo(:one,:two)

First, after arguments are evaluated and right before thesend tofoo:

┌────────────┐  putself                       │    :two    │  putobject :one            0x2 ├────────────┤  putobject :two                │    :one    │► send <:foo, argc:2>       0x1 ├────────────┤  leave                         │    self    │                            0x0 └────────────┘

Theput* instructions have pushed 3 items onto the stack. It’s now time to add a new control frame forfoo. The following is the shape of the stack after one instruction infoo:

cfp->sp=0x8 at this point.                           0x8 ┌────────────┐◄──Stack space for temporaries                               │    :one    │   live above the environment.                           0x7 ├────────────┤  getlocal      x@0            │ < flags  > │   foo's rb_control_frame_t► getlocal      y@1        0x6 ├────────────┤◄──has cfp->ep=0x6  send <:casecmp, argc:1>      │ <no block> │  dup                      0x5 ├────────────┤  The flags, block, and CME triple  setlocal      z@2            │ <CME: foo> │  (VM_ENV_DATA_SIZE) form an  leave                    0x4 ├────────────┤  environment. They can be used to                               │   z (nil)  │  figure out what local variables                           0x3 ├────────────┤  are below them.                               │    :two    │                           0x2 ├────────────┤  Notice how the arguments, now                               │    :one    │  locals, never moved. This layout                           0x1 ├────────────┤  allows for argument transfer                               │    self    │  without copying.                           0x0 └────────────┘

Given that locals have lower address thancfp->ep, it makes sense then thatgetlocal ininsns.def hasval = *(vm_get_ep(GET_EP(), level) - idx);. When accessing variables in the immediate scope, wherelevel=0, it’s essentiallyval = cfp->ep[-idx];.

Note that this EP-relative index has a different basis the index that comes after “@” in disassembly listings. The “@” index is relative to the 0th local (x in this case).

Q&A

Q: It seems that the receiver is always at an offset relative to EP, like locals. Couldn’t we use EP to access it instead of usingcfp->self?

A: Not all calls put theself in the callee on the stack. Two examples areProc#call, where the receiver is theProc object, butself inside the callee isProc#receiver, andyield, where the receiver isn’t pushed onto the stack before the arguments.

Q: Why havecfp->ep when it seems that everything is belowcfp->sp?

A: In the example,cfp->ep points to the stack, but it can also point to theGC heap. Blocks can capture and evacuate their environment to the heap.