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
Builder
object. 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.0
FuncBuilder
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 wheny
is 1, 2, 3, -1, -2, 0.5, 1.5, and -0.5. Otherwise,pow
standard 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)
:x
is less thany
.leq(x, y)
:x
is less than or equal toy
.gt(x, y)
:x
is greater thany
.geq(x, y)
:x
is greater than or equal toy
.eq(x, y)
:x
is equal toy
.neq(x, y)
:x
is not equal toy
.
Boolean variables (encoded as float, discussed above) can be combined using,
and_(x, y)
:x
andy
.or_(x, y)
:x
ory
.xor(x, y)
:x
xory
.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_label
ifcond
(a variable) is True. Ifcond
is False, the control flow continues.branch(cond, true_label, false_label)
: conditional jump totrue_label
ifcond
(a variable) is True and tofalse_label
if 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)