7. pyxc: Code Generation

Next: translate the AST into LLVM IR.

The frontend already performs:

source -> tokens -> AST

Add one more boundary:

AST node -> LLVM Value or Function

This chapter prints IR but does not execute it. Keeping emission and execution separate makes the first backend easier to inspect.

Work in:

cd code/chapter-07

7.2 Create the Three LLVM Objects

Add these code-generation globals:

static unique_ptr<LLVMContext> TheContext;
static unique_ptr<Module> TheModule;
static unique_ptr<IRBuilder<>> TheBuilder;
static map<string, Value *> NamedValues;

They have distinct jobs:

LLVMContext -> owns LLVM's shared type and constant data
Module      -> owns functions and global IR
IRBuilder   -> inserts instructions at the current position
NamedValues -> maps current parameter names to LLVM values

Initialize them once:

static void InitializeModuleAndManagers() {
  TheContext = make_unique<LLVMContext>();
  TheModule = make_unique<Module>("PyxcJIT", *TheContext);
  TheBuilder = make_unique<IRBuilder<>>(*TheContext);
}

Call this from main() before parsing any input.

7.3 Add the Codegen Contract to the AST

Change the expression base class to:

class ExpressionNode {
public:
  virtual ~ExpressionNode() = default;
  virtual Value *codegen() = 0;
};

Add codegen() override to every expression node. Function nodes return Function *:

Function *codegen();

Use nullptr as the failure result. Add:

Value *LogErrorValue(const string &Message) {
  LogErrorExpression(Message);
  return nullptr;
}

7.4 Generate Number Constants

Implement:

Value *NumberExpressionNode::codegen() {
  return ConstantFP::get(*TheContext, APFloat(Value));
}

All values are still double, so every numeric literal becomes an LLVM floating-point constant.

No instruction is emitted for a constant by itself. It becomes an operand of whatever uses it.

7.5 Resolve Name References

Implement:

Value *NameExpressionNode::codegen() {
  auto It = NamedValues.find(Name);
  if (It == NamedValues.end() || !It->second)
    return LogErrorValue("Unknown variable name: '" + Name + "'");

  return It->second;
}

At this stage, NamedValues contains function parameters only. Mutable locals arrive in Chapter 11.

7.6 Generate Unary Minus

Implement:

Value *UnaryExpressionNode::codegen() {
  Value *OperandValue = Operand->codegen();
  if (!OperandValue)
    return nullptr;

  if (Operator != tok_minus)
    return LogErrorValue("Invalid unary operator");

  return TheBuilder->CreateFNeg(OperandValue, "negtmp");
}

CreateFNeg appends a floating-point negation at the builder's current insertion point.

7.7 Generate Binary Arithmetic

Generate both children first:

Value *L = Left->codegen();
if (!L)
  return nullptr;

Value *R = Right->codegen();
if (!R)
  return nullptr;

Then select an instruction:

