Neptune Language Internals

VM Design

Like V8, the VM is a register-based VM that has a special accumulator register. The accumulator register is the implicit input/output register for many ops. This reduces the number of arguments needed. The VM also has many dedicated ops to speed up integer operations like AddInt,LoadSmallInt and ForLoop. The bytecode generated for a function can be viewed by the disassemble function in the vm module.

Value representation

On x86_64 and aarch64 the following scheme is used to represent values.

Empty   0x0000 0000 0000 0000 (nullptr)
Null    0x0000 0000 0000 0001
True    0x0000 0000 0000 0002
False   0x0000 0000 0000 0003
Pointer 0x0000 XXXX XXXX XXXX [due to alignment we can use the last 2bits]
Int     0x0001 0000 XXXX XXXX
Float   0x0002 0000 0000 0000
                  to
        0xFFFA 0000 0000 0000

Doubles lie from 0x0000000000000000 to 0xFFF8000000000000. On adding 2<<48
they lie in the range listed above.

ForLoop op

Many for loops are of the form

for i in a..b {
    do something
}

If hasNext and next methods are called it would be very slow. So two specialized ops exist for for loops of this form.

  • BeginForLoop: It checks whether both the start and end are integers and whether the start is lesser than the end.It is only called once.
  • ForLoop: It just increments the integer loop variable and compares it so it is much faster than other for loops.

Wide and Extrawide arguments

To reduce bytecode size Neptune lang uses the strategy that V8 does. An op can have arguments of any size. 8 bit arguments are used normally but prefix bytecodes are used for 16 bit(wide) and 32 bit(extrawide) arguments. The Wide and Extrawide ops precede instructions with these arguments. These ops read the op next to it and dispatch to the wide and extrawide variants of the ops. The wide and extrawide handlers are assigned entries in the bytecode dispatch table that have a fixed offset from the normal variants. Macros are used to generate the wide and extrawide bytecode handlers. This scheme has the problem that the number of bytes to reserve for jump offsets is not known. To resolve this problem JumpConstant, JumpIfFalseOrNullConstant and similar ops exist. The jump offset is contained in the constants table. If later it is found that enough space exists to store the jump offset directly in the bytecode then they are converted to the non-constant variants like Jump and JumpIfFalseOrNull and the bytecode is patched. If enough space is not available then the constant table must be patched.

|   AddInt  | |     5     |
|    Wide   | |   AddInt  | |         300          |
| Extrawide | |   AddInt  | |                10_000                      |