FuncBuilder is an experimental just-in-time (JIT) compiler. It is a companion of symjit, which is a jit compiler for sympy expressions. FuncBuilder provides a lower-level API that allows for step-by-step constructions of fast functions. While symjit uses two different backends (based on Rust and Python), FuncBuilder uses a pure Python code generator with no dependency on any packages outside the standard library.
The FuncBuilder API is inspired by llvmlite, but is not identical.
As a pure Python package, FuncBuilder can be installed from PyPi as
python -m pip install FuncBuilder
The workflow is as follows:
- Create a
Builderobject. The function arguments are defined as this stage. - Add instructions step-by-step.
- Compile to machine code. The output variable is defined at this stage.
A simple example,
from funcbuilder import FuncBuilder
B, [x, y] = FuncBuilder('x', 'y')
a = B.fadd(x, y)
f = B.compile(a)
print(f(1.0, 2.0)) # prints 3.0FuncBuilder accepts as arguments the names of the input variables (currently, the type of all variables is implicitely float64) and returns a tuple. The first item is a Builder object and the second a list of variables correspoding to the input variables.
Afterward, the program is built stepwise using the Builder API (discussed below). In the example above, fadd takes two variables x and y as inputs and returns the result of floating point addition as a.
Finally, f = B.compile(a) compiles the program and returns a function f, which has a type signature of double f(double x, double y).
These are functions exported from the Builder object to add instructions.
All these functions accept two double variables or constants as input and return a new temporary variable.
fadd(x, y): floating point addition.fsub(x, y): floating point subtraction.fmul(x, y): floating point addition.fdiv(x, y): floating point division.pow(x, y): floating point power. Special shortcut codes are generated whenyis 1, 2, 3, -1, -2, 0.5, 1.5, and -0.5. Otherwise,powstandard function is called.
These functions accept a single double variable or constant as input and return a new temporary variable.
square(x): returnsx**2.cube(x): returnsx**3.recip(x): returns1/x.sqrt(x): returns the square root ofx.
These functions also accept a double variable or constant as input and return a new temporary variable.
exp(x)log(x)sin(x)cos(x)tan(x)sinh(x)cosh(x)tanh(x)asin(x)acos(x)atan(x)asinh(x)acosh(x)atanh(x)
The following functions compare two floating point numbers. The result is encoded as a floating point number, with 0.0 corresponding to False and an all 1 mask (= NaN) to True.
lt(x, y):xis less thany.leq(x, y):xis less than or equal toy.gt(x, y):xis greater thany.geq(x, y):xis greater than or equal toy.eq(x, y):xis equal toy.neq(x, y):xis not equal toy.
Boolean variables (encoded as float, discussed above) can be combined using,
and_(x, y):xandy.or_(x, y):xory.xor(x, y):xxory.not_(x): notx.
Note that and_, or_, and not_ have trailing underscores to distinguish them from Python's reserved words.
Currently, FuncBuilder provides a simple API to implement conditional jumps and loops based on setting labels and branch instructions.
set_label(label): set a label at the current instruction position (labels are strings).branch(label): unconditional jump to label.branch(cond, true_label): conditional jump totrue_labelifcond(a variable) is True. Ifcondis False, the control flow continues.branch(cond, true_label, false_label): conditional jump totrue_labelifcond(a variable) is True and tofalse_labelif it is False.
All the builder functions discussed up to this point return a new variable, as is expected from static single-assignment (SSA) forms. However, to generate loops and accumulators, one needs the ability to reassign a value to the same variable (e.g., i = i + 1). Compilers, including LLVM, accommodate this by including Phi nodes. Therefore, FuncBuilder also provides a simple implementation of a Phi node as phi function to allow for constructing loops. Calling phi returns an uninitiated Phi object. During program construction, one assigns values to each node by calling the add_incoming function of each Phi node. Note that add_incoming is a function of the Phi object and not the builder object.
The following example shows how to calculate factorial. Note that the two Phi nodes (p and n) are updated by calling add_incoming.
from funcbuilder import FuncBuilder
B, [x] = FuncBuilder('x')
p = B.phi()
p.add_incoming(1.0)
n = B.phi()
n.add_incoming(x)
B.set_label('loop')
r1 = B.fmul(p, n)
p.add_incoming(r1)
r2 = B.fsub(n, 1.0)
n.add_incoming(r2)
r3 = B.geq(n, 1.0)
B.cbranch(r3, 'loop')
f = B.compile(p)
assert(f(5) == 120)