switch (Operator) {
case tok_plus:
  return TheBuilder->CreateFAdd(L, R, "addtmp");
case tok_minus:
  return TheBuilder->CreateFSub(L, R, "subtmp");
case tok_star:
  return TheBuilder->CreateFMul(L, R, "multmp");
case tok_slash:
  return TheBuilder->CreateFDiv(L, R, "divtmp");
case tok_percent:
  return TheBuilder->CreateFRem(L, R, "remtmp");

The hint names make printed IR readable. LLVM may append numbers when a hint repeats.

7.8 Generate Less-Than as a pyxc Number

LLVM comparisons return i1, but pyxc currently represents every value as double.

Add:

case tok_less:
  L = TheBuilder->CreateFCmpOLT(L, R, "cmptmp");
  return TheBuilder->CreateUIToFP(
      L, Type::getDoubleTy(*TheContext), "booltmp");

The conversion boundary is:

fcmp olt -> i1 -> uitofp -> double 0.0 or 1.0

Finish the switch with an invalid-operator error.

7.9 Generate Calls

Look up the callee in the current module, validate its arity, generate every argument, and emit the call:

Value *CallExpressionNode::codegen() {
  Function *CalleeF = TheModule->getFunction(Callee);
  if (!CalleeF)
    return LogErrorValue("Unknown function: '" + Callee + "'");

  if (CalleeF->arg_size() != Arguments.size())
    return LogErrorValue("Incorrect number of arguments in call to '" +
                         Callee + "'");

  vector<Value *> ArgsV;
  for (auto &Argument : Arguments) {
    ArgsV.push_back(Argument->codegen());
    if (!ArgsV.back())
      return nullptr;
  }

  return TheBuilder->CreateCall(CalleeF, ArgsV, "calltmp");
}

Calls can target only functions already declared in the module. Chapter 8 adds persistent cross-module signature lookup.

7.10 Generate Function Signatures

Every parameter and return value is a double:

Function *FunctionSignatureNode::codegen() {
  vector<Type *> ParameterTypes(
      Parameters.size(), Type::getDoubleTy(*TheContext));

  FunctionType *FT = FunctionType::get(
      Type::getDoubleTy(*TheContext), ParameterTypes, false);

  Function *F = Function::Create(
      FT, Function::ExternalLinkage, Name, TheModule.get());

  unsigned Index = 0;
  for (auto &Argument : F->args())
    Argument.setName(Parameters[Index++]);

  return F;
}

A signature creates a declaration. It has a name and type but no basic blocks yet.

7.11 Generate Function Bodies

Implement the definition in four steps:

Function *FunctionDefinitionNode::codegen() {
  const string FunctionName = Signature->getName();

  Function *TheFunction = TheModule->getFunction(FunctionName);
  if (TheFunction && !TheFunction->empty())
    return nullptr; // redefinition

  if (!TheFunction)
    TheFunction = Signature->codegen();
  if (!TheFunction)
    return nullptr;

  BasicBlock *BB =
      BasicBlock::Create(*TheContext, "entry", TheFunction);
  TheBuilder->SetInsertPoint(BB);

  NamedValues.clear();
  for (auto &Argument : TheFunction->args())
    NamedValues[string(Argument.getName())] = &Argument;

  if (Value *RetVal = Body->codegen()) {
    TheBuilder->CreateRet(RetVal);
    verifyFunction(*TheFunction);
    return TheFunction;
  }

  TheFunction->eraseFromParent();
  return nullptr;
}

The entry block gives the builder somewhere to insert instructions. NamedValues connects source parameter names to LLVM arguments. Verification checks the finished function's IR invariants.

Erase a partially generated function on failure so broken IR does not remain in the module.

7.12 Generate Anonymous Top-Level Functions

Top-level expressions need a function body too. Wrap each one in an internal zero-argument function such as __anon_expr, then call the same definition codegen path.

This keeps the backend boundary uniform:

named definition      -> LLVM function
top-level expression  -> anonymous LLVM function

In the top-level handler, print successful IR:

if (Function *FunctionIR = Definition->codegen()) {
  FunctionIR->print(errs());
  fprintf(stderr, "\n");
}

7.13 Build and Inspect IR

cmake -S . -B build \
  -DLLVM_DIR="$(llvm-config --cmakedir)"
cmake --build build
./build/pyxc

Enter:

ready> def add(a, b): a + b

Expected IR shape:

define double @add(double %a, double %b) {
entry:
  %addtmp = fadd double %a, %b
  ret double %addtmp
}

Try:

ready> add(2, 3)

The anonymous function should contain a call to @add.

Run the suite:

llvm-lit -v test/

What you built is the first backend:

AST expressions -> LLVM Value*
AST signatures  -> LLVM declarations
AST definitions -> verified LLVM functions

Next: Chapter 8 hands modules to ORC JIT, executes anonymous functions, and adds optimization.

7.14 Need Help?

Build issues? Questions?

Include:

  • Your operating system and version
  • The chapter number
  • The exact command you ran
  • The complete error message
  • The output of c++ --version and cmake --version
  • The output of llvm-config --version for Chapter 6 and later