Skip to content
Robert Virding edited this page Jan 30, 2024 · 3 revisions

Luerl VM

The Luerl VM is a hybrid implementation. It uses normal Erlang function calls for Luerl calls and blocks and has a small instruction set for operations inside a block. This is a pure stack machine.

Blocks keep variables in tuples. There are two variable types depending on how they are defined:

  • Local variables that are used in this block and sub-blocks, but not used in any functions defined in the blocks. These are kept in a stack of tuples, the LocalVars or Lvs, and referenced by offset in stack and offset in tuple.

  • Environment variables that are in functions which are defined in this block or in sub-blocks. This mean they must be kept around as long as the functions are alive and are stored in the global heap as each invocation can modify them. They are kept in a stack of references, the EnvironmentVars or Evs, to tuples in the global heap and referenced by offset in stack and offset in tuple.

A function contains a reference to the stack of environment variables which existed when it was created. Note that the mutable nature of Lua data means that these can be modified and the changes must be visible to every function which references them.

There is also a stack containing arguments and temporary values. This stack is "global" in the sense that it is passed through all calls and blocks.

It is also passed as an argument into functions implemented in Erlang. This is so that event of a Lua/Luerl GC the collector uses the stack to determine which data in the global heap is to be saved.

To handle multiple return values we always return a list of values. The only place this is not done is in luerl_eval.erl when getting values from the environment where we can only have one value. This means a lot of calls to first_value/1 in luerl_emul.erl, but the consistency is worth it.

Similarily all the arguments in a function call are passed in a list. The function then unpacks the list into its arguments, including '...'.

All of the predefined libraries have an install/1 function. This is called when initialising Luerl; it does any library specific initialization necessary and returns a table containing the functions in the library.

We create a unique tag which is saved in the environment. This is used so we can implement 'break' with a simple throw. The thrown value includes the tag so we can uniquely catch it and not get confused with a throw/error/exit from the erlang code.

Garbage collector

Is important to note that the garbage collector is never called by luerl itself.

The main reason is that we could not work out a decent heuristic about when to call it. For example there is no heap as such we can check to see how much luerl memory we are using and when it fills. So it is left up to the caller to do so. It can be called both from inside luerl calling the collectgarbage("collect") or from the Erlang level.

This might be useful if you want to reuse the initial state for each call as you can just throw away the returned state and not waste time doing garbage collection.

Compiler

The compiler has state at different levels:

  • In luerl_comp there is #comp{} containing code, options and errors.
  • In the #cst{} between the compiler modules for data outside the code. This empty so far.
  • Inside and local to the compiler modules.

All the compiler modules are written so that they chain a status argument through their code, even if it not used. When they are not used we just send the atom 'nil' through and check it comes out "the other end".