mirror of
https://github.com/anoma/juvix.git
synced 2024-11-30 05:42:26 +03:00
ee2f8aefbc
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
269 lines
7.0 KiB
Plaintext
269 lines
7.0 KiB
Plaintext
|
|
function apply_1(*, *) : *;
|
|
function apply_2(*, *, *) : *;
|
|
function apply_3(*, *, *, *) : *;
|
|
function apply_4(*, *, *, *, *) : *;
|
|
function S(*, *, *) : *;
|
|
function K(*, *) : *;
|
|
function I(*) : *;
|
|
function f3(integer, integer, integer) : integer;
|
|
function main() : *;
|
|
|
|
function apply_1(*, *) : * {
|
|
tmp[0] = arg[0];
|
|
tmp[0] = argsnum tmp[0];
|
|
tmp[1] = 1;
|
|
tmp[0] = eq tmp[1] tmp[0];
|
|
br tmp[0] {
|
|
true: {
|
|
tmp[0] = arg[1];
|
|
tmp[1] = arg[0];
|
|
tcall tmp[1] (tmp[0]);
|
|
};
|
|
false: {
|
|
prealloc 7, live: (arg[0], arg[1]);
|
|
tmp[0] = arg[1];
|
|
tmp[1] = arg[0];
|
|
tmp[0] = cextend tmp[1] (tmp[0]);
|
|
ret tmp[0];
|
|
};
|
|
};
|
|
}
|
|
|
|
function apply_2(*, *, *) : * {
|
|
tmp[1] = arg[0];
|
|
tmp[1] = argsnum tmp[1];
|
|
n = tmp[1];
|
|
tmp[1] = n;
|
|
tmp[2] = 2;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[2];
|
|
tmp[2] = arg[1];
|
|
tmp[3] = arg[0];
|
|
tcall tmp[3] (tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
tmp[1] = n;
|
|
tmp[2] = 1;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[2];
|
|
tmp[2] = arg[1];
|
|
tmp[3] = arg[0];
|
|
tmp[2] = call tmp[3] (tmp[2]), live: (tmp[0], tmp[1], arg[0], arg[1], arg[2]);
|
|
tcall apply_1 (tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
prealloc 7, live: (tmp[0], arg[0], arg[1], arg[2]);
|
|
tmp[1] = arg[2];
|
|
tmp[2] = arg[1];
|
|
tmp[3] = arg[0];
|
|
tmp[1] = cextend tmp[3] (tmp[2], tmp[1]);
|
|
ret tmp[1];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|
|
|
|
function apply_3(*, *, *, *) : * {
|
|
tmp[1] = arg[0];
|
|
tmp[1] = argsnum tmp[1];
|
|
n = tmp[1];
|
|
tmp[1] = n;
|
|
tmp[2] = 3;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[3];
|
|
tmp[2] = arg[2];
|
|
tmp[3] = arg[1];
|
|
tmp[4] = arg[0];
|
|
tcall tmp[4] (tmp[3], tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
tmp[1] = n;
|
|
tmp[2] = 2;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[3];
|
|
tmp[2] = arg[2];
|
|
tmp[3] = arg[1];
|
|
tmp[4] = arg[0];
|
|
tmp[2] = call tmp[4] (tmp[3], tmp[2]), live: (tmp[0], tmp[1], arg[0], arg[1], arg[2], arg[3]);
|
|
tcall apply_1 (tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
tmp[1] = n;
|
|
tmp[2] = 1;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[3];
|
|
tmp[2] = arg[2];
|
|
tmp[3] = arg[1];
|
|
tmp[4] = arg[0];
|
|
tmp[3] = call tmp[4] (tmp[3]), live: (tmp[0], tmp[1], tmp[2], arg[0], arg[1], arg[2], arg[3]);
|
|
tcall apply_2 (tmp[3], tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
prealloc 7, live: (tmp[0], arg[0], arg[1], arg[2], arg[3]);
|
|
tmp[1] = arg[3];
|
|
tmp[2] = arg[2];
|
|
tmp[3] = arg[1];
|
|
tmp[4] = arg[0];
|
|
tmp[1] = cextend tmp[4] (tmp[3], tmp[2], tmp[1]);
|
|
ret tmp[1];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|
|
|
|
function apply_4(*, *, *, *, *) : * {
|
|
tmp[1] = arg[0];
|
|
tmp[1] = argsnum tmp[1];
|
|
n = tmp[1];
|
|
tmp[1] = n;
|
|
tmp[2] = 4;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[4];
|
|
tmp[2] = arg[3];
|
|
tmp[3] = arg[2];
|
|
tmp[4] = arg[1];
|
|
tmp[5] = arg[0];
|
|
tcall tmp[5] (tmp[4], tmp[3], tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
tmp[1] = n;
|
|
tmp[2] = 3;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[4];
|
|
tmp[2] = arg[3];
|
|
tmp[3] = arg[2];
|
|
tmp[4] = arg[1];
|
|
tmp[5] = arg[0];
|
|
tmp[2] = call tmp[5] (tmp[4], tmp[3], tmp[2]), live: (tmp[0], tmp[1], arg[0], arg[1], arg[2], arg[3], arg[4]);
|
|
tcall apply_1 (tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
tmp[1] = n;
|
|
tmp[2] = 2;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[4];
|
|
tmp[2] = arg[3];
|
|
tmp[3] = arg[2];
|
|
tmp[4] = arg[1];
|
|
tmp[5] = arg[0];
|
|
tmp[3] = call tmp[5] (tmp[4], tmp[3]), live: (tmp[0], tmp[1], tmp[2], arg[0], arg[1], arg[2], arg[3], arg[4]);
|
|
tcall apply_2 (tmp[3], tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
tmp[1] = n;
|
|
tmp[2] = 1;
|
|
tmp[1] = eq tmp[2] tmp[1];
|
|
br tmp[1] {
|
|
true: {
|
|
tmp[1] = arg[4];
|
|
tmp[2] = arg[3];
|
|
tmp[3] = arg[2];
|
|
tmp[4] = arg[1];
|
|
tmp[5] = arg[0];
|
|
tmp[4] = call tmp[5] (tmp[4]), live: (tmp[0], tmp[1], tmp[2], tmp[3], arg[0], arg[1], arg[2], arg[3], arg[4]);
|
|
tcall apply_3 (tmp[4], tmp[3], tmp[2], tmp[1]);
|
|
};
|
|
false: {
|
|
prealloc 7, live: (tmp[0], arg[0], arg[1], arg[2], arg[3], arg[4]);
|
|
tmp[1] = arg[4];
|
|
tmp[2] = arg[3];
|
|
tmp[3] = arg[2];
|
|
tmp[4] = arg[1];
|
|
tmp[5] = arg[0];
|
|
tmp[1] = cextend tmp[5] (tmp[4], tmp[3], tmp[2], tmp[1]);
|
|
ret tmp[1];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|
|
|
|
function S(*, *, *) : * {
|
|
tmp[0] = arg[2];
|
|
tmp[1] = arg[1];
|
|
tmp[0] = call apply_1 (tmp[1], tmp[0]), live: (arg[0], arg[1], arg[2]);
|
|
tmp[1] = arg[2];
|
|
tmp[2] = arg[0];
|
|
tcall apply_2 (tmp[2], tmp[1], tmp[0]);
|
|
}
|
|
|
|
function K(*, *) : * {
|
|
tmp[0] = arg[0];
|
|
ret tmp[0];
|
|
}
|
|
|
|
function I(*) : * {
|
|
prealloc 4, live: (arg[0]);
|
|
tmp[0] = arg[0];
|
|
tmp[1] = calloc K ();
|
|
tmp[2] = calloc K ();
|
|
tcall S (tmp[2], tmp[1], tmp[0]);
|
|
}
|
|
|
|
function f3(integer, integer, integer) : integer {
|
|
tmp[0] = arg[2];
|
|
tmp[1] = arg[1];
|
|
tmp[2] = arg[0];
|
|
tmp[1] = add tmp[2] tmp[1];
|
|
tmp[0] = mul tmp[1] tmp[0];
|
|
ret tmp[0];
|
|
}
|
|
|
|
function main() : * {
|
|
prealloc 14;
|
|
tmp[0] = 7;
|
|
tmp[1] = 1;
|
|
tmp[2] = calloc I ();
|
|
tmp[3] = calloc I ();
|
|
tmp[4] = calloc I ();
|
|
tmp[5] = calloc I ();
|
|
tmp[6] = calloc I ();
|
|
tmp[7] = calloc I ();
|
|
tmp[8] = calloc I ();
|
|
tmp[4] = call apply_4 (tmp[8], tmp[7], tmp[6], tmp[5], tmp[4]), live: (tmp[0], tmp[1], tmp[2], tmp[3]);
|
|
tmp[1] = call apply_3 (tmp[4], tmp[3], tmp[2], tmp[1]), live: (tmp[0]);
|
|
prealloc 6, live: (tmp[0], tmp[1]);
|
|
tmp[2] = 2;
|
|
tmp[3] = calloc I ();
|
|
tmp[4] = calloc I ();
|
|
tmp[5] = calloc I ();
|
|
tmp[2] = call apply_3 (tmp[5], tmp[4], tmp[3], tmp[2]), live: (tmp[0], tmp[1]);
|
|
prealloc 6, live: (tmp[0], tmp[1], tmp[2]);
|
|
tmp[3] = 3;
|
|
tmp[4] = calloc I ();
|
|
tmp[5] = calloc I ();
|
|
tmp[6] = calloc I ();
|
|
tmp[3] = call apply_3 (tmp[6], tmp[5], tmp[4], tmp[3]), live: (tmp[0], tmp[1], tmp[2]);
|
|
prealloc 2, live: (tmp[0], tmp[1], tmp[2], tmp[3]);
|
|
tmp[4] = calloc f3 ();
|
|
tmp[1] = call apply_3 (tmp[4], tmp[3], tmp[2], tmp[1]), live: (tmp[0]);
|
|
prealloc 2, live: (tmp[0], tmp[1]);
|
|
tmp[2] = calloc K ();
|
|
tcall apply_2 (tmp[2], tmp[1], tmp[0]);
|
|
}
|