Typed Lua (Old)
Some random thoughts and ideas about types
Last updated
Some random thoughts and ideas about types
Last updated
There's a lot of stuff that should be formalized and notation that is abused. Mainly to get the point across. This is not an academic paper, we're trying to be readable here.
We distinguish between regular Lua functions which take in some parameters of unknown type and outputs a value of type versus a function which takes in types as parameters and outputs a type (breaking from the traditional lambda form to represent functions).
For this, we assume the influence of external function calls. We also ignore metatables. Type inference/deduction usually is as follows:
Type constructors prove variable types
Types are then propagated through operations
Both of which are complicated by function calls (unintended side effects) and metamethods (unspecified and unknown operation type deductions). So we ignore those for now.
Consider this Lua code:
We can construct an SSA representation like this:
Then the type deductions are as follows:
Notice a few key concepts:
NEWINDEX is called even when replacing the value at a table index. This is due to the semantics of Lua's __newindex
metamethod.
Adding a new entry to a table is considered a new static assignment
This new static assignment gets a new type
Other type deductions are done normally
The deduction for %4 arises because typeof(%3) is a number so number + number gives a number. However this is not trivial if metatables are involved.
We use type intersection to refine the type of t as the program executes. In fact, %1, %2 and %5 are aliased to the same table reference. In Lua, tables are always pass-by-reference-value.
Type intersection will replace table entry types if they exist
We extend this type algebra to consider PHI nodes also. Consider:
Where in the SSA representation, we expect some PHI nodes to be generated:
When we type annotate it (only the interesting annotations are shown):
Where the other types are derived as usual. Just like how NEWINDEX forces a type intersection, the PHI node forces a type union.
This naturally gives rise to an algorithm to process the code in SSA form:
Transform into SSA as usual, using virtual register assigns (such as NEWINDEX)
Perform DCE on the no-return instructions by removing the virtual assignment (treat them as simply side-effect instructions).
Perform the usual SSA optimizations.
A critical step to realizing type inferencing with function calls is to be able to predict types (as much as possible) from unknown parameters.
Problem: Evaluating the output of the generic function F is as complex as parsing the function f itself. Most of this complexity comes from metamethods as operators may have undefined type signatures! Completely unknown at compile-time! Moreover, this is way too slow. For instance,
Becomes the generic version
Idea: Only infer function signatures when we have to. This gives rise to the following algorithm:
For each function, infer the most general type F of f. Note we should get the most general return type of f from this.
Start at the top-level and assume we don't have any untyped parameters/locals/globals. Then for each function invocation, use the most-generic return type for continued inference. Add the function invocation signature.
Then during codegen, we can take the most frequently called function signatures and specialize it into typed IR. Otherwise, we leave the function as generic bytecode for the standard Lua interpreter to use.
This leads to another problem: The more polymorphic the written code is, the less optimal the AOT generated code becomes. This is an acceptable trade-off.
Treat each SSA instruction as a function and if no such function legally exists, throw a type error.
Idea: Consider function then there must exist a function which takes in the parameter types (i.e., typeof(xi)) and outputs the deduced type. Moreover, this function is trivial to compute as the typical type deduction techniques (in previous section) applies.
Where we try to match the type signatures for the op functions (if we even can) for each unique invocation of f. Now, given an average of k calls per function of depth d we have a runtime of which is highly undesirable. Basically it's (way) too slow.