4 - Local variables: Artisan and Organic!
What to implement
Functions
get_local_variable
get_local_variable
Operations
OP_LDC_W
OP_IINC
OP_ILOAD
OP_ISTORE
OP_WIDE
In order to pass test4, correctly implement all of the functions and instructions listed above.
OP_LDC_W
OP_IINC
OP_ILOAD
OP_ISTORE
OP_WIDE
In order to pass test4, correctly implement all of the functions and instructions listed above.
Introduction
In this chapter, you will be tasked with implementing loading constants onto the stack and handling local variables.
The constant pool
The constant pool is the location in memory which contains read-only constants. These constants are loaded into the constant pool at load-time, and are never changed thereafter. In task 1, you already loaded in the constants from file (and ensured the right endianess). The only thing you have to do now is push the desired constant on the stack when encountering and LDC_W instructions by using get_constant
you implemented earlier.
Local variables
Next to the stack, data can also be in stored local variables. Each local variable contains a word of data, which can be loaded on the stack using ILOAD. The ISTORE instruction pops the topmost stack word to the given local variable .
Consider a small example which uses ILOAD and ISTORE:
.main
.var
a
b
.end-var
BIPUSH 0x1 // stack: 1
ISTORE a // stack:
BIPUSH 0x2 // stack: 2
ILOAD a // stack: 2, 1
IADD // stack: 3
ISTORE b // stack:
ILOAD b // stack: 3
ILOAD b // stack: 3, 3
HALT
In this example, main has two variables, a and b. Above, these variables are refered to by name, but in the ijvm file these are replaced with indicices, starting from 0. In this example a -> 0
and b -> 1
. Hence, 0x15(ILOAD) 0x01
, is the translation of ILOAD a
above.
As we will see in the next task, the ijvm binary lists for each method how many local variables there are, except for main. You can safely assume that the main
method contains a maximum of 256 local variables.
Frames
The main challenge with local variables is to store them in a way that also works once you also implement methods in the next task. Local variables are stored in a structure that contains all information for the current method: the local frame.
All IJVM methods, including main
, are assigned their own local frame. Each local frame contains the arguments, local variables and return data: the data that is needed to return from a method. Consequently, the IJVM stack is built up of (local) frames, one for each method invoked. Frames are typically stored on the stack, and the layout is typically as follows:
For methods other than the first method, main, the bottom of the frame contains a link pointer which points to the location at which the previous program counter is stored (this so that you can find it regardless of how many elements are stored on the stack above it). For the main method this link pointer is omitted, as there is no previous program counter. To keep track of where the current frame starts, we maintain a frame pointer in a variable called lv (for local variable), which always points to the start of the frame. The local variables of the current frame can always be found at their offset from lv. The current frame is area between lv and the top of the stack sp (including both words pointed to).
Above the link pointer, you can find the method arguments and local variables. Above the local variables, you can find the return data: the previous frame pointer prev_lv
and the previous program counter prev_pc. The values stored at these locations are restored appropriately when a method returns.
Arguments are handled in the same way as local variables, there is no distinction between the two other than that arguments are initialized by values pushed before the method call. Both are handled in the same way via ILOAD and ISTORE. The assembler gives every argument and local variable a unique label, which happens to be their index in the local frame. For example, if a method has a single argument x
and two variables a
and b
, the assignment would be x -> 1
, a -> 2
and b -> 3
. The index is counted from local variable pointer lv
, so the location of an argument with index i
is lv + i
(if the stack grows upwards). Note that for methods other than main, the local variable/argument at index 0 is the link pointer itself, which before method invocation contains the OBJREF
(see next chapter).
In the main method, there is no OBJREF
, so the local variables start at 0 instead of 1. There hence is no space for the link pointer, but this is not a problem as there is no caller to return to. You can assume that IRETURN
is never called in the main method, but if you want to be complete you can detect if this happens (by for example checking if lv points to the start of the stack), and then halt the program.
The suggested approach
If your method of storing and loading local variables does not also work in combination with the method which are introduced in the next chapter, you may have to redesign you implementation later. Hence, make sure you first also read the next chapter on methods.
Then, start with implementing the LDC_W instruction.
Afterward, shift your focus to local variables. The simplest option is to store the local variables exactly as described above. If you find this hard or illogical, you can also try another method: local frames can be implemented separately from the operand stack. You could, for example, implement them using a separate stack or a linked-list.
Once you have implemented IINC, ILOAD and ISTORE, implement WIDE. The WIDE instruction is a prefix instruction that indicates that the index argument of the subsequent instruction is represented with a short, as opposed to a byte, and is thus 2 bytes long. Only the IINC, ILOAD and ISTORE instructions can be prefixed with WIDE. The instruction WIDE is not considered a step itself, so WIDE ISTORE 0x0000 is one step of the step
function, not two.
OpCode | Instruction | Args | Description |
---|---|---|---|
0x13 | LDC_W | short | Push the constant with index short from the constant pool onto the stack |
0x15 | ILOAD | byte | Push the local variable with (unsigned) index byte onto the stack |
0x36 | ISTORE | byte | Pop a word from the stack and store it in the local variable with (unsigned) index byte |
0x84 | IINC | byte byte | Increment a local variable by a value. The first byte is the (unsigned) index of the local variable. The second byte is the (signed) value. |
0xC4 | WIDE | N/A | Prefix instruction; the next instruction has a 16-bit (unsigned) index. This can be prefixed to ILOAD, ISTORE and IINC. This only changes the size of the index to 16 bits, the constant of IINC remains 8 bits. |
Hints
The main challenge of this task lies in creating a solid foundation for methods, which will be covered in the next chapter. A bad design may come back to haunt you, so think ahead.
Our tests assume that the step
function executes the whole WIDE-prefixed instruction in one step. In other words, the WIDE instruction and the subsequent instruction are treated as a single instruction.
The main challenge of this task lies in creating a solid foundation for methods, which will be covered in the next chapter. A bad design may come back to haunt you, so think ahead.
Our tests assume that the step
function executes the whole WIDE-prefixed instruction in one step. In other words, the WIDE instruction and the subsequent instruction are treated as a single instruction.