10: Building Your Own Dialect
Welcome to Chapter 10 of the "Implementing a language with MLIR" tutorial. So far, our AST has generated operations from MLIR's existing dialects directly. This has served us well: arith represents arithmetic, func represents functions, scf represents structured control flow, and memref provides storage for mutable variables. In this chapter, you'll create a small custom dialect called Kaleidoscope. We'll add a handful of operations, and they all solve one specific problem.
10.1 The Problem We're Solving
Let's start with a simple function:
def incrementAndSquare(x)
var y = x in
(y = y + 1) * y;
Up to now, when our compiler saw x and y, it immediately turned them into generic memory allocations. We lost the x and y. In Chapter 9, we worked around this by saving the parameter names separately and stitching them back together later. That worked, but it was a patch. Ideally, we wouldn't lose the information in the first place. We can fix this with a custom operation for which we need a custom dialect.
Here's what we are aiming to emit in MLIR.
func.func @incrementAndSquare(%arg0: f64) -> f64 {
%cst = arith.constant 1.000000e+00 : f64
%0 = kaleidoscope.var "x"(%arg0 : f64) {argumentNumber = 1 : i64} : !kaleidoscope.variable
%1 = kaleidoscope.read(%0 : !kaleidoscope.variable) : f64
%2 = kaleidoscope.var "y"(%1 : f64) {argumentNumber = 0 : i64} : !kaleidoscope.variable
%3 = kaleidoscope.read(%2 : !kaleidoscope.variable) : f64
%4 = arith.addf %3, %cst : f64
kaleidoscope.assign(%4 : f64, %2 : !kaleidoscope.variable)
%5 = kaleidoscope.read(%2 : !kaleidoscope.variable) : f64
%6 = arith.mulf %4, %5 : f64
return %6 : f64
}
We'll invent 3 kaleidoscope.* operations.
kaleidoscope.var: Declares a variable and retains its name. You can see how the "x" and "y" are now part of the IR above.
kaleidoscope.read: Reads a variable.
kaleidoscope.assign: Assigns a value to a variable.
Notice that everything else still uses the standard dialects you already know: arith for math, func for functions.
10.2 Round-tripping MLIR
As we'll see in more concrete terms soon, MLIR's textual assembly format is meant to round-trip: print an in-memory IR to text and parse that text back into IR to reconstruct the operations. This way we can save serialize the IR to files and read them back in later if needed. Each custom operation below declares an assemblyFormat in TableGen. MLIR uses it to generate the operation's printer and parser, so the syntax you see in --dump-mlir can be serialized and read back in later. The VariableType definition supplies the text representation !kaleidoscope.variable, and useDefaultTypePrinterParser generates a printer and parser for the dialect.
10.3 Define the Dialect
MLIR dialects are usually defined with TableGen, a compact language for describing types and operations.
Create a file called KaleidoscopeOps.td and start with the dialect itself:
// Provides the TableGen definitions for MLIR dialects, types, and operations.
include "mlir/IR/OpBase.td"
def Kaleidoscope_Dialect : Dialect {
// The prefix for every operation and type in this dialect. An operation
// with the mnemonic `var` will therefore print as `kaleidoscope.var`.
let name = "kaleidoscope";
// Where the generated C++ classes live. Our classes end up in the
// `mlir::kaleidoscope` namespace.
let cppNamespace = "::mlir::kaleidoscope";
let summary = "Operations that preserve Kaleidoscope variable semantics";
// Ask MLIR to generate the parser and printer for our types based on the
// assembly format each type declares. We only have one type, and its format
// is defined below.
let useDefaultTypePrinterParser = 1;
}
The name gives our operations their kaleidoscope. prefix. The cppNamespace places the generated classes in mlir::kaleidoscope, and useDefaultTypePrinterParser gives our types the usual MLIR parsing and printing support.
10.4 Define the Variable Type
We will build the dialect one piece at a time. For each piece, we will look at the TableGen definition, the C++ that TableGen generates, the C++ that we write to use it, and the MLIR that eventually gets printed.
Start with the type:
// A type representing a mutable source variable.
//
// TypeDef generates the C++ class `mlir::kaleidoscope::VariableType` from the
// class stem "Variable", and the mnemonic "variable" gives it the textual
// spelling `!kaleidoscope.variable`.
//
// The type deliberately says nothing about how the variable is stored. That
// is a lowering decision, not a property of the source language.
def Kaleidoscope_VariableType
: TypeDef<Kaleidoscope_Dialect, "Variable"> {
let mnemonic = "variable";
let summary = "a mutable Kaleidoscope variable";
// An empty assembly format means the type has no parameters to print after
// its mnemonic, so it always appears as the bare `!kaleidoscope.variable`.
let assemblyFormat = "";
}
From that small definition, mlir-tblgen generates the real VariableType class below. This is an excerpt from KaleidoscopeTypes.h.inc; the generated file also contains parser, printer, and registration machinery.
To see the C++ declaration generated for our type, run:
/path/to/llvm-project/build/bin/mlir-tblgen \
-I /path/to/llvm-project/mlir/include \
-gen-typedef-decls \
KaleidoscopeOps.td
If LLVM was installed with Homebrew instead of built from source, run:
"$(brew --prefix llvm)/bin/mlir-tblgen" \
-I "$(brew --prefix llvm)/include" \
-gen-typedef-decls \
KaleidoscopeOps.td
And hopefully, you can see this printed to the terminal.
#ifdef GET_TYPEDEF_CLASSES
#undef GET_TYPEDEF_CLASSES
namespace mlir {
class AsmParser;
class AsmPrinter;
} // namespace mlir
namespace mlir {
namespace kaleidoscope {
/// a mutable Kaleidoscope variable
class VariableType;
class VariableType : public ::mlir::Type::TypeBase<VariableType, ::mlir::Type, ::mlir::TypeStorage> {
public:
using Base::Base;
static constexpr ::llvm::StringLiteral name = "kaleidoscope.variable";
static constexpr ::llvm::StringLiteral dialectName = "kaleidoscope";
static constexpr ::llvm::StringLiteral getMnemonic() {
return {"variable"};
}
static ::mlir::Type parse(::mlir::AsmParser &odsParser);
void print(::mlir::AsmPrinter &odsPrinter) const;
};
} // namespace kaleidoscope
} // namespace mlir
MLIR_DECLARE_EXPLICIT_TYPE_ID(::mlir::kaleidoscope::VariableType)
#endif // GET_TYPEDEF_CLASSES
};
TypeBase gives us the normal MLIR type behavior. Types are uniqued inside an MLIRContext, so asking the same context for VariableType twice gives us handles to the same underlying type storage. Our compiler asks for the type like this:
auto variableType =
kaleidoscope::VariableType::get(TheContext.get());
When MLIR prints that type, it appears as:
!kaleidoscope.variable
The type says only, "this SSA value represents a Kaleidoscope variable." It does not decide how the variable is stored. We make that decision later during lowering.
10.5 Add an Operation Base Class
All three operations in this chapter share the same dialect and basic setup. This small TableGen class saves us from repeating it:
// A common base for every operation in this dialect. Each concrete operation
// supplies its own mnemonic and, optionally, a list of traits describing its
// behavior.
class Kaleidoscope_Op<string mnemonic, list<Trait> traits = []>
: Op<Kaleidoscope_Dialect, mnemonic, traits>;
We can now define kaleidoscope.var, kaleidoscope.read, and kaleidoscope.assign one at a time.
10.6 Define kaleidoscope.var
The source example begins with a function parameter named x:
def incrementAndSquare(x)
Here is the TableGen definition for the operation that preserves that variable:
// Declares and initializes a mutable source variable.
//
// This generates the C++ class `kaleidoscope::DeclareOp`, printed in IR as
// `kaleidoscope.var`. It is the operation that carries the source name and
// argument number that would otherwise be lost the moment a variable became
// an anonymous allocation.
def Kaleidoscope_DeclareOp : Kaleidoscope_Op<"var", []> {
let summary = "declare and initialize a mutable source variable";
// `(ins ...)` lists everything the operation takes in. The `$` names
// generate C++ accessors (getInitialValue, getName, getArgumentNumber).
//
// - F64:$initialValue is an SSA operand, constrained to f64
// - StrAttr:$name is an attribute holding the source name
// - I64Attr:$argumentNumber is an attribute; 0 means a local variable,
// and 1+ means a function parameter (DWARF numbers
// parameters starting at 1)
let arguments = (ins F64:$initialValue, StrAttr:$name,
I64Attr:$argumentNumber);
// The SSA result represents the *variable itself*, not the floating-point
// value currently stored in it. Subsequent read and assign operations use
// this value to refer to the variable.
let results = (outs Kaleidoscope_VariableType:$variable);
// The textual syntax. Backticks contain literal punctuation, `$name` and
// `$initialValue` refer to the fields above, and `type(...)` prints the type
// of the named operand or result. `attr-dict` prints any attributes not
// already consumed by the format.
//
// This produces, for example:
// %0 = kaleidoscope.var "x"(%arg0 : f64)
// {argumentNumber = 1 : i64} : !kaleidoscope.variable
let assemblyFormat = "$name `(` $initialValue `:` type($initialValue) `)` attr-dict `:` type($variable)";
}
The operation takes an initial f64 value, a source name, and an argument number. It defines one result of our new variable type. The $ names generate accessors such as getInitialValue(), getName(), getArgumentNumber(), and getVariable().
To print the generated operation declarations in the terminal, run:
/path/to/llvm-project/build/bin/mlir-tblgen \
-I /path/to/llvm-project/mlir/include \
-gen-op-decls \
KaleidoscopeOps.td
To write the generated declarations into KaleidoscopeOps.h.inc, run:
/path/to/llvm-project/build/bin/mlir-tblgen \
-I /path/to/llvm-project/mlir/include \
-gen-op-decls \
-o KaleidoscopeOps.h.inc \
KaleidoscopeOps.td
If LLVM was installed with Homebrew instead of built from source, run:
"$(brew --prefix llvm)/bin/mlir-tblgen" \
-I "$(brew --prefix llvm)/include" \
-gen-op-decls \
KaleidoscopeOps.td
Here is the relevant part of the actual DeclareOp class generated in KaleidoscopeOps.h.inc. The full class contains additional builder overloads and MLIR bookkeeping.
class DeclareOp : public ::mlir::Op<
DeclareOp,
::mlir::OpTrait::OneResult,
::mlir::OpTrait::OneTypedResult<VariableType>::Impl,
::mlir::OpTrait::OneOperand> {
public:
static constexpr ::llvm::StringLiteral getOperationName() {
return "kaleidoscope.var";
}
::mlir::TypedValue<::mlir::FloatType> getInitialValue();
::llvm::StringRef getName();
uint64_t getArgumentNumber();
::mlir::TypedValue<VariableType> getVariable();
static void build(::mlir::OpBuilder &builder,
::mlir::OperationState &state,
::mlir::Type variable,
::mlir::Value initialValue,
::mlir::StringAttr name,
::mlir::IntegerAttr argumentNumber);
};
The parser has already turned our source into an AST by the time we use this class. During AST code generation, this helper asks the builder to create a DeclareOp:
static Value CreateVariable(StringRef Name, Value InitialValue,
int64_t ArgumentNumber = 0) {
return TheBuilder->create<kaleidoscope::DeclareOp>(
getLocation(), kaleidoscope::VariableType::get(TheContext.get()),
InitialValue, TheBuilder->getStringAttr(Name),
TheBuilder->getI64IntegerAttr(ArgumentNumber));
}
Function parameters call that helper with one-based argument numbers:
func::FuncOp FunctionAST::codegen() {
...
unsigned Index = 0;
for (BlockArgument Argument : TheFunction.getArguments()) {
StringRef Name = P.getArgs()[Index];
Value Storage = CreateVariable(Name, Argument, Index + 1);
NamedValues[Name.str()] = Storage;
++Index;
}
...
}
Locals and loop variables use the helper's default argument number of 0.
The call to create<DeclareOp> uses the generated build method, inserts the operation into the current MLIR block, and creates its result at the same time. MLIR prints the result as %0 here:
%0 = kaleidoscope.var "x"(%arg0 : f64) {argumentNumber = 1 : i64} : !kaleidoscope.variable
%arg0 : f64is the initial value received by the function."x"is the name from the source.argumentNumber = 1 : i64records thatxis the first function parameter. Locals use0.%0is the SSA value defined byDeclareOp. Its type is!kaleidoscope.variable.
Other operations use %0 to say exactly which variable they mean. When this dialect is lowered, %0 will be replaced by the address of the storage allocated for x.
10.7 Define kaleidoscope.read
The read operation accepts a variable and defines an f64 result containing its current value:
def Kaleidoscope_ReadOp : Kaleidoscope_Op<"read", []> {
let summary = "read a mutable source variable";
// Consumes the variable; produces the f64 value currently stored in it.
let arguments = (ins Kaleidoscope_VariableType:$variable);
let results = (outs F64:$value);
let assemblyFormat = "`(` $variable `:` type($variable) `)` attr-dict `:` type($value)";
}
TableGen generates a ReadOp with typed accessors for both sides of that relationship:
class ReadOp : public ::mlir::Op<
ReadOp,
::mlir::OpTrait::OneResult,
::mlir::OpTrait::OneTypedResult<::mlir::FloatType>::Impl,
::mlir::OpTrait::OneOperand> {
public:
static constexpr ::llvm::StringLiteral getOperationName() {
return "kaleidoscope.read";
}
::mlir::TypedValue<VariableType> getVariable();
::mlir::TypedValue<::mlir::FloatType> getValue();
static void build(::mlir::OpBuilder &builder,
::mlir::OperationState &state,
::mlir::Type value,
::mlir::Value variable);
};
When a VariableExprAST needs the current value of x, its codegen() method creates that operation:
Value VariableExprAST::codegen() {
...
return TheBuilder->create<kaleidoscope::ReadOp>(
getLocation(), TheBuilder->getF64Type(), It->second);
}
The resulting MLIR makes both the input and output types explicit:
%1 = kaleidoscope.read(%0 : !kaleidoscope.variable) : f64
This says, "Read the current value from the variable %0 and define the f64 result %1."
ReadOp is not marked Pure. Two reads of the same variable might return different values if an assignment occurs between them. Marking it pure would let common-subexpression elimination incorrectly merge those reads.
10.8 Define kaleidoscope.assign
The assign operation accepts a new f64 value and the variable that should receive it:
def Kaleidoscope_AssignOp : Kaleidoscope_Op<"assign", []> {
let summary = "assign a new value to a mutable source variable";
// Consumes the variable and the replacement value. Print both operand types
// explicitly so it is immediately clear that the value is an f64 and the
// destination is a !kaleidoscope.variable.
let arguments = (ins Kaleidoscope_VariableType:$variable, F64:$value);
let assemblyFormat = "`(` $value `:` type($value) `,` $variable `:` type($variable) `)` attr-dict";
}
Its generated class has two operands and no result:
class AssignOp : public ::mlir::Op<
AssignOp,
::mlir::OpTrait::ZeroResults,
::mlir::OpTrait::NOperands<2>::Impl> {
public:
static constexpr ::llvm::StringLiteral getOperationName() {
return "kaleidoscope.assign";
}
::mlir::TypedValue<VariableType> getVariable();
::mlir::TypedValue<::mlir::FloatType> getValue();
static void build(::mlir::OpBuilder &builder,
::mlir::OperationState &state,
::mlir::Value variable,
::mlir::Value value);
};
Assignment code generation creates the operation for its side effect and returns AssignedValue separately. That preserves Kaleidoscope's rule that an assignment is also an expression: (y = y + 1) evaluates to the newly assigned value.
Value BinaryExprAST::codegen() {
...
if (Op == '=') {
...
TheBuilder->create<kaleidoscope::AssignOp>(getLocation(), It->second,
AssignedValue);
return AssignedValue;
}
...
}
Because AssignOp defines no result, there is no %result = and no result type at the end:
kaleidoscope.assign(%4 : f64, %2 : !kaleidoscope.variable)
Putting the three operations together gives us a complete variable story:
%0 = kaleidoscope.var "x"(%arg0 : f64) {argumentNumber = 1 : i64} : !kaleidoscope.variable
%1 = kaleidoscope.read(%0 : !kaleidoscope.variable) : f64
%2 = arith.addf %1, %cst : f64
kaleidoscope.assign(%2 : f64, %0 : !kaleidoscope.variable)
%3 = kaleidoscope.read(%0 : !kaleidoscope.variable) : f64
DeclareOp defines %0. The two reads and the assignment use %0 to identify the same variable. %1, %2, and %3 are ordinary f64 values.
10.9 Generate and Include the C++ Classes
Add these rules to CMakeLists.txt to generate the dialect, type, and operation declarations and definitions:
# Every mlir_tablegen invocation below reads this TableGen source file.
set(LLVM_TARGET_DEFINITIONS KaleidoscopeOps.td)
# Generate the KaleidoscopeDialect class declaration and definition from the
# `def Kaleidoscope_Dialect : Dialect` record.
mlir_tablegen(KaleidoscopeDialect.h.inc -gen-dialect-decls
-dialect=kaleidoscope)
mlir_tablegen(KaleidoscopeDialect.cpp.inc -gen-dialect-defs
-dialect=kaleidoscope)
# Generate C++ declarations and definitions for our TypeDef records.
mlir_tablegen(KaleidoscopeTypes.h.inc -gen-typedef-decls
-typedefs-dialect=kaleidoscope)
mlir_tablegen(KaleidoscopeTypes.cpp.inc -gen-typedef-defs
-typedefs-dialect=kaleidoscope)
# Generate C++ declarations and definitions for our Op records.
mlir_tablegen(KaleidoscopeOps.h.inc -gen-op-decls)
mlir_tablegen(KaleidoscopeOps.cpp.inc -gen-op-defs)
# Give the generated fragments one build target so the executable can depend
# on all of them before compiling files that #include those fragments.
add_custom_target(KaleidoscopeOpsIncGen DEPENDS
KaleidoscopeDialect.h.inc
KaleidoscopeDialect.cpp.inc
KaleidoscopeTypes.h.inc
KaleidoscopeTypes.cpp.inc
KaleidoscopeOps.h.inc
KaleidoscopeOps.cpp.inc
)
The add_custom_target groups the six generated files under one target name. The generated .inc files live in the build directory, so add that directory to the include path and make the executable depend on the generated target:
target_include_directories(toy PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${LLVM_INCLUDE_DIRS}
${MLIR_INCLUDE_DIRS}
)
add_dependencies(toy KaleidoscopeOpsIncGen)
Include the generated declarations in the dialect header:
// Generated declaration of mlir::kaleidoscope::KaleidoscopeDialect.
#include "KaleidoscopeDialect.h.inc"
// KaleidoscopeTypes.h.inc contains several selectable sections. Defining this
// macro asks it to emit the generated type class declarations at this include.
#define GET_TYPEDEF_CLASSES
#include "KaleidoscopeTypes.h.inc"
// Likewise, select the generated operation class declarations from the
// operation header fragment.
#define GET_OP_CLASSES
#include "KaleidoscopeOps.h.inc"
Include the generated definitions in the implementation file:
// Generated definitions for the dialect class declared by the matching header
// fragment.
#include "KaleidoscopeDialect.cpp.inc"
// Select the generated C++ definitions for our TypeDef and Op records.
#define GET_TYPEDEF_CLASSES
#include "KaleidoscopeTypes.cpp.inc"
#define GET_OP_CLASSES
#include "KaleidoscopeOps.cpp.inc"
The selector macros tell each generated .inc file which section to provide at that include location.
Finally, register the generated type and operations when the dialect initializes:
void KaleidoscopeDialect::initialize() {
// The same generated .inc files also contain lists of every type and
// operation in the dialect. These macros select those lists so the dialect
// can register all generated classes with MLIR.
addTypes<
#define GET_TYPEDEF_LIST
#include "KaleidoscopeTypes.cpp.inc"
>();
addOperations<
#define GET_OP_LIST
#include "KaleidoscopeOps.cpp.inc"
>();
}
Once TheContext->loadDialect<kaleidoscope::KaleidoscopeDialect>() runs, MLIR knows how to create, parse, print, and verify everything in the dialect.
We can also delete the FunctionParameters map from Chapter 9. Each declaration now carries its own name, location, and argument number, so there is nothing to reconstruct later.
10.10 Lower the Dialect to the LLVM Dialect
When lowering time comes, we convert each kaleidoscope.* operation into operations in MLIR's LLVM dialect. Note carefully: the LLVM dialect is still MLIR. It is not LLVM IR. Translation to actual LLVM IR happens later in the pipeline, after all our lowering passes have run. The MLIR LLVM dialect is what we lower to here; the translation step is a separate, final stage.
A declaration becomes an allocation plus an initializing store:
Value One = LLVM::ConstantOp::create(Rewriter, Loc, Rewriter.getI64Type(),
Rewriter.getI64IntegerAttr(1));
Value Address =
LLVM::AllocaOp::create(Rewriter, Loc, PointerType, DoubleType, One, 0);
LLVM::StoreOp::create(Rewriter, Loc, Adaptor.getInitialValue(), Address);
We still have the source variable's name, location, and argument number in hand, and we've just produced its final stack address. That means we can create the debug declaration right here, without any reconstruction:
auto Variable = LLVM::DILocalVariableAttr::get(
Scope, Op.getName(), Scope.getFile(), Line, Op.getArgumentNumber(),
/*alignInBits=*/0, VariableType, LLVM::DIFlags::Zero);
LLVM::DbgDeclareOp::create(
Rewriter, Loc, Address, Variable,
LLVM::DIExpressionAttr::get(Rewriter.getContext()));
Reads and assignments become simple loads and stores:
Rewriter.replaceOpWithNewOp<LLVM::LoadOp>(Op, Rewriter.getF64Type(),
Adaptor.getVariable());
Rewriter.replaceOpWithNewOp<LLVM::StoreOp>(Op, Adaptor.getValue(),
Adaptor.getVariable());
Finally, tell MLIR that leaving a kaleidoscope.* operation unconverted is an error:
void runOnOperation() override {
...
ConversionTarget Target(Context);
Target.addIllegalDialect<kaleidoscope::KaleidoscopeDialect>();
Target.markUnknownOpDynamicallyLegal([](Operation *) { return true; });
RewritePatternSet Patterns(&Context);
Patterns.add<DeclareOpLowering, ReadOpLowering, AssignOpLowering>(Converter,
&Context);
if (failed(applyPartialConversion(getOperation(), Target,
std::move(Patterns))))
signalPassFailure();
}
Without addIllegalDialect, a buggy pattern could silently skip an operation and pass IR with dangling kaleidoscope ops to the LLVM dialect translation step. Declaring the dialect illegal makes that failure loud.
10.11 Wire Up the Pass Pipeline
The order matters here. First, lower the standard high-level dialects down to the LLVM dialect. Then run our Chapter 9 debug pass to create the compile unit and function scopes:
lowerToLLVM(const llvm::DataLayout &DataLayout) {
...
PassManager DebugPM(TheContext.get());
DebugPM.addPass(
createKaleidoscopeDebugInfoPass(InputFilename.getValue(), OptLevel));
...
}
With function scopes now available, we can run the variable-lowering pass, which creates both the stack storage and the variable debug declarations together:
lowerToLLVM(const llvm::DataLayout &DataLayout) {
...
DebugPM.addPass(std::make_unique<LowerKaleidoscopeVariablesPass>());
...
}
Then let MLIR's standard pass fill in the remaining scopes on the lowered operations:
lowerToLLVM(const llvm::DataLayout &DataLayout) {
...
LLVM::DIScopeForLLVMFuncOpPassOptions DebugOptions;
DebugOptions.emissionKind = LLVM::DIEmissionKind::Full;
DebugPM.addPass(
LLVM::createDIScopeForLLVMFuncOpPass(std::move(DebugOptions)));
...
}
After all MLIR passes have finished, the module is in the LLVM dialect. From there, the standard translation step turns it into LLVM IR:
%x = alloca double, i64 1, align 8
store double %arg0, ptr %x, align 8
#dbg_declare(ptr %x, !variable, !DIExpression(), !location)
Once we're at LLVM IR, everything downstream, including JIT, object emission, and DWARF generation, proceeds unchanged.
10.12 Try It Out
Configure and build the example first, setting MLIR_DIR to the directory containing MLIRConfig.cmake in your LLVM build:
cmake -S . -B build \
-DMLIR_DIR=/path/to/llvm-project/build/lib/cmake/mlir
cmake --build build
Now run the interpreter:
$ ./build/toyready> def incrementAndSquare(x) var y = x in (y = y + 1) * y; ready> incrementAndSquare(4);Evaluated to 25.000000
Now dump the MLIR before lowering to see your named variables:
$ ./build/toy --dump-mlirready> def incrementAndSquare(x) var y = x in (y = y + 1) * y;Read function definition:func.func @incrementAndSquare(%arg0: f64) -> f64 { %cst = arith.constant 1.000000e+00 : f64 %0 = kaleidoscope.var "x"(%arg0 : f64) {argumentNumber = 1 : i64} : !kaleidoscope.variable %1 = kaleidoscope.read(%0 : !kaleidoscope.variable) : f64 %2 = kaleidoscope.var "y"(%1 : f64) {argumentNumber = 0 : i64} : !kaleidoscope.variable %3 = kaleidoscope.read(%2 : !kaleidoscope.variable) : f64 %4 = arith.addf %3, %cst : f64 kaleidoscope.assign(%4 : f64, %2 : !kaleidoscope.variable) %5 = kaleidoscope.read(%2 : !kaleidoscope.variable) : f64 %6 = arith.mulf %4, %5 : f64 return %6 : f64 }
And the LLVM IR after lowering, where variables have become stack allocations with debug declarations attached:
$ ./build/toy --dump-llvm-irready> def incrementAndSquare(x) var y = x in (y = y + 1) * y;; ModuleID = 'LLVMDialectModule' source_filename = "LLVMDialectModule" target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32" define double @incrementAndSquare(double %0) !dbg !3 { %2 = alloca double, i64 1, align 8, !dbg !6 store double %0, ptr %2, align 8, !dbg !6 #dbg_declare(ptr %2, !7, !DIExpression(), !6) %3 = load double, ptr %2, align 8, !dbg !9 %4 = alloca double, i64 1, align 8, !dbg !10 store double %3, ptr %4, align 8, !dbg !10 #dbg_declare(ptr %4, !11, !DIExpression(), !10) %5 = load double, ptr %4, align 8, !dbg !12 %6 = fadd double %5, 1.000000e+00, !dbg !13 store double %6, ptr %4, align 8, !dbg !14 %7 = load double, ptr %4, align 8, !dbg !15 %8 = fmul double %6, %7, !dbg !16 ret double %8, !dbg !6 } ; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none) declare void @llvm.dbg.declare(metadata, metadata, metadata) #0 attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) } !llvm.dbg.cu = !{!0} !llvm.module.flags = !{!2} !0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "Kaleidoscope", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug) !1 = !DIFile(filename: "<stdin>", directory: "") !2 = !{i32 2, !"Debug Info Version", i32 3} !3 = distinct !DISubprogram(name: "incrementAndSquare", linkageName: "incrementAndSquare", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0) !4 = !DISubroutineType(cc: DW_CC_normal, types: !5) !5 = !{} !6 = !DILocation(line: 1, column: 5, scope: !3) !7 = !DILocalVariable(name: "x", arg: 1, scope: !3, file: !1, line: 1, type: !8) !8 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float) !9 = !DILocation(line: 1, column: 35, scope: !3) !10 = !DILocation(line: 1, column: 27, scope: !3) !11 = !DILocalVariable(name: "y", scope: !3, file: !1, line: 1, type: !8) !12 = !DILocation(line: 1, column: 45, scope: !3) !13 = !DILocation(line: 1, column: 47, scope: !3) !14 = !DILocation(line: 1, column: 43, scope: !3) !15 = !DILocation(line: 1, column: 54, scope: !3) !16 = !DILocation(line: 1, column: 52, scope: !3)
10.13 Where the Code Lives
The full implementation is spread across these files:
toy.cpp: the compiler and the variable-lowering passKaleidoscopeOps.td: the variable type and operation definitionsKaleidoscopeDialect.h/KaleidoscopeDialect.cpp: connects generated classes to the compilerKaleidoscopeDebugInfo.h/KaleidoscopeDebugInfo.cpp: creates compile-unit and function scopesCMakeLists.txt: runs TableGen and builds the executable
10.13.1 Build Configuration
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(kaleidoscope-chapter-10 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
find_package(MLIR REQUIRED CONFIG)
list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}")
include(TableGen)
include(AddMLIR)
include_directories(${MLIR_INCLUDE_DIRS})
# Every mlir_tablegen invocation below reads this TableGen source file.
set(LLVM_TARGET_DEFINITIONS KaleidoscopeOps.td)
# Generate the KaleidoscopeDialect class declaration and definition from the
# `def Kaleidoscope_Dialect : Dialect` record.
mlir_tablegen(KaleidoscopeDialect.h.inc -gen-dialect-decls
-dialect=kaleidoscope)
mlir_tablegen(KaleidoscopeDialect.cpp.inc -gen-dialect-defs
-dialect=kaleidoscope)
# Generate C++ declarations and definitions for our TypeDef records.
mlir_tablegen(KaleidoscopeTypes.h.inc -gen-typedef-decls
-typedefs-dialect=kaleidoscope)
mlir_tablegen(KaleidoscopeTypes.cpp.inc -gen-typedef-defs
-typedefs-dialect=kaleidoscope)
# Generate C++ declarations and definitions for our Op records.
mlir_tablegen(KaleidoscopeOps.h.inc -gen-op-decls)
mlir_tablegen(KaleidoscopeOps.cpp.inc -gen-op-defs)
# Give the generated fragments one build target so the executable can depend
# on all of them before compiling files that #include those fragments.
add_custom_target(KaleidoscopeOpsIncGen DEPENDS
KaleidoscopeDialect.h.inc
KaleidoscopeDialect.cpp.inc
KaleidoscopeTypes.h.inc
KaleidoscopeTypes.cpp.inc
KaleidoscopeOps.h.inc
KaleidoscopeOps.cpp.inc
)
add_executable(toy
toy.cpp
KaleidoscopeDialect.cpp
KaleidoscopeDebugInfo.cpp
)
add_dependencies(toy KaleidoscopeOpsIncGen)
# Make symbols in the executable available to the JIT for runtime lookup.
set_target_properties(toy PROPERTIES ENABLE_EXPORTS ON)
target_include_directories(toy PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${LLVM_INCLUDE_DIRS}
${MLIR_INCLUDE_DIRS}
)
target_compile_definitions(toy PRIVATE ${LLVM_DEFINITIONS})
if(NOT LLVM_ENABLE_RTTI)
target_compile_options(toy PRIVATE -fno-rtti)
endif()
llvm_map_components_to_libnames(LLVM_LIBS
Core
CodeGen
OrcJIT
Support
Target
${LLVM_TARGETS_TO_BUILD}
)
target_link_libraries(toy PRIVATE
MLIRArithToLLVM
MLIRArithDialect
MLIRBuiltinToLLVMIRTranslation
MLIRControlFlowDialect
MLIRControlFlowToLLVM
MLIRFuncToLLVM
MLIRFuncDialect
MLIRLLVMDialect
MLIRLLVMIRTransforms
MLIRLLVMToLLVMIRTranslation
MLIRMemRefDialect
MLIRMemRefToLLVM
MLIRReconcileUnrealizedCasts
MLIRSCFDialect
MLIRSCFToControlFlow
MLIRTargetLLVMIRExport
MLIRTransforms
${LLVM_LIBS}
)
10.13.2 Compiler
// toy.cpp
#include "../include/KaleidoscopeJIT.h"
#include "KaleidoscopeDebugInfo.h"
#include "KaleidoscopeDialect.h"
#include "mlir/Conversion/ArithToLLVM/ArithToLLVM.h"
#include "mlir/Conversion/ControlFlowToLLVM/ControlFlowToLLVM.h"
#include "mlir/Conversion/FuncToLLVM/ConvertFuncToLLVMPass.h"
#include "mlir/Conversion/LLVMCommon/TypeConverter.h"
#include "mlir/Conversion/MemRefToLLVM/MemRefToLLVM.h"
#include "mlir/Conversion/ReconcileUnrealizedCasts/ReconcileUnrealizedCasts.h"
#include "mlir/Conversion/SCFToControlFlow/SCFToControlFlow.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/Dialect/LLVMIR/Transforms/Passes.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/MLIRContext.h"
#include "mlir/IR/OperationSupport.h"
#include "mlir/IR/Verifier.h"
#include "mlir/Pass/PassManager.h"
#include "mlir/Target/LLVMIR/Dialect/Builtin/BuiltinToLLVMIRTranslation.h"
#include "mlir/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.h"
#include "mlir/Target/LLVMIR/Export.h"
#include "mlir/Transforms/DialectConversion.h"
#include "mlir/Transforms/Passes.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/BinaryFormat/Dwarf.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Target/TargetMachine.h"
#include "llvm/Target/TargetOptions.h"
#include "llvm/TargetParser/Host.h"
#include <cassert>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <system_error>
#include <utility>
#include <vector>
using namespace mlir;
//===----------------------------------------------------------------------===//
// Lexer
//===----------------------------------------------------------------------===//
// The lexer returns tokens [0-255] if it is an unknown character, otherwise one
// of these for known things.
enum Token {
tok_eof = -1,
// commands
tok_def = -2,
tok_extern = -3,
// primary
tok_identifier = -4,
tok_number = -5,
// control
tok_if = -6,
tok_then = -7,
tok_else = -8,
tok_for = -9,
tok_in = -10,
// operators
tok_binary = -11,
tok_unary = -12,
// var definition
tok_var = -13
};
struct SourceLocation {
int Line;
int Col;
};
static SourceLocation CurLoc;
static SourceLocation LexLoc = {1, 0};
static int advance() {
int LastChar = getchar();
if (LastChar == '\n' || LastChar == '\r') {
++LexLoc.Line;
LexLoc.Col = 0;
} else {
++LexLoc.Col;
}
return LastChar;
}
static std::string IdentifierStr; // Filled in for identifiers and keywords
static double NumVal; // Filled in if tok_number
/// gettok - Return the next token from standard input.
static int gettok() {
static int LastChar = ' ';
// Skip any whitespace.
while (isspace(LastChar))
LastChar = advance();
CurLoc = LexLoc;
if (isalpha(LastChar)) { // identifier: [a-zA-Z][a-zA-Z0-9]*
IdentifierStr = LastChar;
while (isalnum((LastChar = advance())))
IdentifierStr += LastChar;
if (IdentifierStr == "def")
return tok_def;
if (IdentifierStr == "extern")
return tok_extern;
if (IdentifierStr == "if")
return tok_if;
if (IdentifierStr == "then")
return tok_then;
if (IdentifierStr == "else")
return tok_else;
if (IdentifierStr == "for")
return tok_for;
if (IdentifierStr == "in")
return tok_in;
if (IdentifierStr == "binary")
return tok_binary;
if (IdentifierStr == "unary")
return tok_unary;
if (IdentifierStr == "var")
return tok_var;
return tok_identifier;
}
if (isdigit(LastChar) || LastChar == '.') { // Number: [0-9.]+
std::string NumStr;
do {
NumStr += LastChar;
LastChar = advance();
} while (isdigit(LastChar) || LastChar == '.');
NumVal = strtod(NumStr.c_str(), nullptr);
return tok_number;
}
if (LastChar == '#') {
// Comment until end of line.
do
LastChar = advance();
while (LastChar != EOF && LastChar != '\n' && LastChar != '\r');
if (LastChar != EOF)
return gettok();
}
// Check for end of file. Don't eat the EOF.
if (LastChar == EOF)
return tok_eof;
// Otherwise, just return the character as its ascii value.
int ThisChar = LastChar;
LastChar = advance();
return ThisChar;
}
//===----------------------------------------------------------------------===//
// Abstract Syntax Tree (aka Parse Tree)
//===----------------------------------------------------------------------===//
namespace {
/// ExprAST - Base class for all expression nodes.
class ExprAST {
SourceLocation Loc;
public:
ExprAST(SourceLocation Loc = CurLoc) : Loc(Loc) {}
virtual ~ExprAST() = default;
virtual Value codegen() = 0;
virtual const std::string *getVariableName() const { return nullptr; }
SourceLocation getSourceLocation() const { return Loc; }
};
/// NumberExprAST - Expression class for numeric literals like "1.0".
class NumberExprAST : public ExprAST {
double Val;
public:
NumberExprAST(double Val) : Val(Val) {}
Value codegen() override;
};
/// VariableExprAST - Expression class for referencing a variable, like "a".
class VariableExprAST : public ExprAST {
std::string Name;
public:
VariableExprAST(SourceLocation Loc, const std::string &Name)
: ExprAST(Loc), Name(Name) {}
Value codegen() override;
const std::string *getVariableName() const override { return &Name; }
};
/// UnaryExprAST - Expression class for a unary operator.
class UnaryExprAST : public ExprAST {
char Opcode;
std::unique_ptr<ExprAST> Operand;
public:
UnaryExprAST(SourceLocation Loc, char Opcode,
std::unique_ptr<ExprAST> Operand)
: ExprAST(Loc), Opcode(Opcode), Operand(std::move(Operand)) {}
Value codegen() override;
};
/// BinaryExprAST - Expression class for a binary operator.
class BinaryExprAST : public ExprAST {
char Op;
std::unique_ptr<ExprAST> LHS, RHS;
public:
BinaryExprAST(SourceLocation Loc, char Op, std::unique_ptr<ExprAST> LHS,
std::unique_ptr<ExprAST> RHS)
: ExprAST(Loc), Op(Op), LHS(std::move(LHS)), RHS(std::move(RHS)) {}
Value codegen() override;
};
/// CallExprAST - Expression class for function calls.
class CallExprAST : public ExprAST {
std::string Callee;
std::vector<std::unique_ptr<ExprAST>> Args;
public:
CallExprAST(SourceLocation Loc, const std::string &Callee,
std::vector<std::unique_ptr<ExprAST>> Args)
: ExprAST(Loc), Callee(Callee), Args(std::move(Args)) {}
Value codegen() override;
};
/// IfExprAST - Expression class for if/then/else.
class IfExprAST : public ExprAST {
std::unique_ptr<ExprAST> Cond, Then, Else;
public:
IfExprAST(SourceLocation Loc, std::unique_ptr<ExprAST> Cond,
std::unique_ptr<ExprAST> Then, std::unique_ptr<ExprAST> Else)
: ExprAST(Loc), Cond(std::move(Cond)), Then(std::move(Then)),
Else(std::move(Else)) {}
Value codegen() override;
};
/// ForExprAST - Expression class for for/in.
class ForExprAST : public ExprAST {
std::string VarName;
std::unique_ptr<ExprAST> Start, End, Step, Body;
public:
ForExprAST(SourceLocation Loc, const std::string &VarName,
std::unique_ptr<ExprAST> Start, std::unique_ptr<ExprAST> End,
std::unique_ptr<ExprAST> Step, std::unique_ptr<ExprAST> Body)
: ExprAST(Loc), VarName(VarName), Start(std::move(Start)),
End(std::move(End)), Step(std::move(Step)), Body(std::move(Body)) {}
Value codegen() override;
};
/// VarExprAST - Expression class for var/in.
class VarExprAST : public ExprAST {
std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> VarNames;
std::unique_ptr<ExprAST> Body;
public:
VarExprAST(
SourceLocation Loc,
std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> VarNames,
std::unique_ptr<ExprAST> Body)
: ExprAST(Loc), VarNames(std::move(VarNames)), Body(std::move(Body)) {}
Value codegen() override;
};
/// PrototypeAST - This class represents the "prototype" for a function,
/// which captures its argument names as well as if it is an operator.
class PrototypeAST {
std::string Name;
std::vector<std::string> Args;
bool IsOperator;
unsigned Precedence; // Precedence if a binary op.
SourceLocation Loc;
public:
PrototypeAST(SourceLocation Loc, const std::string &Name,
std::vector<std::string> Args, bool IsOperator = false,
unsigned Prec = 0)
: Name(Name), Args(std::move(Args)), IsOperator(IsOperator),
Precedence(Prec), Loc(Loc) {}
func::FuncOp codegen();
const std::string &getName() const { return Name; }
const std::vector<std::string> &getArgs() const { return Args; }
bool isUnaryOp() const { return IsOperator && Args.size() == 1; }
bool isBinaryOp() const { return IsOperator && Args.size() == 2; }
char getOperatorName() const {
assert(isUnaryOp() || isBinaryOp());
return Name.back();
}
unsigned getBinaryPrecedence() const { return Precedence; }
SourceLocation getSourceLocation() const { return Loc; }
};
/// FunctionAST - This class represents a function definition itself.
class FunctionAST {
std::unique_ptr<PrototypeAST> Proto;
std::unique_ptr<ExprAST> Body;
public:
FunctionAST(std::unique_ptr<PrototypeAST> Proto,
std::unique_ptr<ExprAST> Body)
: Proto(std::move(Proto)), Body(std::move(Body)) {}
func::FuncOp codegen();
};
} // end anonymous namespace
//===----------------------------------------------------------------------===//
// Parser
//===----------------------------------------------------------------------===//
/// CurTok/getNextToken - Provide a simple token buffer. CurTok is the current
/// token the parser is looking at. getNextToken reads another token from the
/// lexer and updates CurTok with its results.
static int CurTok;
static int getNextToken() { return CurTok = gettok(); }
/// BinopPrecedence - This holds the precedence for each binary operator that is
/// defined.
static std::map<char, int> BinopPrecedence;
/// GetTokPrecedence - Get the precedence of the pending binary operator token.
static int GetTokPrecedence() {
if (!isascii(CurTok))
return -1;
// Make sure it's a declared binop.
int TokPrec = BinopPrecedence[CurTok];
if (TokPrec <= 0)
return -1;
return TokPrec;
}
/// LogError* - These are little helper functions for error handling.
std::unique_ptr<ExprAST> LogError(const char *Str) {
fprintf(stderr, "Error: %s\n", Str);
return nullptr;
}
std::unique_ptr<PrototypeAST> LogErrorP(const char *Str) {
LogError(Str);
return nullptr;
}
static std::unique_ptr<ExprAST> ParseExpression();
/// numberexpr ::= number
static std::unique_ptr<ExprAST> ParseNumberExpr() {
auto Result = std::make_unique<NumberExprAST>(NumVal);
getNextToken(); // consume the number
return std::move(Result);
}
/// parenexpr ::= '(' expression ')'
static std::unique_ptr<ExprAST> ParseParenExpr() {
getNextToken(); // eat (.
auto V = ParseExpression();
if (!V)
return nullptr;
if (CurTok != ')')
return LogError("expected ')'");
getNextToken(); // eat ).
return V;
}
/// identifierexpr
/// ::= identifier
/// ::= identifier '(' expression* ')'
static std::unique_ptr<ExprAST> ParseIdentifierExpr() {
std::string IdName = IdentifierStr;
SourceLocation IdLoc = CurLoc;
getNextToken(); // eat identifier.
if (CurTok != '(') // Simple variable ref.
return std::make_unique<VariableExprAST>(IdLoc, IdName);
// Call.
getNextToken(); // eat (
std::vector<std::unique_ptr<ExprAST>> Args;
if (CurTok != ')') {
while (true) {
if (auto Arg = ParseExpression())
Args.push_back(std::move(Arg));
else
return nullptr;
if (CurTok == ')')
break;
if (CurTok != ',')
return LogError("Expected ')' or ',' in argument list");
getNextToken();
}
}
// Eat the ')'.
getNextToken();
return std::make_unique<CallExprAST>(IdLoc, IdName, std::move(Args));
}
/// ifexpr ::= 'if' expression 'then' expression 'else' expression
static std::unique_ptr<ExprAST> ParseIfExpr() {
SourceLocation IfLoc = CurLoc;
getNextToken(); // eat the if.
auto Cond = ParseExpression();
if (!Cond)
return nullptr;
if (CurTok != tok_then)
return LogError("expected then");
getNextToken(); // eat the then.
auto Then = ParseExpression();
if (!Then)
return nullptr;
if (CurTok != tok_else)
return LogError("expected else");
getNextToken(); // eat the else.
auto Else = ParseExpression();
if (!Else)
return nullptr;
return std::make_unique<IfExprAST>(IfLoc, std::move(Cond), std::move(Then),
std::move(Else));
}
/// forexpr ::= 'for' identifier '=' expr ',' expr (',' expr)? 'in' expression
static std::unique_ptr<ExprAST> ParseForExpr() {
SourceLocation ForLoc = CurLoc;
getNextToken(); // eat the for.
if (CurTok != tok_identifier)
return LogError("expected identifier after for");
std::string IdName = IdentifierStr;
getNextToken(); // eat identifier.
if (CurTok != '=')
return LogError("expected '=' after for");
getNextToken(); // eat '='.
auto Start = ParseExpression();
if (!Start)
return nullptr;
if (CurTok != ',')
return LogError("expected ',' after for start value");
getNextToken();
auto End = ParseExpression();
if (!End)
return nullptr;
// The step value is optional.
std::unique_ptr<ExprAST> Step;
if (CurTok == ',') {
getNextToken();
Step = ParseExpression();
if (!Step)
return nullptr;
}
if (CurTok != tok_in)
return LogError("expected 'in' after for");
getNextToken(); // eat the in.
auto Body = ParseExpression();
if (!Body)
return nullptr;
return std::make_unique<ForExprAST>(ForLoc, IdName, std::move(Start),
std::move(End), std::move(Step),
std::move(Body));
}
/// varexpr ::= 'var' identifier ('=' expression)?
/// (',' identifier ('=' expression)?)* 'in' expression
static std::unique_ptr<ExprAST> ParseVarExpr() {
SourceLocation VarLoc = CurLoc;
getNextToken(); // eat the var.
std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> VarNames;
if (CurTok != tok_identifier)
return LogError("expected identifier after var");
while (true) {
std::string Name = IdentifierStr;
getNextToken(); // eat identifier.
std::unique_ptr<ExprAST> Init;
if (CurTok == '=') {
getNextToken(); // eat '='.
Init = ParseExpression();
if (!Init)
return nullptr;
}
VarNames.emplace_back(Name, std::move(Init));
if (CurTok != ',')
break;
getNextToken(); // eat ','.
if (CurTok != tok_identifier)
return LogError("expected identifier list after var");
}
if (CurTok != tok_in)
return LogError("expected 'in' keyword after 'var'");
getNextToken(); // eat 'in'.
auto Body = ParseExpression();
if (!Body)
return nullptr;
return std::make_unique<VarExprAST>(VarLoc, std::move(VarNames),
std::move(Body));
}
/// primary
/// ::= identifierexpr
/// ::= numberexpr
/// ::= parenexpr
/// ::= ifexpr
/// ::= forexpr
/// ::= varexpr
static std::unique_ptr<ExprAST> ParsePrimary() {
switch (CurTok) {
default:
return LogError("unknown token when expecting an expression");
case tok_identifier:
return ParseIdentifierExpr();
case tok_number:
return ParseNumberExpr();
case '(':
return ParseParenExpr();
case tok_if:
return ParseIfExpr();
case tok_for:
return ParseForExpr();
case tok_var:
return ParseVarExpr();
}
}
/// unary
/// ::= primary
/// ::= '!' unary
static std::unique_ptr<ExprAST> ParseUnary() {
// If the current token is not an operator, it must be a primary expression.
if (!isascii(CurTok) || CurTok == '(' || CurTok == ',')
return ParsePrimary();
// If this is a unary operator, read it.
int Opc = CurTok;
SourceLocation UnaryLoc = CurLoc;
getNextToken();
if (auto Operand = ParseUnary())
return std::make_unique<UnaryExprAST>(UnaryLoc, Opc, std::move(Operand));
return nullptr;
}
/// binoprhs
/// ::= ('+' unary)*
static std::unique_ptr<ExprAST> ParseBinOpRHS(int ExprPrec,
std::unique_ptr<ExprAST> LHS) {
// If this is a binop, find its precedence.
while (true) {
int TokPrec = GetTokPrecedence();
// If this is a binop that binds at least as tightly as the current binop,
// consume it, otherwise we are done.
if (TokPrec < ExprPrec)
return LHS;
// Okay, we know this is a binop.
int BinOp = CurTok;
SourceLocation BinLoc = CurLoc;
getNextToken(); // eat binop
// Parse the unary expression after the binary operator.
auto RHS = ParseUnary();
if (!RHS)
return nullptr;
// If BinOp binds less tightly with RHS than the operator after RHS, let
// the pending operator take RHS as its LHS.
int NextPrec = GetTokPrecedence();
if (TokPrec < NextPrec) {
RHS = ParseBinOpRHS(TokPrec + 1, std::move(RHS));
if (!RHS)
return nullptr;
}
// Merge LHS/RHS.
LHS = std::make_unique<BinaryExprAST>(BinLoc, BinOp, std::move(LHS),
std::move(RHS));
}
}
/// expression
/// ::= unary binoprhs
///
static std::unique_ptr<ExprAST> ParseExpression() {
auto LHS = ParseUnary();
if (!LHS)
return nullptr;
return ParseBinOpRHS(0, std::move(LHS));
}
/// prototype
/// ::= id '(' id* ')'
/// ::= binary LETTER number? (id, id)
/// ::= unary LETTER (id)
static std::unique_ptr<PrototypeAST> ParsePrototype() {
SourceLocation FnLoc = CurLoc;
std::string FnName;
unsigned Kind = 0; // 0 = identifier, 1 = unary, 2 = binary.
unsigned BinaryPrecedence = 30;
switch (CurTok) {
default:
return LogErrorP("Expected function name in prototype");
case tok_identifier:
FnName = IdentifierStr;
getNextToken();
break;
case tok_unary:
getNextToken();
if (!isascii(CurTok))
return LogErrorP("Expected unary operator");
FnName = "unary";
FnName += static_cast<char>(CurTok);
Kind = 1;
getNextToken();
break;
case tok_binary:
getNextToken();
if (!isascii(CurTok))
return LogErrorP("Expected binary operator");
FnName = "binary";
FnName += static_cast<char>(CurTok);
Kind = 2;
getNextToken();
// Read the precedence if present.
if (CurTok == tok_number) {
if (NumVal < 1 || NumVal > 100)
return LogErrorP("Invalid precedence: must be 1..100");
BinaryPrecedence = static_cast<unsigned>(NumVal);
getNextToken();
}
break;
}
if (CurTok != '(')
return LogErrorP("Expected '(' in prototype");
std::vector<std::string> ArgNames;
while (getNextToken() == tok_identifier)
ArgNames.push_back(IdentifierStr);
if (CurTok != ')')
return LogErrorP("Expected ')' in prototype");
// success.
getNextToken(); // eat ')'.
// Verify the right number of names for an operator.
if (Kind && ArgNames.size() != Kind)
return LogErrorP("Invalid number of operands for operator");
return std::make_unique<PrototypeAST>(FnLoc, FnName, std::move(ArgNames),
Kind != 0, BinaryPrecedence);
}
/// definition ::= 'def' prototype expression
static std::unique_ptr<FunctionAST> ParseDefinition() {
getNextToken(); // eat def.
auto Proto = ParsePrototype();
if (!Proto)
return nullptr;
if (auto E = ParseExpression())
return std::make_unique<FunctionAST>(std::move(Proto), std::move(E));
return nullptr;
}
/// toplevelexpr ::= expression
static std::unique_ptr<FunctionAST> ParseTopLevelExpr() {
SourceLocation FnLoc = CurLoc;
if (auto E = ParseExpression()) {
// Make an anonymous proto.
auto Proto = std::make_unique<PrototypeAST>(FnLoc, "__anon_expr",
std::vector<std::string>());
return std::make_unique<FunctionAST>(std::move(Proto), std::move(E));
}
return nullptr;
}
/// external ::= 'extern' prototype
static std::unique_ptr<PrototypeAST> ParseExtern() {
getNextToken(); // eat extern.
return ParsePrototype();
}
//===----------------------------------------------------------------------===//
// Code Generation
//===----------------------------------------------------------------------===//
static std::unique_ptr<MLIRContext> TheContext;
static OwningOpRef<ModuleOp> TheModule;
static std::unique_ptr<OpBuilder> TheBuilder;
static std::unique_ptr<PassManager> ThePM;
static std::map<std::string, Value> NamedValues;
static std::unique_ptr<llvm::orc::KaleidoscopeJIT> TheJIT;
static std::map<std::string, std::unique_ptr<PrototypeAST>> FunctionProtos;
static llvm::ExitOnError ExitOnErr;
static llvm::cl::opt<bool> DumpMLIR("dump-mlir",
llvm::cl::desc("Print generated MLIR"),
llvm::cl::init(false));
static llvm::cl::opt<bool> EmitObject(
"emit-object",
llvm::cl::desc(
"Compile an input file to an object instead of using the JIT"),
llvm::cl::init(false));
static llvm::cl::opt<std::string> InputFilename(llvm::cl::Positional,
llvm::cl::desc("<input file>"),
llvm::cl::init(""));
static llvm::cl::opt<std::string>
TargetTripleOption("target",
llvm::cl::desc("Target triple for object emission"),
llvm::cl::value_desc("triple"), llvm::cl::init(""));
static llvm::cl::opt<std::string>
OutputFilename("o", llvm::cl::desc("Output filename"),
llvm::cl::value_desc("filename"), llvm::cl::init(""));
static llvm::cl::opt<char>
OptLevel("O", llvm::cl::desc("Optimization level: -O0, -O1, -O2, or -O3"),
llvm::cl::Prefix, llvm::cl::init('2'));
static llvm::cl::opt<bool>
DumpLLVMIR("dump-llvm-ir",
llvm::cl::desc("Print LLVM IR before emitting the object file"),
llvm::cl::init(false));
static SourceLocation CodegenLoc = {1, 1};
static Location getLocation() {
llvm::StringRef Filename = "<stdin>";
if (!InputFilename.empty())
Filename = InputFilename.getValue();
return FileLineColLoc::get(TheContext.get(), Filename, CodegenLoc.Line,
CodegenLoc.Col);
}
class LocationGuard {
SourceLocation Previous;
public:
explicit LocationGuard(SourceLocation Loc) : Previous(CodegenLoc) {
CodegenLoc = Loc;
}
~LocationGuard() { CodegenLoc = Previous; }
};
Value LogErrorV(const char *Str) {
LogError(Str);
return {};
}
func::FuncOp getFunction(const std::string &Name) {
// First, see if the function has already been added to the current module.
if (auto Function = TheModule->lookupSymbol<func::FuncOp>(Name))
return Function;
// If not, codegen the declaration from an existing prototype.
auto It = FunctionProtos.find(Name);
if (It != FunctionProtos.end()) {
auto Function = It->second->codegen();
Function.setPrivate();
return Function;
}
return {};
}
static func::FuncOp getCurrentFunction() {
Operation *Parent = TheBuilder->getInsertionBlock()->getParentOp();
if (auto Function = dyn_cast<func::FuncOp>(Parent))
return Function;
return Parent->getParentOfType<func::FuncOp>();
}
/// CreateVariable - Preserve a source variable until dialect lowering.
static Value CreateVariable(StringRef Name, Value InitialValue,
int64_t ArgumentNumber = 0) {
return TheBuilder->create<kaleidoscope::DeclareOp>(
getLocation(), kaleidoscope::VariableType::get(TheContext.get()),
InitialValue, TheBuilder->getStringAttr(Name),
TheBuilder->getI64IntegerAttr(ArgumentNumber));
}
Value NumberExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
return TheBuilder->create<arith::ConstantOp>(
getLocation(), TheBuilder->getF64FloatAttr(Val));
}
Value VariableExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
// Look this variable up in the function.
auto It = NamedValues.find(Name);
if (It == NamedValues.end())
return LogErrorV("Unknown variable name");
return TheBuilder->create<kaleidoscope::ReadOp>(
getLocation(), TheBuilder->getF64Type(), It->second);
}
Value UnaryExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
Value OperandV = Operand->codegen();
if (!OperandV)
return {};
auto Operator = getFunction(std::string("unary") + Opcode);
if (!Operator)
return LogErrorV("Unknown unary operator");
return TheBuilder->create<func::CallOp>(getLocation(), Operator, OperandV)
.getResult(0);
}
Value BinaryExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
// Assignment stores into the variable's mutable memref slot.
if (Op == '=') {
const std::string *Name = LHS->getVariableName();
if (!Name)
return LogErrorV("destination of '=' must be a variable");
Value AssignedValue = RHS->codegen();
if (!AssignedValue)
return {};
auto It = NamedValues.find(*Name);
if (It == NamedValues.end())
return LogErrorV("Unknown variable name");
TheBuilder->create<kaleidoscope::AssignOp>(getLocation(), It->second,
AssignedValue);
return AssignedValue;
}
Value L = LHS->codegen();
Value R = RHS->codegen();
if (!L || !R)
return {};
switch (Op) {
case '+':
return TheBuilder->create<arith::AddFOp>(getLocation(), L, R);
case '-':
return TheBuilder->create<arith::SubFOp>(getLocation(), L, R);
case '*':
return TheBuilder->create<arith::MulFOp>(getLocation(), L, R);
case '<': {
Value Comparison = TheBuilder->create<arith::CmpFOp>(
getLocation(), arith::CmpFPredicate::ULT, L, R);
// Convert bool 0/1 to double 0.0 or 1.0.
return TheBuilder->create<arith::UIToFPOp>(
getLocation(), TheBuilder->getF64Type(), Comparison);
}
default:
break;
}
// If it wasn't a builtin binary operator, it must be a user-defined one.
auto Operator = getFunction(std::string("binary") + Op);
if (!Operator)
return LogErrorV("Unknown binary operator");
Value Operands[] = {L, R};
return TheBuilder->create<func::CallOp>(getLocation(), Operator, Operands)
.getResult(0);
}
Value CallExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
// Look up the name in the global module table.
auto CalleeF = getFunction(Callee);
if (!CalleeF)
return LogErrorV("Unknown function referenced");
// If argument mismatch error.
if (CalleeF.getNumArguments() != Args.size())
return LogErrorV("Incorrect # arguments passed");
std::vector<Value> ArgsV;
for (auto &Arg : Args) {
ArgsV.push_back(Arg->codegen());
if (!ArgsV.back())
return {};
}
return TheBuilder->create<func::CallOp>(getLocation(), CalleeF, ArgsV)
.getResult(0);
}
Value IfExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
Value CondV = Cond->codegen();
if (!CondV)
return {};
// Convert the condition to a boolean by comparing it with 0.0.
Value Zero = TheBuilder->create<arith::ConstantOp>(
getLocation(), TheBuilder->getF64FloatAttr(0.0));
CondV = TheBuilder->create<arith::CmpFOp>(
getLocation(), arith::CmpFPredicate::ONE, CondV, Zero);
bool CodegenFailed = false;
auto IfOp = TheBuilder->create<scf::IfOp>(
getLocation(), CondV,
[&](OpBuilder &Builder, Location Loc) {
Value ThenV = Then->codegen();
if (!ThenV) {
CodegenFailed = true;
ThenV = Builder.create<arith::ConstantOp>(
Loc, Builder.getF64FloatAttr(0.0));
}
Builder.create<scf::YieldOp>(Loc, ThenV);
},
[&](OpBuilder &Builder, Location Loc) {
Value ElseV = Else->codegen();
if (!ElseV) {
CodegenFailed = true;
ElseV = Builder.create<arith::ConstantOp>(
Loc, Builder.getF64FloatAttr(0.0));
}
Builder.create<scf::YieldOp>(Loc, ElseV);
});
if (CodegenFailed)
return {};
return IfOp.getResult(0);
}
Value ForExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
// Emit the start value before putting the loop variable in scope.
Value StartVal = Start->codegen();
if (!StartVal)
return {};
Value Variable = CreateVariable(VarName, StartVal);
auto OldValue = NamedValues.find(VarName);
bool HadOldValue = OldValue != NamedValues.end();
Value SavedValue = HadOldValue ? OldValue->second : Value();
NamedValues[VarName] = Variable;
bool CodegenFailed = false;
// Test the condition before each iteration, then emit the body and step.
TheBuilder->create<scf::WhileOp>(
getLocation(), TypeRange{}, ValueRange{},
[&](OpBuilder &Builder, Location Loc, ValueRange) {
Value EndCond = End->codegen();
if (!EndCond) {
CodegenFailed = true;
EndCond = Builder.create<arith::ConstantOp>(
Loc, Builder.getF64FloatAttr(0.0));
}
Value Zero = Builder.create<arith::ConstantOp>(
Loc, Builder.getF64FloatAttr(0.0));
EndCond = Builder.create<arith::CmpFOp>(Loc, arith::CmpFPredicate::ONE,
EndCond, Zero);
Builder.create<scf::ConditionOp>(Loc, EndCond, ValueRange{});
},
[&](OpBuilder &Builder, Location Loc, ValueRange) {
if (!Body->codegen())
CodegenFailed = true;
Value StepVal;
if (Step)
StepVal = Step->codegen();
else
StepVal = Builder.create<arith::ConstantOp>(
Loc, Builder.getF64FloatAttr(1.0));
if (!StepVal) {
CodegenFailed = true;
StepVal = Builder.create<arith::ConstantOp>(
Loc, Builder.getF64FloatAttr(1.0));
}
// Reload after the body and step in case either mutated the variable.
Value Current = Builder.create<kaleidoscope::ReadOp>(
Loc, Builder.getF64Type(), Variable);
Value NextVar = Builder.create<arith::AddFOp>(Loc, Current, StepVal);
Builder.create<kaleidoscope::AssignOp>(Loc, Variable, NextVar);
Builder.create<scf::YieldOp>(Loc);
});
// Restore any variable shadowed by the loop induction variable.
if (HadOldValue)
NamedValues[VarName] = SavedValue;
else
NamedValues.erase(VarName);
if (CodegenFailed)
return {};
// A for expression always returns 0.0.
return TheBuilder->create<arith::ConstantOp>(
getLocation(), TheBuilder->getF64FloatAttr(0.0));
}
Value VarExprAST::codegen() {
LocationGuard Guard(getSourceLocation());
std::vector<std::pair<std::string, std::optional<Value>>> OldBindings;
auto RestoreBindings = [&]() {
for (auto It = OldBindings.rbegin(); It != OldBindings.rend(); ++It) {
if (It->second)
NamedValues[It->first] = *It->second;
else
NamedValues.erase(It->first);
}
};
for (auto &Variable : VarNames) {
const std::string &Name = Variable.first;
// Generate the initializer before introducing the new binding.
Value InitialValue;
if (Variable.second)
InitialValue = Variable.second->codegen();
else
InitialValue = TheBuilder->create<arith::ConstantOp>(
getLocation(), TheBuilder->getF64FloatAttr(0.0));
if (!InitialValue) {
RestoreBindings();
return {};
}
Value Storage = CreateVariable(Name, InitialValue);
auto Old = NamedValues.find(Name);
OldBindings.emplace_back(Name, Old == NamedValues.end()
? std::optional<Value>()
: std::optional<Value>(Old->second));
NamedValues[Name] = Storage;
}
Value BodyValue = Body->codegen();
RestoreBindings();
return BodyValue;
}
func::FuncOp PrototypeAST::codegen() {
LocationGuard Guard(getSourceLocation());
// Make the function type: double(double, double), etc.
std::vector<Type> Doubles(Args.size(), TheBuilder->getF64Type());
auto FunctionType =
TheBuilder->getFunctionType(Doubles, {TheBuilder->getF64Type()});
auto Function = func::FuncOp::create(getLocation(), Name, FunctionType);
TheModule->push_back(Function);
return Function;
}
func::FuncOp FunctionAST::codegen() {
// Save the prototype so declarations can be emitted in later modules.
auto &P = *Proto;
LocationGuard Guard(P.getSourceLocation());
FunctionProtos[Proto->getName()] = std::move(Proto);
auto TheFunction = getFunction(P.getName());
if (!TheFunction)
return {};
if (!TheFunction.isDeclaration()) {
LogError("Function cannot be redefined.");
return {};
}
// A definition is visible outside the module, even if an earlier extern
// declaration created the function with private symbol visibility.
TheFunction.setPublic();
// If this is a binary operator, install its precedence.
if (P.isBinaryOp())
BinopPrecedence[P.getOperatorName()] = P.getBinaryPrecedence();
// Create a new basic block to start insertion into.
Block *EntryBlock = TheFunction.addEntryBlock();
TheBuilder->setInsertionPointToStart(EntryBlock);
// Give each function argument a mutable storage slot.
NamedValues.clear();
unsigned Index = 0;
for (BlockArgument Argument : TheFunction.getArguments()) {
StringRef Name = P.getArgs()[Index];
Value Storage = CreateVariable(Name, Argument, Index + 1);
NamedValues[Name.str()] = Storage;
++Index;
}
if (Value RetVal = Body->codegen()) {
// Finish off the function.
TheBuilder->create<func::ReturnOp>(getLocation(), RetVal);
// Validate the generated code, checking for consistency.
if (succeeded(verify(TheFunction))) {
// Run the optimizer on the module.
if (failed(ThePM->run(*TheModule))) {
LogError("Could not optimize function.");
TheFunction.erase();
if (P.isBinaryOp())
BinopPrecedence.erase(P.getOperatorName());
return {};
}
return TheFunction;
}
}
// Error reading body, remove function.
TheFunction.erase();
if (P.isBinaryOp())
BinopPrecedence.erase(P.getOperatorName());
return {};
}
//===----------------------------------------------------------------------===//
// Top-Level parsing and JIT Driver
//===----------------------------------------------------------------------===//
static void InitializeModuleAndManagers() {
// Destroy objects that refer to the old context before replacing it.
ThePM.reset();
TheBuilder.reset();
TheModule = OwningOpRef<ModuleOp>();
TheContext.reset();
// Open a new context and module.
TheContext = std::make_unique<MLIRContext>();
TheContext->loadDialect<arith::ArithDialect, cf::ControlFlowDialect,
func::FuncDialect, kaleidoscope::KaleidoscopeDialect,
memref::MemRefDialect, scf::SCFDialect>();
TheModule = ModuleOp::create(UnknownLoc::get(TheContext.get()));
// Create a new builder for the module.
TheBuilder = std::make_unique<OpBuilder>(TheContext.get());
// Create a pass manager and enable our simple optimizations above -O0.
ThePM = std::make_unique<PassManager>(TheContext.get());
if (OptLevel != '0') {
ThePM->addNestedPass<func::FuncOp>(createCanonicalizerPass());
ThePM->addNestedPass<func::FuncOp>(createCSEPass());
}
}
struct LoweredModule {
std::unique_ptr<llvm::LLVMContext> Context;
std::unique_ptr<llvm::Module> Module;
};
class DeclareOpLowering : public OpConversionPattern<kaleidoscope::DeclareOp> {
public:
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(kaleidoscope::DeclareOp Op, OpAdaptor Adaptor,
ConversionPatternRewriter &Rewriter) const override {
Location Loc = Op.getLoc();
Type DoubleType = Rewriter.getF64Type();
Type PointerType = LLVM::LLVMPointerType::get(Rewriter.getContext());
Value One = LLVM::ConstantOp::create(Rewriter, Loc, Rewriter.getI64Type(),
Rewriter.getI64IntegerAttr(1));
Value Address =
LLVM::AllocaOp::create(Rewriter, Loc, PointerType, DoubleType, One, 0);
LLVM::StoreOp::create(Rewriter, Loc, Adaptor.getInitialValue(), Address);
auto Function = Op->getParentOfType<LLVM::LLVMFuncOp>();
auto ScopeLoc =
Function.getLoc()
->findInstanceOf<FusedLocWith<LLVM::DISubprogramAttr>>();
if (!ScopeLoc)
return Rewriter.notifyMatchFailure(Op, "function has no debug scope");
LLVM::DISubprogramAttr Scope = ScopeLoc.getMetadata();
int64_t Line = Scope.getLine();
if (auto FileLoc = Loc->findInstanceOf<FileLineColLoc>())
Line = FileLoc.getLine();
auto VariableType = LLVM::DIBasicTypeAttr::get(
Rewriter.getContext(), llvm::dwarf::DW_TAG_base_type, "double", 64,
llvm::dwarf::DW_ATE_float);
auto Variable = LLVM::DILocalVariableAttr::get(
Scope, Op.getName(), Scope.getFile(), Line, Op.getArgumentNumber(),
/*alignInBits=*/0, VariableType, LLVM::DIFlags::Zero);
LLVM::DbgDeclareOp::create(
Rewriter, Loc, Address, Variable,
LLVM::DIExpressionAttr::get(Rewriter.getContext()));
Rewriter.replaceOp(Op, Address);
return success();
}
};
class ReadOpLowering : public OpConversionPattern<kaleidoscope::ReadOp> {
public:
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(kaleidoscope::ReadOp Op, OpAdaptor Adaptor,
ConversionPatternRewriter &Rewriter) const override {
Rewriter.replaceOpWithNewOp<LLVM::LoadOp>(Op, Rewriter.getF64Type(),
Adaptor.getVariable());
return success();
}
};
class AssignOpLowering : public OpConversionPattern<kaleidoscope::AssignOp> {
public:
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(kaleidoscope::AssignOp Op, OpAdaptor Adaptor,
ConversionPatternRewriter &Rewriter) const override {
Rewriter.replaceOpWithNewOp<LLVM::StoreOp>(Op, Adaptor.getValue(),
Adaptor.getVariable());
return success();
}
};
class LowerKaleidoscopeVariablesPass
: public PassWrapper<LowerKaleidoscopeVariablesPass,
OperationPass<ModuleOp>> {
public:
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LowerKaleidoscopeVariablesPass)
void runOnOperation() override {
MLIRContext &Context = getContext();
LLVMTypeConverter Converter(&Context);
Converter.addConversion([](kaleidoscope::VariableType InputType) -> Type {
return LLVM::LLVMPointerType::get(InputType.getContext());
});
ConversionTarget Target(Context);
Target.addIllegalDialect<kaleidoscope::KaleidoscopeDialect>();
Target.markUnknownOpDynamicallyLegal([](Operation *) { return true; });
RewritePatternSet Patterns(&Context);
Patterns.add<DeclareOpLowering, ReadOpLowering, AssignOpLowering>(Converter,
&Context);
if (failed(applyPartialConversion(getOperation(), Target,
std::move(Patterns))))
signalPassFailure();
}
};
static llvm::Expected<LoweredModule>
lowerToLLVM(const llvm::DataLayout &DataLayout) {
// Lower the high-level MLIR operations to the LLVM dialect.
PassManager LoweringPM(TheContext.get());
LoweringPM.addPass(createSCFToControlFlowPass());
LoweringPM.addPass(createConvertFuncToLLVMPass());
LoweringPM.addPass(createArithToLLVMConversionPass());
LoweringPM.addPass(createFinalizeMemRefToLLVMConversionPass());
LoweringPM.addPass(createConvertControlFlowToLLVMPass());
// Clean up any temporary casts introduced by dialect conversion.
LoweringPM.addPass(createReconcileUnrealizedCastsPass());
if (failed(LoweringPM.run(*TheModule)))
return llvm::make_error<llvm::StringError>(
"could not lower module to the LLVM dialect",
llvm::inconvertibleErrorCode());
// Add compile-unit and function scopes before lowering source variables.
PassManager DebugPM(TheContext.get());
DebugPM.addPass(
createKaleidoscopeDebugInfoPass(InputFilename.getValue(), OptLevel));
// Lower source variables while their names and locations are still present.
DebugPM.addPass(std::make_unique<LowerKaleidoscopeVariablesPass>());
// Fill in the remaining debug scopes on the lowered LLVM operations.
LLVM::DIScopeForLLVMFuncOpPassOptions DebugOptions;
DebugOptions.emissionKind = LLVM::DIEmissionKind::Full;
DebugPM.addPass(
LLVM::createDIScopeForLLVMFuncOpPass(std::move(DebugOptions)));
if (failed(DebugPM.run(*TheModule)))
return llvm::make_error<llvm::StringError>(
"could not add LLVM debug scopes", llvm::inconvertibleErrorCode());
// Register the translations from MLIR's LLVM dialect to LLVM IR.
registerBuiltinDialectTranslation(*TheContext);
registerLLVMDialectTranslation(*TheContext);
// Translate the lowered MLIR module into the LLVM IR module consumed by the
// target's object-file emitter.
auto LLVMContext = std::make_unique<llvm::LLVMContext>();
auto LLVMModule = translateModuleToLLVMIR(*TheModule, *LLVMContext);
if (!LLVMModule)
return llvm::make_error<llvm::StringError>(
"could not translate the LLVM dialect to LLVM IR",
llvm::inconvertibleErrorCode());
LLVMModule->setDataLayout(DataLayout);
if (DumpLLVMIR) {
LLVMModule->print(llvm::errs(), nullptr);
llvm::errs() << '\n';
}
return LoweredModule{std::move(LLVMContext), std::move(LLVMModule)};
}
static void HandleDefinition() {
if (auto FnAST = ParseDefinition()) {
if (auto FnIR = FnAST->codegen()) {
if (DumpMLIR) {
fprintf(stderr, "Read function definition:\n");
FnIR.print(llvm::errs(), OpPrintingFlags().assumeVerified());
fprintf(stderr, "\n");
}
if (!EmitObject) {
auto Lowered = ExitOnErr(lowerToLLVM(TheJIT->getDataLayout()));
ExitOnErr(TheJIT->addModule(llvm::orc::ThreadSafeModule(
std::move(Lowered.Module), std::move(Lowered.Context))));
InitializeModuleAndManagers();
}
}
} else {
// Skip token for error recovery.
getNextToken();
}
}
static void HandleExtern() {
if (auto ProtoAST = ParseExtern()) {
if (auto FnIR = ProtoAST->codegen()) {
FnIR.setPrivate();
if (DumpMLIR) {
fprintf(stderr, "Read extern:\n");
FnIR.print(llvm::errs(), OpPrintingFlags().assumeVerified());
fprintf(stderr, "\n");
}
FunctionProtos[ProtoAST->getName()] = std::move(ProtoAST);
}
} else {
// Skip token for error recovery.
getNextToken();
}
}
static void HandleTopLevelExpression() {
// Evaluate a top-level expression with the JIT, or retain it when compiling.
if (auto FnAST = ParseTopLevelExpr()) {
if (auto FnIR = FnAST->codegen()) {
if (DumpMLIR) {
fprintf(stderr, "Read top-level expression:\n");
FnIR.print(llvm::errs(), OpPrintingFlags().assumeVerified());
fprintf(stderr, "\n");
}
if (!EmitObject) {
auto RT = TheJIT->getMainJITDylib().createResourceTracker();
auto Lowered = ExitOnErr(lowerToLLVM(TheJIT->getDataLayout()));
ExitOnErr(TheJIT->addModule(
llvm::orc::ThreadSafeModule(std::move(Lowered.Module),
std::move(Lowered.Context)),
RT));
InitializeModuleAndManagers();
auto ExprSymbol = ExitOnErr(TheJIT->lookup("__anon_expr"));
double (*FP)() = ExprSymbol.getAddress().toPtr<double (*)()>();
fprintf(stderr, "Evaluated to %f\n", FP());
ExitOnErr(RT->remove());
}
}
} else {
// Skip token for error recovery.
getNextToken();
}
}
//===----------------------------------------------------------------------===//
// "Library" functions that can be "extern'd" from user code.
//===----------------------------------------------------------------------===//
#ifdef _WIN32
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif
/// putchard - putchar that takes a double and returns 0.
extern "C" DLLEXPORT double putchard(double X) {
fputc((char)X, stderr);
return 0;
}
/// printd - printf that takes a double, prints it as "%f\n", and returns 0.
extern "C" DLLEXPORT double printd(double X) {
fprintf(stderr, "%f\n", X);
return 0;
}
/// top ::= definition | external | expression | ';'
static void MainLoop() {
while (true) {
switch (CurTok) {
case tok_eof:
return;
case ';': // ignore top-level semicolons.
if (!EmitObject)
fprintf(stderr, "ready> ");
getNextToken();
continue;
case tok_def:
HandleDefinition();
break;
case tok_extern:
HandleExtern();
break;
default:
HandleTopLevelExpression();
break;
}
if (!EmitObject && CurTok != tok_eof && CurTok != ';')
fprintf(stderr, "ready> ");
}
}
//===----------------------------------------------------------------------===//
// Main driver code.
//===----------------------------------------------------------------------===//
int main(int argc, char **argv) {
llvm::cl::ParseCommandLineOptions(argc, argv,
"Kaleidoscope object file compiler\n");
if (OptLevel < '0' || OptLevel > '3') {
llvm::errs() << "Error: optimization level must be -O0, -O1, -O2, or -O3\n";
return 1;
}
if (EmitObject && InputFilename.empty()) {
llvm::errs() << "Error: --emit-object requires an input file\n";
return 1;
}
if (!EmitObject && !InputFilename.empty()) {
llvm::errs() << "Error: an input file requires --emit-object\n";
return 1;
}
if (!EmitObject && !TargetTripleOption.empty()) {
llvm::errs() << "Error: --target requires --emit-object\n";
return 1;
}
if (!EmitObject && !OutputFilename.empty()) {
llvm::errs() << "Error: -o requires --emit-object\n";
return 1;
}
if (EmitObject && !std::freopen(InputFilename.c_str(), "r", stdin)) {
std::perror(("Error opening " + InputFilename).c_str());
return 1;
}
llvm::InitializeAllTargetInfos();
llvm::InitializeAllTargets();
llvm::InitializeAllTargetMCs();
llvm::InitializeAllAsmParsers();
llvm::InitializeAllAsmPrinters();
// Install standard binary operators.
// 1 is lowest precedence.
BinopPrecedence['='] = 2;
BinopPrecedence['<'] = 10;
BinopPrecedence['+'] = 20;
BinopPrecedence['-'] = 20;
BinopPrecedence['*'] = 40; // highest.
// Prime the first token.
if (!EmitObject)
fprintf(stderr, "ready> ");
getNextToken();
if (!EmitObject)
TheJIT = ExitOnErr(llvm::orc::KaleidoscopeJIT::Create());
// JIT mode replaces this module as definitions are submitted. Object mode
// retains it until the entire input has been parsed.
InitializeModuleAndManagers();
// Run the main "interpreter loop" now.
MainLoop();
if (!EmitObject)
return 0;
// Select the host target and configure its object-file emitter.
std::string TargetTriple = TargetTripleOption.empty()
? llvm::sys::getDefaultTargetTriple()
: llvm::Triple::normalize(TargetTripleOption);
std::string Error;
const llvm::Target *Target =
llvm::TargetRegistry::lookupTarget(TargetTriple, Error);
if (!Target) {
llvm::errs() << Error << '\n';
return 1;
}
llvm::TargetOptions Options;
llvm::CodeGenOptLevel CodeGenOpt;
switch (OptLevel) {
case '0':
CodeGenOpt = llvm::CodeGenOptLevel::None;
break;
case '1':
CodeGenOpt = llvm::CodeGenOptLevel::Less;
break;
case '2':
CodeGenOpt = llvm::CodeGenOptLevel::Default;
break;
case '3':
CodeGenOpt = llvm::CodeGenOptLevel::Aggressive;
break;
}
std::unique_ptr<llvm::TargetMachine> TargetMachine(
Target->createTargetMachine(llvm::Triple(TargetTriple), "generic", "",
Options, llvm::Reloc::PIC_, std::nullopt,
CodeGenOpt));
if (!TargetMachine) {
llvm::errs() << "Could not create the target machine\n";
return 1;
}
// Lower the complete MLIR module once, then attach the target information
// required to produce a native object file.
auto Lowered = ExitOnErr(lowerToLLVM(TargetMachine->createDataLayout()));
Lowered.Module->setTargetTriple(llvm::Triple(TargetTriple));
llvm::SmallString<256> Filename;
if (OutputFilename.empty()) {
Filename = InputFilename;
llvm::sys::path::replace_extension(Filename, "o");
} else {
Filename = OutputFilename;
}
std::error_code EC;
llvm::raw_fd_ostream Dest(Filename, EC, llvm::sys::fs::OF_None);
if (EC) {
llvm::errs() << "Could not open " << Filename << ": " << EC.message()
<< '\n';
return 1;
}
llvm::legacy::PassManager EmitPM;
if (TargetMachine->addPassesToEmitFile(EmitPM, Dest, nullptr,
llvm::CodeGenFileType::ObjectFile)) {
llvm::errs() << "Target machine cannot emit an object file\n";
return 1;
}
EmitPM.run(*Lowered.Module);
Dest.flush();
llvm::outs() << "Wrote " << Filename << '\n';
return 0;
}
10.13.3 Dialect Definitions
// KaleidoscopeOps.td
//
// Defines the Kaleidoscope dialect: a small set of operations that preserve
// source-level variable semantics (name, location, argument number) in the IR
// until lowering is ready to turn them into stack storage and debug info.
// Provides the TableGen definitions for MLIR dialects, types, and operations.
include "mlir/IR/OpBase.td"
def Kaleidoscope_Dialect : Dialect {
// The prefix for every operation and type in this dialect. An operation
// with the mnemonic `var` will therefore print as `kaleidoscope.var`.
let name = "kaleidoscope";
// Where the generated C++ classes live. Our classes end up in the
// `mlir::kaleidoscope` namespace.
let cppNamespace = "::mlir::kaleidoscope";
let summary = "Operations that preserve Kaleidoscope variable semantics";
// Ask MLIR to generate the parser and printer for our types based on the
// assembly format each type declares. We only have one type, and its format
// is defined below.
let useDefaultTypePrinterParser = 1;
}
// A common base for every operation in this dialect. Each concrete operation
// supplies its own mnemonic and, optionally, a list of traits describing its
// behavior.
class Kaleidoscope_Op<string mnemonic, list<Trait> traits = []>
: Op<Kaleidoscope_Dialect, mnemonic, traits>;
// A type representing a mutable source variable.
//
// TypeDef generates the C++ class `mlir::kaleidoscope::VariableType` from the
// class stem "Variable", and the mnemonic "variable" gives it the textual
// spelling `!kaleidoscope.variable`.
//
// The type deliberately says nothing about how the variable is stored. That
// is a lowering decision, not a property of the source language.
def Kaleidoscope_VariableType
: TypeDef<Kaleidoscope_Dialect, "Variable"> {
let mnemonic = "variable";
let summary = "a mutable Kaleidoscope variable";
// An empty assembly format means the type has no parameters to print after
// its mnemonic, so it always appears as the bare `!kaleidoscope.variable`.
let assemblyFormat = "";
}
// Declares and initializes a mutable source variable.
//
// This generates the C++ class `kaleidoscope::DeclareOp`, printed in IR as
// `kaleidoscope.var`. It is the operation that carries the source name and
// argument number that would otherwise be lost the moment a variable became
// an anonymous allocation.
def Kaleidoscope_DeclareOp : Kaleidoscope_Op<"var", []> {
let summary = "declare and initialize a mutable source variable";
// `(ins ...)` lists everything the operation takes in. The `$` names
// generate C++ accessors (getInitialValue, getName, getArgumentNumber).
//
// - F64:$initialValue is an SSA operand, constrained to f64
// - StrAttr:$name is an attribute holding the source name
// - I64Attr:$argumentNumber is an attribute; 0 means a local variable,
// and 1+ means a function parameter (DWARF numbers
// parameters starting at 1)
let arguments = (ins F64:$initialValue, StrAttr:$name,
I64Attr:$argumentNumber);
// The SSA result represents the *variable itself*, not the floating-point
// value currently stored in it. Subsequent read and assign operations use
// this value to refer to the variable.
let results = (outs Kaleidoscope_VariableType:$variable);
// The textual syntax. Backticks contain literal punctuation, `$name` and
// `$initialValue` refer to the fields above, and `type(...)` prints the type
// of the named operand or result. `attr-dict` prints any attributes not
// already consumed by the format.
//
// This produces, for example:
// %0 = kaleidoscope.var "x"(%arg0 : f64)
// {argumentNumber = 1 : i64} : !kaleidoscope.variable
let assemblyFormat = "$name `(` $initialValue `:` type($initialValue) `)` attr-dict `:` type($variable)";
}
// Reads the current value of a mutable source variable.
//
// Not marked `Pure`: two reads of the same variable are not necessarily equal,
// since an assignment may occur between them. Marking it pure would let CSE
// incorrectly collapse the two reads into one.
def Kaleidoscope_ReadOp : Kaleidoscope_Op<"read", []> {
let summary = "read a mutable source variable";
// Consumes the variable; produces the f64 value currently stored in it.
let arguments = (ins Kaleidoscope_VariableType:$variable);
let results = (outs F64:$value);
let assemblyFormat = "`(` $variable `:` type($variable) `)` attr-dict `:` type($value)";
}
// Assigns a new value to a mutable source variable.
//
// The operation itself produces no SSA result. The AST layer returns the
// assigned value separately so that an assignment expression can be used as a
// subexpression (as in `(y = y + 1) * y`).
def Kaleidoscope_AssignOp : Kaleidoscope_Op<"assign", []> {
let summary = "assign a new value to a mutable source variable";
// Consumes the variable and the replacement value. Print both operand types
// explicitly so it is immediately clear that the value is an f64 and the
// destination is a !kaleidoscope.variable.
let arguments = (ins Kaleidoscope_VariableType:$variable, F64:$value);
let assemblyFormat = "`(` $value `:` type($value) `,` $variable `:` type($variable) `)` attr-dict";
}
10.13.4 Dialect Declaration
// KaleidoscopeDialect.h
#ifndef KALEIDOSCOPE_DIALECT_H
#define KALEIDOSCOPE_DIALECT_H
#include "mlir/Bytecode/BytecodeOpInterface.h"
#include "mlir/IR/Dialect.h"
#include "mlir/IR/OpDefinition.h"
#include "mlir/Interfaces/SideEffectInterfaces.h"
// Generated declaration of mlir::kaleidoscope::KaleidoscopeDialect.
#include "KaleidoscopeDialect.h.inc"
// KaleidoscopeTypes.h.inc contains several selectable sections. Defining this
// macro asks it to emit the generated type class declarations at this include.
#define GET_TYPEDEF_CLASSES
#include "KaleidoscopeTypes.h.inc"
// Likewise, select the generated operation class declarations from the
// operation header fragment.
#define GET_OP_CLASSES
#include "KaleidoscopeOps.h.inc"
#endif
10.13.5 Dialect Implementation
// KaleidoscopeDialect.cpp
#include "KaleidoscopeDialect.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/DialectImplementation.h"
#include "llvm/ADT/TypeSwitch.h"
using namespace mlir;
using namespace mlir::kaleidoscope;
// Generated definitions for the dialect class declared by the matching header
// fragment.
#include "KaleidoscopeDialect.cpp.inc"
// Select the generated C++ definitions for our TypeDef and Op records.
#define GET_TYPEDEF_CLASSES
#include "KaleidoscopeTypes.cpp.inc"
#define GET_OP_CLASSES
#include "KaleidoscopeOps.cpp.inc"
void KaleidoscopeDialect::initialize() {
// The same generated .inc files also contain lists of every type and
// operation in the dialect. These macros select those lists so the dialect
// can register all generated classes with MLIR.
addTypes<
#define GET_TYPEDEF_LIST
#include "KaleidoscopeTypes.cpp.inc"
>();
addOperations<
#define GET_OP_LIST
#include "KaleidoscopeOps.cpp.inc"
>();
}
10.13.6 Debug-Scope Pass Declaration
// KaleidoscopeDebugInfo.h
#ifndef KALEIDOSCOPE_DEBUG_INFO_H
#define KALEIDOSCOPE_DEBUG_INFO_H
#include "mlir/Pass/Pass.h"
#include "llvm/ADT/StringRef.h"
#include <memory>
#include <string>
/// Create the module pass that attaches compile-unit and function debug scopes
/// before the LLVM dialect is translated to LLVM IR.
///
/// Chapter 10 no longer needs a separate parameter-name map here: source
/// variable names are preserved by the Kaleidoscope dialect and consumed by
/// its lowering pass.
std::unique_ptr<mlir::Pass>
createKaleidoscopeDebugInfoPass(llvm::StringRef inputFilename, char optLevel);
#endif
10.13.7 Debug-Scope Pass Implementation
// KaleidoscopeDebugInfo.cpp
//
// Constructs the compile-unit and function-scope debug metadata that later
// lowering and LLVM IR translation will attach to instructions. Variable-
// level debug info is produced separately by the variable-lowering pass.
#include "KaleidoscopeDebugInfo.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/IR/BuiltinOps.h"
#include "llvm/BinaryFormat/Dwarf.h"
#include "llvm/Support/Path.h"
using namespace mlir;
namespace {
class KaleidoscopeDebugInfoPass
: public PassWrapper<KaleidoscopeDebugInfoPass, OperationPass<ModuleOp>> {
public:
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(KaleidoscopeDebugInfoPass)
KaleidoscopeDebugInfoPass(StringRef inputFilename, char optLevel)
: inputFilename(inputFilename.str()), optLevel(optLevel) {}
StringRef getArgument() const final { return "kaleidoscope-debug-info"; }
StringRef getDescription() const final {
return "Add Kaleidoscope compile-unit and function debug info";
}
void runOnOperation() override {
ModuleOp module = getOperation();
MLIRContext *context = module.getContext();
// A DWARF file entry is a (directory, filename) pair, not a single path.
// Input read interactively from the REPL has no real path, so we invent
// a synthetic <stdin> entry to describe it.
StringRef inputPath = inputFilename;
auto file = inputPath.empty()
? LLVM::DIFileAttr::get(context, "<stdin>", "")
: LLVM::DIFileAttr::get(
context, llvm::sys::path::filename(inputPath),
llvm::sys::path::parent_path(inputPath));
// Create the top-level debug record for this translation unit. We wrap
// it in a DistinctAttr so it has identity: two compile units with
// otherwise identical fields must not be uniqued into one, which would
// be invalid DWARF.
auto compileUnit = LLVM::DICompileUnitAttr::get(
DistinctAttr::create(UnitAttr::get(context)), llvm::dwarf::DW_LANG_C,
file, StringAttr::get(context, "Kaleidoscope"),
/*isOptimized=*/optLevel != '0', LLVM::DIEmissionKind::Full);
// MLIR carries debug metadata on locations rather than in a separate
// side table. Fusing the compile unit into the module's existing
// location is how we attach it so that LLVM IR translation can recover
// both the original source location and the new metadata.
module->setLoc(FusedLoc::get(context, {module.getLoc()}, compileUnit));
for (LLVM::LLVMFuncOp function : module.getOps<LLVM::LLVMFuncOp>()) {
// Prefer the function's own file and line. Fall back to the module's
// input file and line 1 for compiler-generated functions that carry
// no concrete source location.
Location originalLoc = function.getLoc();
LLVM::DIFileAttr functionFile = file;
int64_t line = 1;
if (auto fileLoc = originalLoc->findInstanceOf<FileLineColLoc>()) {
StringRef functionPath = fileLoc.getFilename().getValue();
functionFile = LLVM::DIFileAttr::get(
context, llvm::sys::path::filename(functionPath),
llvm::sys::path::parent_path(functionPath));
line = fileLoc.getLine();
}
// A definition and an external declaration need different DISubprogram
// configurations. A definition belongs to this compile unit, carries a
// distinct identity so it won't be merged with an identical-looking
// subprogram, and is flagged as a Definition. A declaration has no
// body emitted by this compile unit, so it gets neither the attachment
// nor the flag.
DistinctAttr id;
LLVM::DICompileUnitAttr functionCompileUnit = compileUnit;
auto flags = static_cast<LLVM::DISubprogramFlags>(0);
if (optLevel != '0')
flags = flags | LLVM::DISubprogramFlags::Optimized;
if (function.isExternal()) {
functionCompileUnit = {};
} else {
id = DistinctAttr::create(UnitAttr::get(context));
flags = flags | LLVM::DISubprogramFlags::Definition;
}
// Create the function's lexical debug scope. A complete source-level
// function signature is outside this tutorial's scope, but a
// DISubprogram still requires a subroutine-type metadata node, so we
// supply an empty one. Individual parameter variables receive their
// types later through their debug declarations; the function signature
// itself remains unspecified.
auto functionType = LLVM::DISubroutineTypeAttr::get(
context, llvm::dwarf::DW_CC_normal, {});
auto name = function.getNameAttr();
auto scope = LLVM::DISubprogramAttr::get(
context, id, functionCompileUnit, functionFile, name, name,
functionFile, line, line, flags, functionType,
/*retainedNodes=*/{}, /*annotations=*/{});
// Preserve the original source location while attaching the subprogram
// scope. Subsequent debug-info lowering and LLVM IR translation use
// this scope when they need to describe instructions in the function
// body, so it must be reachable from the function's location.
function->setLoc(FusedLoc::get(context, {originalLoc}, scope));
}
}
private:
// The pass owns copies of these because execution of the pass manager is
// not guaranteed to finish within the lifetime of the caller's arguments.
std::string inputFilename;
char optLevel;
};
} // namespace
// Hide the concrete pass class behind a factory so the driver only needs the
// standard mlir::Pass interface.
std::unique_ptr<Pass> createKaleidoscopeDebugInfoPass(StringRef inputFilename,
char optLevel) {
return std::make_unique<KaleidoscopeDebugInfoPass>(inputFilename, optLevel);
}
10.14 Wrapping Up
Our dialect is small: it has only three operations and one type. It remembers useful details about each variable, like its name, where it appears in the source code, and whether it is a function parameter. We keep those details around until we create the lower-level code, which makes it easier to generate storage and debugging information correctly.
If you want to take this further, the next natural step would be moving the entire Kaleidoscope AST into a dialect. The MLIR Toy tutorial shows what that architecture looks like. But for the problem we set out to solve, three operations were enough.
10.15 Download the Source
Copyright © 2026. All rights reserved.