Expressions
The LV2Expr
structure represents any computation that leads to a new value on the stack. Expressions can be nested arbitrarily.
To give you an overview of what an expression could look like, here is the stripped down version of its actual implementation.
#![allow(unused)] fn main() { pub enum LV2Expr { // a constant value Value, // variable in read position Variable, // a branch that evaluates expressions conditionally Branch, // call to a function Call, // operations with one operand Operation1, // operations with two operands Operation2, // ... and many more. } }
Resolvement of Variables
The scope of a variable is determined by the previously emitted Global
and Local
declarations.
In a block, the variable's scope is not fixed but can change after each statement.
Behavior before v0.5.0: While lowering, the runtime keeps track of locally assigned identifiers. This is crucial for determining the scope during read access later. If a variable is not known locally, a fallback to global scope happens.
Example
We want to transform the formula (1 + 2) * f(2)
to lovm2
bytecode. Use a parser of your choice to generate an abstract syntax tree from the textual representation. Note that lovm2
does not care about operator priorities so its your parsers duty to correctly handle them. After processing the input, your AST should look something like this:
Value(1)
\
-- Operation(+)
/ \
Value(2) -- Operation(*)
/
Call(f, 2)
And here is the compiletime representation of said formula:
#![allow(unused)] fn main() { let formula = LV2Expr::from(1) .add(2) .mul(LV2Call::new("f").arg(2)); }
The unoptimized LIR
now looks like this:
CPush(1)
CPush(2)
Operator2(Add)
CPush(2)
Call(f, 1)
Operator2(Mul)