A compiler for Petra programs that emits LLVM.
- Python >= 3.7
- llvmlite
source setup.sh
pip install -e .
./run_tests.sh
- Create a Conda environment with
source setup.sh
. - Locally install Petra for development with
pip install -e .
. - Optional: Install development tools (e.g. mypy) with
pip install -r requirements-dev.txt
.
- Create a virtual environment with
python3 -m venv venv
. - Enter the virtual environment with
source venv/bin/activate
. - Locally install Petra for development with
pip install -e .
. - Optional: Install development tools (e.g. mypy) with
pip install -r requirements-dev.txt
.
- Write a Petra program.
import petra
to use the Petra library. - Typecheck your program with
mypy <program name>.py
. - If your program is a test, add it to the tests/ directory and write test cases in a format similar to other tests in the directory.
- Run all tests with
./run_tests.sh
.
Sometimes you may want to compile the emitted LLVM manually.
- Generate llvm bytecode by writing a Python file that uses the Petra library
and calling
petra.Program.to_llvm()
. llc --mtriple=<target-architecture> <llvm-file>
(for target architecture, on Linux 64-bit tryx86_64-unknown-linux-gnu
, and on macOS tryx86_64-apple-darwin18.7.0
)- Assemble and link to create an executable.
Petra code is also Python code - every piece of syntax is actually a call to a Python function in the Petra library. Assuming that the code abides by the type hints written for each function (which can be verified with mypy), when each piece of syntax is constructed, it checks that its arguments are legitimate. When each function is constructed, it typechecks its statements. This means that if Python can execute your Petra code, then it is valid Petra code!
If your program typechecks, you can programmatically compile your program to
LLVM bytecode or JIT it to call it from other Python code. See the to_llvm()
and compile()
methods of Petra.program
.
Petra takes some inspiration from the C programming language and implements a subset of it, so much of the syntax discussed may be familiar.
Petra currently supports 4 types, all of which are primitive.
-
petra.Int8_t
: A 8-bit integer type. -
petra.Int32_t
: A 32-bit integer type. -
petra.Float_t
: A single-precision float type. No operations have been implemented on floats yet - it was introduced for extern compatibility. -
petra.Bool_t
: A boolean type.
Two metatypes are defined for the inputs and outputs of a function.
-
petra.Ftypein
: This is equivalent to Tuple[Type, ...], which means a possibly-empty tuple of types. -
petra.Ftypeout
: This is equivalent to Union[Tuple[()], Type], which means either the empty tuple (void, which is otherwise not a valid type) or a single type.
-
petra.Program(name: str)
Creates a program with the given name.
-
petra.Program.add_func_decl(name: str, t_in: Ftypein, t_out: Ftypeout)
Declares an extern function (for typechecking reasons) that can be called from Petra code.
-
petra.Program.add_func(name: str, args: Tuple[Declare, ...], t_out: Ftypeout, block: Block)
Adds a function with the given name, declaration, and content to the program, then returns the program (for easy chaining).
-
petra.Program.to_llvm()
Returns an unoptimized LLVM representation of the program as a string.
-
petra.Program.save_object(filename: str)
Saves an unoptimized object file of the program suitable for passing to the platform linker.
-
petra.Program.compile()
Returns a MCJIT execution engine with the program loaded. See tests for an example of how to use this.
-
petra.Block(statements: List[Union[Expr, Statement]])
Creates a block of statements. Used in other control flow constructs and in function bodies.
-
petra.If(pred: Expr, then_block: Block, else_block: Block)
Creates an if-else statement predicated on the given expression. The then and else clause can be empty.
-
petra.Call(name: str, args: List[Expr])
Creates a function call statement to a function that was either declared extern or previously added to the program.
-
petra.DefineVar(symbol: Symbol, value: Expr)
Defines a new variable. Variables with the same symbol cannot be redeclared within a scope (defined by a function or an if/else clause).
-
petra.Assign(var: Var, value: Expr)
Creates an assignment statement assigning the expression to either to an existing variable.
-
petra.Return(e: Union[Tuple[()], Expr])
Creates a return statement that returns either nothing or an expression.
-
petra.Symbol(type_: Type, name: str)
Creates a symbol, which can be used in variable declarations, assignments or when adding parameters to a function. The name must pass the regex
r"^[a-z][a-zA-z0-9_]*$"
. Note that the name is only used for display to humans and that every symbol is unique regardless of name. Two (or more) symbols with the same name can be defined in the same scope, but a given symbol may be defined in a scope only once.
-
petra.Var(symbol: Symbol)
Creates a variable reference to an argument or previously declared variable.
-
petra.Int8(value: int)
Creates an
Int8_t
constant. -
petra.Int32(value: int)
Creates an
Int32_t
constant. -
petra.Float(value: float)
Creates a
Float_t
constant. -
petra.Bool(value: bool)
Creates a
Bool_t
constant.
-
petra.Add(left: Expr, right: Expr)
Creates a addition of two arithmetic expressions.
-
petra.Sub(left: Expr, right: Expr)
Creates a subtraction of two arithmetic expressions.
-
petra.Mul(left: Expr, right: Expr)
Creates a multiplication of two arithmetic expressions.
-
petra.Div(left: Expr, right: Expr)
Creates a division of two arithmetic expressions.
-
petra.Mod(left: Expr, right: Expr)
Creates a division remainder of two arithmetic expressions.
-
petra.Lt(left: Expr, right: Expr)
Creates a less-than comparison between two arithmetic expressions.
-
petra.Lte(left: Expr, right: Expr)
Creates a less-than-or-equal comparison between two arithmetic expressions.
-
petra.Gt(left: Expr, right: Expr)
Creates a greater-than comparison between two arithmetic expressions.
-
petra.Gte(left: Expr, right: Expr)
Creates a greater-than-or-equal comparison between two arithmetic expressions.
-
petra.Eq(left: Expr, right: Expr)
Creates an equality check between two expressions.
-
petra.Neq(left: Expr, right: Expr)
Creates an unequality check between two expressions.
-
petra.And(left: Expr, right: Expr)
Creates a short-circuiting boolean and of two boolean expressions.
-
petra.Or(left: Expr, right: Expr)
Creates a short-circuiting boolean or of two boolean expressions.
-
petra.Not(e: Expr)
Creates a boolean not of a boolean expression.
-
petra.ValidateError
An exception thrown if Petra code does not confirm to certain structural checks such as a variable name conforming to a regex.
-
petra.TypeCheckError
An exception thrown if Petra code fails to typecheck.
The static checks and compilation of Petra are completed in stages.
Static errors are checked upon construction of all Petra syntax and will throw a StaticException if any are found.
Typechecking occurs each time a function is added to the program. A TypeContext is constructed using the set of extern functions declared, the set of previously added functions, and an empty variable type context. All statements in the program are sequentially type-checked.
Code generation also occurs each time a function is added to the program. llvmlite is liberally used to simplify construction of basic blocks, and a codegen context is passed along to help resolve internal references to variables and functions.
Petra includes a testing framework built upon Python's unittest. By taking advantage of the LLVM MCJIT execution engine and Python's ctypes, it's possible to run Petra functions from Python which eases testing. Static and type exceptions can also be caught and verified to be thrown for invalid programs.
Petra is incomplete, and programming features are still missing. Here's a partial list:
- types:
- more basic types (signed and unsigned int8/16/32/64, float32/64)
- aggregate types (arrays and structs)
- strings
- control flow:
- loops
- elseif
- pointers, reference/dereference, l-values
- floating point arithmetic
- casting between types
In addition, parts of Petra infrastructure could be improved:
- Some error messages could be reworded or tested due to lack of usage.
- Petra's testing framework, while decently robust, is missing a lot of tests. An unfortunate side-effect is that there may be latent bugs in the compiler as well.
- Run optimizations on the generated code.
- Generate debug symbols in the generated code.
Petra was initially designed and built by Andrew Benson in Fall 2019 with the helpful guidance of Elliott Slaughter. Professor Alex Aiken served as advisor.