mirror of
https://github.com/anoma/juvix.git
synced 2024-09-11 08:15:41 +03:00
Cairo VM imposes restrictions on memory access order stricter than described in the documentation, which necessitates changing the compilation concept for local variables. Summary ------------- To ensure that memory is accessed sequentially at all times, we divide instructions into basic blocks. Within each basic block, the `ap` offset (i.e. how much `ap` increased since the beginning of the block) is known at each instruction, which allows to statically associate `fp` offsets to local variables while still generating only sequential assignments to `[ap]` with increasing `ap`. When the `ap` offset can no longer be statically determined for new local variables (e.g. due to an intervening recursive call), we switch to the next basic block by calling it with the `call` instruction. The arguments of the basic block call are the variables live at the beginning of the called block. Note that the `fp` offsets of "old" variables are still statically determined even after the current `ap` offset becomes unknown -- the arbitrary increase of `ap` does not influence the previous variable associations. Hence, we can transfer the needed local variables to the next basic block. Example ----------- The JuvixReg function ``` function f(integer) : integer { tmp[0] = add arg[0] 1; tmp[1] = call g(tmp[0]); tmp[2] = add tmp[1] arg[0]; tmp[3] = mul tmp[2] 2; tmp[4] = call g(tmp[2]); tmp[5] = add tmp[4] tmp[3]; ret tmp[5]; } ``` is compiled to ``` f: -- code for basic block 1 [ap] = [fp - 3] + 1; ap++ -- now [fp] is tmp[0], because fp = ap at function start (ap offset is zero) -- transfer call argument (in this case, could be optimized away) [ap] = [fp]; ap++ call g -- now [ap - 1] contains the result tmp[1] (it is already a call argument now) -- we additionally transfer arg[0] which is live in the next block [ap] = [fp - 3]; ap++ call rel 3 ret nop -- code for basic block 2 -- the above "call rel" jumps here -- [fp - 4] is tmp[1] -- [fp - 3] is arg[0] [ap] = [fp - 4] + [fp - 3]; ap++ -- now [fp] is tmp[2] [ap] = [fp] * 2; ap++ -- now [fp + 1] is tmp[3] [ap] = [fp]; ap++ call g -- now [ap - 1] is tmp[4] [ap] = [fp + 1]; ap++ call rel 3 ret nop -- code for basic block 3 -- [fp - 4] is tmp[4] -- [fp - 3] is tmp[3] [ap] = [fp - 4] + [fp - 3]; ap++ -- now [fp] is tmp[5] -- the next assignment could be optimized away in this case [ap] = [fp]; ap++ ret ``` There are three basic blocks separated by the `call` instructions. In each basic block, we know statically the `ap` offset at each instruction (i.e. how much `ap` increased since the beginning of the block). We can therefore associate the temporary variables with `[fp + k]` for appropriate `k`. At basic block boundaries we transfer live temporary variables as arguments for the call to the next basic block. Checklist ------------ - [x] Divide JuvixReg instructions into [basic blocks](https://en.wikipedia.org/wiki/Basic_block). - [x] Implement liveness analysis for each basic block. - [x] Translate transitions between basic blocks into CASM relative calls with local live variable transfer. - [x] Tests for the translation from JuvixReg to Cairo bytecode executed with the Cairo VM |
||
---|---|---|
.. | ||
negative | ||
positive |