Implement the Core CodeGen Struct for LLVM Backend

Mục tiêu: Create the foundational CodeGen struct in a new Rust module. This struct will manage all necessary inkwell LLVM components (Context, Builder, Module) and state (variable and function maps) for the compiler’s code generation phase.


An absolutely stellar job on completing the entire front-end of your compiler! You have successfully built a lexer, a robust parser, and a thorough semantic analyzer. The Abstract Syntax Tree (AST) your compiler now holds is not just a representation of the code’s structure; it’s a semantically validated blueprint. You’ve confirmed that the code is grammatically sound, all variables are declared, types are used correctly, and function calls are valid. This is a monumental achievement and the solid foundation upon which we will build the rest of the compiler.

With the analysis phase complete, we now transition into the exciting world of the compiler’s “back-end.” Our new goal is to take this validated, high-level AST and translate it into a low-level language that machines can execute. This process is called code generation, and our target language is the powerful and ubiquitous LLVM Intermediate Representation (IR).

Before we can start walking the AST and emitting instructions, we need to set up our workshop. We need a central place to hold all of our LLVM tools and manage the state of our generated code. This central manager, or “context,” will be a Rust struct we’ll call CodeGen. This task is dedicated to creating this foundational struct, which will be the heart of our entire code generation engine.

Step 1: Create the Code Generation Module

Just as we did for the lexer, parser, and semantic analyzer, our code generation logic deserves its own dedicated module. This keeps our compiler’s architecture clean and organized.

First, create a new directory named codegen inside your src directory:

mkdir src/codegen

Inside this new directory, create two files: mod.rs to declare the module and generator.rs where our main CodeGen struct will live.

touch src/codegen/mod.rs
touch src/codegen/generator.rs

Finally, register this new module in your project’s main file (e.g., src/main.rs or src/lib.rs).

// In src/main.rs or src/lib.rs
pub mod ast;
pub mod lexer;
pub mod parser;
pub mod semantic;
pub mod codegen; // Add this line

Step 2: Designing and Implementing the CodeGen Struct

Our CodeGen struct will be the primary owner and manager of all the core inkwell objects needed to generate LLVM IR. Let’s open our new file, src/codegen/generator.rs, and add the code to define this struct. We will break down the role of each component in detail below.

// src/codegen/generator.rs

use inkwell::builder::Builder;
use inkwell::context::Context;
use inkwell::module::Module;
use inkwell::values::{PointerValue, FunctionValue};
use std::collections::HashMap;

/// The main struct for the code generation phase.
///
/// It holds all the necessary LLVM components from the `inkwell` crate,
/// along with state needed to track variables and functions during the AST traversal.
/// The lifetime parameter `'ctx` ensures that all LLVM objects (which are managed by
/// the context) do not outlive the context itself.
pub struct CodeGen<'ctx> {
    /// The global LLVM context. It owns and manages the core LLVM data structures.
    pub context: &'ctx Context,

    /// The instruction builder. A helper that simplifies creating and inserting
    /// LLVM instructions into basic blocks.
    pub builder: Builder<'ctx>,

    /// The LLVM module. A module is a container for all the code we will generate,
    /// including all function definitions. It's equivalent to a single source file.
    pub module: Module<'ctx>,

    /// This is the code generation equivalent of our semantic analyzer's symbol table.
    /// It maps variable names (as Strings) to their `PointerValue` on the stack.
    /// A `PointerValue` is essentially a memory address, the result of an `alloca` instruction.
    variables: HashMap<String, PointerValue<'ctx>>,

    /// A map to keep track of the `FunctionValue` for each function we have defined.
    /// This allows us to easily find a function's reference when generating a call instruction.
    functions: HashMap<String, FunctionValue<'ctx>>,
}

impl<'ctx> CodeGen<'ctx> {
    /// Creates a new `CodeGen` instance, initializing all the LLVM components.
    ///
    /// The `context` is passed in as a reference, making the caller the owner of the context.
    /// A module with the specified name is created within this context.
    pub fn new(context: &'ctx Context, module_name: &str) -> Self {
        let module = context.create_module(module_name);
        let builder = context.create_builder();

        CodeGen {
            context,
            builder,
            module,
            variables: HashMap::new(),
            functions: HashMap::new(),
        }
    }
}

Step 3: Wiring Up the Module

To make our new CodeGen struct accessible to the rest of the compiler, we need to re-export it from our codegen module. Open src/codegen/mod.rs and add the following code:

// src/codegen/mod.rs

// Declare the `generator` module, corresponding to `generator.rs`.
pub mod generator;

// Re-export the public items from the `generator` module so they can be
// accessed via `crate::codegen::`.
pub use generator::*;

Dissecting the CodeGen Struct

You have just created the complete “workshop” for our code generator. Let’s examine each piece and its role:

  • context: &'ctx Context: The Context is the top-level container for all things LLVM. It’s an opaque object that manages memory and owns the core data structures used by the compiler. We pass it in as a reference with a lifetime 'ctx to signify that our CodeGen struct is borrowing it and cannot outlive it.
  • builder: Builder<'ctx>: The Builder is our primary tool for creating instructions. Instead of manually creating an add instruction and inserting it into a specific place in our code, we can just call builder.build_add(...). The builder keeps track of the current insertion point (which function and which block we are in) and handles the details for us. It’s a massive convenience and a central part of inkwell’s API.
  • module: Module<'ctx>: An LLVM Module represents a single “translation unit,” which you can think of as a single source file (like a .c or .rs file). Our SimpliLang program will be compiled into one module, which will contain all the functions defined in the source code. At the end of the compilation pipeline, this is the object we will either print as human-readable IR or compile down to a native object file.
  • The 'ctx Lifetime: This is a crucial Rust concept that inkwell uses to ensure memory safety. The 'ctx lifetime annotation on the struct and its fields tells the Rust compiler that the Builder and Module are tied to the lifetime of the Context. Rust will prevent us from, for example, destroying the Context while the Module is still in use, which would lead to a crash. It’s a powerful, compile-time guarantee of safety.
  • variables: HashMap<String, PointerValue<'ctx>>: This is our runtime symbol table. During semantic analysis, the SymbolTable mapped names to type information. Here, in code generation, we map names to memory locations. When we encounter let x: int = 5;, we will generate an alloca instruction to allocate space for an integer on the function’s stack. This instruction returns a PointerValue (a memory address), which we will store in this map with the key "x". When we later need to use x, we will look it up here to get its address so we can load its value.
  • functions: HashMap<String, FunctionValue<'ctx>>: Similar to the variables map, this tracks the FunctionValue for every function we generate. A FunctionValue is LLVM’s representation of a function, and we need this reference to create the call instruction when we encounter a function call in the AST.

You have now successfully set up the entire foundation for the code generation phase. This CodeGen struct is a clean, organized, and powerful tool that is ready to be used to traverse the AST and bring your SimpliLang code to life.

Next Steps

With our CodeGen “workshop” fully assembled, the next task is to create the main “foreman” who will direct the work. You will implement a main traversal method on CodeGen that will walk the validated AST, starting from the Program node, and dispatch to more specific generation methods for each type of AST node it encounters (functions, statements, expressions, etc.).

Further Reading

Build the Code Generation AST Traversal Framework

Mục tiêu: Implement the AST traversal framework in the code generator. Add a set of recursive generate\_\* methods to the CodeGen struct to walk the AST using a Visitor Pattern, setting up the skeleton for future LLVM IR emission.


Excellent work setting up the CodeGen struct! You have successfully created the “workshop” for our code generation phase, a central place to manage all the essential LLVM components like the context, builder, and module. This organized structure is the perfect foundation for the next crucial step: building the machinery that will walk our validated Abstract Syntax Tree (AST) and translate each node into LLVM IR.

Just as you did for the semantic analyzer, we will implement a version of the Visitor Pattern. Our CodeGen struct will act as the “visitor,” systematically traversing the AST from its root down to the leaves. We will create a main entry-point method that starts the traversal and a series of specialized, recursive methods for each type of AST node. This task is all about creating that traversal skeleton, preparing our CodeGen struct to visit every part of the program and leaving placeholders where the actual instruction generation will happen in the tasks to come.

Implementing the AST Traversal Framework

We will now add a set of new methods to the impl<'ctx> CodeGen<'ctx> block in your src/codegen/generator.rs file. This framework will mirror the structure you built for the semantic analyzer, but instead of checking rules, it will eventually emit code.

Add the following code to your impl<'ctx> CodeGen<'ctx> block. Notice how the methods are currently placeholders, designed to be filled in one by one.

First, let’s add the necessary use statements at the top of src/codegen/generator.rs.

// src/codegen/generator.rs

use crate::ast;
use inkwell::builder::Builder;
use inkwell::context::Context;
use inkwell::module::Module;
// NEW: Add these `use` statements for LLVM value types.
use inkwell::values::{BasicValue, BasicValueEnum, PointerValue, FunctionValue};
use std::collections::HashMap;

Now, let’s add the traversal methods to the impl block for CodeGen.

// This code goes inside the `impl<'ctx> CodeGen<'ctx>` block in `src/codegen/generator.rs`

    /// The main entry point for code generation.
    /// It traverses the top-level `Program` AST node and generates code for each function.
    /// This is the public API of our code generator.
    pub fn generate(&mut self, program: &ast::Program) {
        // In a more complex compiler, we might do a first pass to declare all functions
        // (similar to semantic analysis) to handle mutual recursion. For SimpliLang, a single
        // top-down pass is sufficient for now.
        for function in &program.functions {
            self.generate_function_definition(function);
        }
    }

    /// Generates code for a single function definition.
    /// This will create the LLVM function, set up its basic blocks, and then generate
    /// code for the statements in its body.
    fn generate_function_definition(&mut self, func: &ast::FunctionDefinition) {
        // TODO: This will be implemented in a future task.
        // For now, we'll just traverse into the body's block.
        self.generate_block(&func.body);
    }

    /// Generates code for a block of statements.
    /// This corresponds to code within `{...}`.
    fn generate_block(&mut self, block: &ast::Block) {
        // A block is simply a sequence of statements.
        for stmt in &block.statements {
            self.generate_statement(stmt);
        }
    }

    /// Dispatches to the correct code generation logic based on the statement type.
    fn generate_statement(&mut self, stmt: &ast::Stmt) {
        match stmt {
            ast::Stmt::Let { initializer, .. } => {
                // Generate the code for the expression on the right-hand side.
                self.generate_expression(initializer);
                // TODO: Implement `alloca` and `store` instructions.
            }
            ast::Stmt::Return(expr) => {
                // Generate the code for the expression to be returned.
                self.generate_expression(expr);
                // TODO: Implement the `ret` instruction.
            }
            ast::Stmt::Expression(expr) => {
                // Generate the code for a standalone expression. The result is often unused.
                self.generate_expression(expr);
            }
        }
    }

    /// Generates code for an expression and returns the resulting LLVM value.
    /// This is the heart of the code generator, where values and computations are produced.
    ///
    /// The method returns an `Option<BasicValueEnum<'ctx>>`.
    /// - `Some(value)`: Code generation was successful, and `value` is the resulting
    ///   LLVM value (e.g., a constant, or the result of an instruction in a register).
    /// - `None`: Indicates an error during code generation. Since our AST is semantically
    ///   validated, this should ideally not happen, but it's a robust pattern for error handling.
    fn generate_expression(&mut self, expr: &ast::Expr) -> Option<BasicValueEnum<'ctx>> {
        match expr {
            ast::Expr::Literal(_) => {
                // Logic to be implemented in the next task.
                unimplemented!("Code generation for literals is not yet implemented.")
            }
            ast::Expr::Variable(_) => {
                unimplemented!("Code generation for variables is not yet implemented.")
            }
            ast::Expr::Binary { .. } => {
                unimplemented!("Code generation for binary expressions is not yet implemented.")
            }
            ast::Expr::Unary { .. } => {
                unimplemented!("Code generation for unary expressions is not yet implemented.")
            }
            ast::Expr::If { .. } => {
                unimplemented!("Code generation for if-expressions is not yet implemented.")
            }
            ast::Expr::FunctionCall { .. } => {
                unimplemented!("Code generation for function calls is not yet implemented.")
            }
        }
    }

Dissecting the Traversal Framework

You have just created the complete skeleton for the code generation process. This structure is powerful because it’s both simple and incredibly extensible.

  • Top-Down Traversal: The process starts with a single public method, generate, which acts as the entry point. It then performs a recursive descent through the AST, with each generate_* method being responsible for its corresponding AST node type. This is a clean and predictable flow of control.
  • Dispatching with match: The generate_statement and generate_expression methods use Rust’s powerful match statement. This acts as a dispatcher, cleanly delegating the work to the correct logic for each variant of the Stmt and Expr enums. As you add more features to SimpliLang, you would simply add new arms to these match statements.
  • The Role of generate_expression: This is the most critical method in the framework. Unlike statements, which are primarily about control flow, expressions produce values. This is reflected in the method’s signature: -> Option<BasicValueEnum<'ctx>>.
    • BasicValueEnum<'ctx>: This is a powerful enum from the inkwell crate that can represent any of LLVM’s basic “first-class” values, such as an integer, a float, or a pointer. When we generate code for 2 + 3, the generate_expression for this node will return a BasicValueEnum that holds the IntValue representing the result, 5.
    • Returning Values: This return value is essential for composition. To generate code for (2 + 3) * 4, the generate_expression for the * operator will first recursively call itself on 2 + 3, receive the IntValue for 5, and then use that result as an input to the LLVM multiplication instruction.

You now have a complete, well-structured, but empty framework. The unimplemented! macros serve as a perfect to-do list for the upcoming tasks. You are now ready to start filling in this skeleton and bringing your language to life, one instruction at a time.

Next Steps

With the traversal framework in place, we can begin implementing the code generation logic, starting with the simplest “leaf” nodes of our AST. In the very next task, you will tackle the first unimplemented! block by writing the codegen method for numeric literals. You will learn how to take a number like 42 from the AST and create its corresponding constant value representation in LLVM IR.

Further Reading

Implement Code Generation for Literal Values

Mục tiêu: Implement the logic within the generate\_expression method to handle ast::Expr::Literal. This involves converting integer, float, and boolean literals from the Abstract Syntax Tree (AST) into their corresponding LLVM constant values using the inkwell library.


Excellent work setting up the traversal framework for your CodeGen struct! You have a perfectly organized skeleton, with methods ready to visit every node in the Abstract Syntax Tree. Those unimplemented! macros are now our to-do list, and we’re ready to tackle the first and most fundamental one: generating code for literals.

From AST Values to LLVM Constants

The journey of code generation begins with the simplest, most atomic pieces of our language: the literal values like 123, 3.14, and true. These are the “leaves” of our AST’s expression trees. In LLVM IR, these constant, known-at-compile-time values are represented as, unsurprisingly, constants.

Our task is to take a literal value from a SimpliLang AST node and create its corresponding inkwell value object.

  • An ast::LiteralValue::Int(123) will become an LLVM 64-bit integer constant (i64 123).
  • An ast::LiteralValue::Float(3.14) will become an LLVM 64-bit floating-point constant (double 3.14).
  • An ast::LiteralValue::Bool(true) will become an LLVM 1-bit integer constant (i1 true or i1 1).

To do this, we’ll use specific methods on our CodeGen’s context to get the desired LLVM type (e.g., i64_type()) and then call a method on that type to create a constant value (e.g., const_int(...)). The result will be a specific inkwell value type, like IntValue or FloatValue, which we will then wrap in the BasicValueEnum that our generate_expression method is designed to return.

Implementing Literal Code Generation

Let’s modify the generate_expression method in src/codegen/generator.rs and replace the first unimplemented! macro with the logic to handle all three kinds of literals.

Here are the changes. We are focusing only on the ast::Expr::Literal arm.

// src/codegen/generator.rs

// ... (use statements and CodeGen struct definition are unchanged)

impl<'ctx> CodeGen<'ctx> {
    // ... (new, generate, generate_function_definition, etc. are unchanged)

    fn generate_expression(&mut self, expr: &ast::Expr) -> Option<BasicValueEnum<'ctx>> {
        match expr {
            ast::Expr::Literal(value) => {
                // A new inner match to handle the different kinds of literals.
                match value {
                    ast::LiteralValue::Int(int_val) => {
                        // For an integer literal, we first get the LLVM type for a 64-bit integer.
                        let ty = self.context.i64_type();

                        // Then, we create a constant value of that type.
                        // The first argument is the value itself.
                        // The `false` for the second argument (`sign_extend`) indicates that the
                        // value should be treated as unsigned for the purpose of bit-width extension,
                        // which is a safe default for creating constants from a known `u64`.
                        let val = ty.const_int(*int_val as u64, false);

                        // Our `generate_expression` method must return a `BasicValueEnum`.
                        // The `.into()` method conveniently converts the specific `IntValue`
                        // into the general `BasicValueEnum`.
                        Some(val.into())
                    }
                    ast::LiteralValue::Float(float_val) => {
                        // The process for floats is very similar.
                        // Get the 64-bit float type (a `double` in C terms).
                        let ty = self.context.f64_type();
                        // Create a constant float value.
                        let val = ty.const_float(*float_val);
                        // Convert and return.
                        Some(val.into())
                    }
                    ast::LiteralValue::Bool(bool_val) => {
                        // In LLVM, a boolean is represented as a 1-bit integer (`i1`).
                        let ty = self.context.bool_type(); // This is just an alias for `i1_type()`.

                        // We create a constant 1-bit integer, where `true` is 1 and `false` is 0.
                        let val = ty.const_int(if *bool_val { 1 } else { 0 }, false);

                        // Convert and return.
                        Some(val.into())
                    }
                }
            }
            ast::Expr::Variable(_) => {
                unimplemented!("Code generation for variables is not yet implemented.")
            }
            ast::Expr::Binary { .. } => {
                unimplemented!("Code generation for binary expressions is not yet implemented.")
            }
            // ... (rest of the unimplemented arms)
            ast::Expr::Unary { .. } => {
                unimplemented!("Code generation for unary expressions is not yet implemented.")
            }
            ast::Expr::If { .. } => {
                unimplemented!("Code generation for if-expressions is not yet implemented.")
            }
            ast::Expr::FunctionCall { .. } => {
                unimplemented!("Code generation for function calls is not yet implemented.")
            }
        }
    }
}

Dissecting the Code

You have just written your first piece of code that generates LLVM IR! Let’s break down the key concepts:

  • LLVM Types: Before you can create a value in LLVM, you must first specify its type. We access these standard types through our context object: self.context.i64_type(), self.context.f64_type(), and self.context.bool_type(). These correspond directly to the primitive types you designed for SimpliLang.
  • Constant Values: Once you have a type, you can create a constant value of that type using methods like const_int and const_float. These methods create an LLVM value that is known at compile time and will be embedded directly into the final executable.
  • Booleans as Integers: A crucial concept is that LLVM does not have a distinct “boolean” type. It uses a 1-bit integer type, i1, where 0 represents false and 1 represents true. Our code correctly maps the Rust bool from our AST to this i1 constant.
  • Into<BasicValueEnum>: Your generate_expression function promises to return a BasicValueEnum, which can hold any basic value. The const_int method, however, returns a more specific IntValue. The .into() call is a beautiful piece of Rust’s trait system. inkwell provides the necessary From implementations so that you can seamlessly convert specific value types like IntValue and FloatValue into the general BasicValueEnum wrapper.

You have successfully filled in the first and most fundamental piece of the code generation puzzle. You can now transform the constant values from your high-level language into their low-level LLVM equivalents. These constant values will serve as the raw materials for the instructions we will generate next.

Next Steps

With the ability to generate constant values, you are now ready to generate code for operations that use them. The next logical task is to write the code generator for binary expressions. You will take the constant values generated for the left and right-hand sides of an expression like 5 + 10 and use the inkwell builder to create an LLVM add instruction that computes the result.

Further Reading

Implement LLVM Code Generation for Binary Expressions

Mục tiêu: Extend a compiler’s code generator, written in Rust, to translate binary arithmetic and comparison expressions from an Abstract Syntax Tree (AST) into low-level LLVM instructions using the inkwell builder API.


Excellent work on generating code for literals! You have successfully taught your compiler to translate the constant values from SimpliLang into their low-level LLVM equivalents. These constants are the raw materials, the foundational numbers and booleans of our program. Now, it’s time to build the machinery that operates on these materials.

In this next step, we will tackle one of the most important parts of code generation: translating binary expressions. We will take an AST node representing 5 + 10 and generate the actual LLVM instruction that performs the addition. This is where your compiler transitions from simply representing data to generating active, computational logic.

Introducing the Builder: Your Instruction Factory

The key to this process is the builder field within your CodeGen struct. The inkwell::Builder is a powerful helper object that acts as your instruction factory. It keeps track of where in the code you are currently inserting instructions (which function and which basic block) and provides simple, type-safe methods to create and insert them. Instead of manually constructing complex LLVM data structures, you can simply call a method like builder.build_int_add(...), and the builder will handle the rest.

The Strategy: Recurse, Check, Build

Our strategy for generating code for a binary expression is a beautiful, recursive process that leverages the structure of our AST and the work we’ve already done:

  1. Recurse: For an expression like left + right, we will first recursively call generate_expression on the left operand and then on the right operand. This will yield two LLVM values (which could be the constants you generated in the last task, or the results of other, nested binary expressions).
  2. Check: LLVM is a strongly-typed system. An add instruction for integers is different from an fadd instruction for floats. We will inspect the type of the LLVM values we received from our recursive calls to determine whether we’re dealing with integers or floats. Since our semantic analyzer has already guaranteed type compatibility, we can be confident that both operands will be of the same kind.
  3. Build: Based on the operator from the AST (+, -, ==, etc.) and the type we just checked, we will call the appropriate method on our builder to generate the correct LLVM instruction. The result of this instruction—a new LLVM value representing the computed result—will then be returned.

Let’s implement this logic by filling in the ast::Expr::Binary arm of your generate_expression method.

Implementing Binary Expression Code Generation

Update the generate_expression method in src/codegen/generator.rs with the following code. Remember to add the new use statements at the top of your file.

// Add these to the `use` statements at the top of src/codegen/generator.rs
use inkwell::{IntPredicate, FloatPredicate};

// ... inside the impl<'ctx> CodeGen<'ctx> block

fn generate_expression(&mut self, expr: &ast::Expr) -> Option<BasicValueEnum<'ctx>> {
    match expr {
        // The Literal arm is unchanged from the previous task.
        ast::Expr::Literal(value) => { /* ... */ },

        ast::Expr::Binary { left, op, right } => {
            // 1. Recursively generate code for the left and right-hand sides.
            // The `?` operator is a convenient way to propagate `None` if either
            // sub-expression fails to generate code.
            let lhs = self.generate_expression(left)?;
            let rhs = self.generate_expression(right)?;

            // 2. Based on the operator, call the appropriate builder method.
            match op {
                // --- Arithmetic Operators ---
                ast::BinaryOp::Add => {
                    // We check if the LHS is an integer value. Due to our semantic analysis,
                    // if the LHS is an integer, the RHS must be one too.
                    if let (BasicValueEnum::IntValue(lhs_int), BasicValueEnum::IntValue(rhs_int)) = (lhs, rhs) {
                        // Use the builder to create an integer `add` instruction.
                        // The third argument is the name for the temporary register that will
                        // hold the result, which is helpful for debugging the generated IR.
                        let result = self.builder.build_int_add(lhs_int, rhs_int, "addtmp");
                        Some(result.into())
                    } else if let (BasicValueEnum::FloatValue(lhs_float), BasicValueEnum::FloatValue(rhs_float)) = (lhs, rhs) {
                        // Similarly, build a floating-point `fadd` instruction.
                        let result = self.builder.build_float_add(lhs_float, rhs_float, "addtmp");
                        Some(result.into())
                    } else {
                        // This case should be unreachable if semantic analysis is correct.
                        None
                    }
                }
                ast::BinaryOp::Subtract => {
                    if let (BasicValueEnum::IntValue(lhs_int), BasicValueEnum::IntValue(rhs_int)) = (lhs, rhs) {
                        let result = self.builder.build_int_sub(lhs_int, rhs_int, "subtmp");
                        Some(result.into())
                    } else if let (BasicValueEnum::FloatValue(lhs_float), BasicValueEnum::FloatValue(rhs_float)) = (lhs, rhs) {
                        let result = self.builder.build_float_sub(lhs_float, rhs_float, "subtmp");
                        Some(result.into())
                    } else {
                        None
                    }
                }
                ast::BinaryOp::Multiply => {
                    if let (BasicValueEnum::IntValue(lhs_int), BasicValueEnum::IntValue(rhs_int)) = (lhs, rhs) {
                        let result = self.builder.build_int_mul(lhs_int, rhs_int, "multmp");
                        Some(result.into())
                    } else if let (BasicValueEnum::FloatValue(lhs_float), BasicValueEnum::FloatValue(rhs_float)) = (lhs, rhs) {
                        let result = self.builder.build_float_mul(lhs_float, rhs_float, "multmp");
                        Some(result.into())
                    } else {
                        None
                    }
                }
                ast::BinaryOp::Divide => {
                    // For division, we must use signed division for integers.
                    if let (BasicValueEnum::IntValue(lhs_int), BasicValueEnum::IntValue(rhs_int)) = (lhs, rhs) {
                        let result = self.builder.build_int_signed_div(lhs_int, rhs_int, "divtmp");
                        Some(result.into())
                    } else if let (BasicValueEnum::FloatValue(lhs_float), BasicValueEnum::FloatValue(rhs_float)) = (lhs, rhs) {
                        let result = self.builder.build_float_div(lhs_float, rhs_float, "divtmp");
                        Some(result.into())
                    } else {
                        None
                    }
                }

                // --- Comparison Operators ---
                // These operators produce a boolean (`i1` in LLVM).
                _ => {
                    // All comparison operators use a similar instruction, `icmp` (for integers)
                    // or `fcmp` (for floats), which takes a "predicate" to specify the
                    // type of comparison (e.g., equal, less than).
                    let predicate = match op {
                        ast::BinaryOp::Equal => IntPredicate::EQ,
                        ast::BinaryOp::NotEqual => IntPredicate::NE,
                        ast::BinaryOp::LessThan => IntPredicate::SLT, // Signed Less Than
                        ast::BinaryOp::GreaterThan => IntPredicate::SGT, // Signed Greater Than
                        _ => unreachable!(), // All other operators handled above
                    };

                    let float_predicate = match op {
                        ast::BinaryOp::Equal => FloatPredicate::OEQ, // Ordered and Equal
                        ast::BinaryOp::NotEqual => FloatPredicate::ONE, // Ordered and Not Equal
                        ast::BinaryOp::LessThan => FloatPredicate::OLT, // Ordered and Less Than
                        ast::BinaryOp::GreaterThan => FloatPredicate::OGT, // Ordered and Greater Than
                        _ => unreachable!(),
                    };

                    if let (BasicValueEnum::IntValue(lhs_int), BasicValueEnum::IntValue(rhs_int)) = (lhs, rhs) {
                        let result = self.builder.build_int_compare(predicate, lhs_int, rhs_int, "cmptmp");
                        Some(result.into())
                    } else if let (BasicValueEnum::FloatValue(lhs_float), BasicValueEnum::FloatValue(rhs_float)) = (lhs, rhs) {
                        let result = self.builder.build_float_compare(float_predicate, lhs_float, rhs_float, "cmptmp");
                        Some(result.into())
                    } else {
                        None
                    }
                }
            }
        }
        ast::Expr::Variable(_) => {
            unimplemented!("Code generation for variables is not yet implemented.")
        }
        // ... (rest of the unimplemented arms)
    }
}

Dissecting the Implementation

This block of code is the heart of your compiler’s computational logic. Let’s break it down:

  • Recursive Generation: The first thing we do is call self.generate_expression for the left and right sides. This is incredibly powerful. If you have an expression like (2 * 3) + 4, the call for + will first trigger a recursive call for 2 * 3. That inner call will generate the multiplication instruction and return the resulting LLVM value. This value then becomes the lhs for the outer addition instruction. The recursive structure of the generator perfectly mirrors the recursive structure of the AST.
  • Type-Driven Logic: The if let chains are used to determine if we are working with integers or floats. This allows us to call the correct builder method (build_int_add vs. build_float_add). Because of the semantic analysis you already performed, you can trust that if the lhs is an IntValue, the rhs will be too.
  • Comparison Predicates: For comparisons, LLVM uses a single instruction (icmp or fcmp) that is configured by a predicate. A predicate is just an enum that tells the instruction what kind of comparison to perform.

    • IntPredicate::EQ: Compare for equality.
    • IntPredicate::SLT: Compare for “Signed Less Than”. This is important for correctly comparing negative numbers.
    • FloatPredicate::OEQ: Compare for “Ordered and Equal”. The “ordered” part handles special floating-point values like NaN (Not a Number) correctly. In most high-level languages, comparing with NaN is always false, and the “ordered” predicates provide this behavior.

You have now built a powerful piece of your compiler that can translate any valid arithmetic or comparison expression from SimpliLang into efficient, low-level LLVM instructions. You’ve transformed your compiler from a data-representer into a true logic-generator.

Next Steps

We can now generate instructions that operate on values. But where do these values come from, other than being literals? They come from variables. The next logical task is to implement code generation for let statements. This will involve two new and fundamental LLVM instructions:

  1. alloca: To allocate space for a new variable on the function’s stack.
  2. store: To store the initial value (which you can now compute with your binary expression generator!) into that allocated memory.

Further Reading

Implement Code Generation for ‘let’ Statements

Mục tiêu: Extend the compiler’s code generator to handle let statements. This involves implementing logic to allocate memory for variables on the stack using LLVM’s alloca instruction and initializing them with the store instruction.


Of course! Let’s continue building your SimpliLang compiler.

You have masterfully constructed the computational core of your code generator. With the logic for binary expressions in place, your compiler can now translate complex mathematical and logical operations into LLVM IR. However, these computations are currently ephemeral—their results are calculated and then disappear. To build a truly useful language, we need a way to store these results, give them names, and retrieve them later. This is the fundamental role of variables, and in SimpliLang, they are introduced via the let statement.

This task is dedicated to implementing the code generation for let statements. This will introduce you to two of the most fundamental concepts in low-level code generation: allocating memory on the stack and storing values into that memory.

The Stack: A Function’s Private Scratchpad

Every time a function is called, the program sets aside a small, private region of memory for it called a stack frame. This is where the function’s local variables, parameters, and other temporary data live. This memory is fast, efficient, and automatically managed: when the function finishes and returns, its stack frame is destroyed, and all the memory is reclaimed.

When we see a let statement, our job as the compiler is to generate instructions that reserve a small piece of the current function’s stack frame for our new variable.

alloca and store: The Two-Step Process of Variable Creation

To bring a SimpliLang variable to life in LLVM, we perform a two-step process:

  1. Allocation (alloca): We use the alloca instruction to tell LLVM, “Reserve a space on the current function’s stack frame that is large enough to hold a value of this type.” This instruction doesn’t create the value itself; it just reserves the memory and gives us a pointer to that memory’s location. This pointer is the variable’s address.
  2. Storage (store): Once we have a memory location (the pointer from alloca) and a value (from generating the code for the initializer expression), we use the store instruction. This instruction says, “Take this value and copy it into the memory location pointed to by this pointer.”

With these two instructions, the variable is fully initialized and ready to be used.

Implementing let Statement Code Generation

Let’s put this theory into practice. We will modify the generate_statement method in src/codegen/generator.rs to handle the ast::Stmt::Let arm.

First, to make our code cleaner, let’s add a small helper function inside our impl<'ctx> CodeGen<'ctx> block. This function will map our SimpliLang primitive types to their corresponding inkwell type representations. This is a common and useful pattern that keeps our main logic focused.

We also need to add a new use statement at the top of the file.

// Add this to the `use` statements at the top of src/codegen/generator.rs
use inkwell::types::{BasicType, BasicTypeEnum};

// ... inside the impl<'ctx> CodeGen<'ctx> block

    /// A helper function to map a SimpliLang AST type to an LLVM `BasicTypeEnum`.
    /// This keeps our main code generation logic cleaner.
    fn map_type(&self, ty: &ast::PrimitiveType) -> BasicTypeEnum<'ctx> {
        match ty {
            ast::PrimitiveType::Int => self.context.i64_type().as_basic_type_enum(),
            ast::PrimitiveType::Float => self.context.f64_type().as_basic_type_enum(),
            ast::PrimitiveType::Bool => self.context.bool_type().as_basic_type_enum(),
        }
    }

Now, let’s update the generate_statement method to implement the core logic for this task.

// src/codegen/generator.rs

// ... (inside the impl<'ctx> CodeGen<'ctx> block)

    fn generate_statement(&mut self, stmt: &ast::Stmt) {
        match stmt {
            ast::Stmt::Let { var_name, var_type, initializer } => {
                // 1. Map our high-level AST type to the corresponding low-level LLVM type.
                let llvm_type = self.map_type(var_type);

                // 2. Use the builder to create an `alloca` instruction.
                // This allocates memory on the stack for the variable. The `build_alloca`
                // method returns a `PointerValue` that acts as the variable's address.
                // The `var_name` is used as a hint for the name in the generated IR,
                // which is invaluable for debugging.
                let alloca = self.builder.build_alloca(llvm_type, var_name);

                // 3. Generate the LLVM value for the initializer expression.
                // We can now use the powerful expression generator you've already built!
                let initializer_val = self.generate_expression(initializer)
                    // Since the semantic analyzer has already validated the code, we can be
                    // confident that the initializer is valid. An error here would indicate
                    // a bug in our compiler, so we use `expect` to make that explicit.
                    .expect("Semantic analysis should have caught invalid initializers.");

                // 4. Use the builder to create a `store` instruction.
                // This instruction takes the value we just generated (`initializer_val`)
                // and stores it in the memory location pointed to by `alloca`.
                self.builder.build_store(alloca, initializer_val);

                // 5. IMPORTANT: Register the variable's name and its pointer in our map.
                // This is how we remember where each variable lives. When we encounter
                // a use of this variable later, we'll look it up in this map to get its
                // `PointerValue`.
                self.variables.insert(var_name.clone(), alloca);
            }
            ast::Stmt::Return(expr) => {
                // ... (to be implemented)
            }
            ast::Stmt::Expression(expr) => {
                // ... (unchanged)
            }
        }
    }

Dissecting the Implementation

You have just taught your compiler how to manage one of the most fundamental aspects of programming: variables.

  1. build_alloca: This is our direct line to the function’s stack. We provide the type and a name, and inkwell gives us back a PointerValue<'ctx>. This value doesn’t represent the data in the variable, but its address.
  2. build_store: This is the instruction that moves data. It takes two arguments: a PointerValue (the destination address) and a BasicValueEnum (the source data). After this instruction, the memory on the stack now holds the result of our initializer expression.
  3. The variables Map: The line self.variables.insert(var_name.clone(), alloca); is the crucial link between the SimpliLang source code and the LLVM runtime. We are mapping the human-readable variable name to its machine-level memory address. This map is the memory of our code generator, analogous to the SymbolTable from the semantic analysis phase. Without this step, we would allocate the memory but immediately forget where it is.

You have now successfully bridged the gap between a high-level variable declaration and the low-level memory operations required to make it a reality.

Next Steps

You’ve implemented the “store” side of a variable’s life. The next logical and essential step is to implement the “load” side. When the compiler encounters a variable being used in an expression (e.g., let y = x + 5;), it needs to retrieve the value stored at that variable’s memory address. In the next task, you will implement the code generation for Expr::Variable nodes, where you will look up the variable’s PointerValue in your variables map and use the builder.build_load instruction to fetch its value.

Further Reading

Implement Variable Value Loading in a Compiler

Mục tiêu: Enhance the compiler’s code generator to handle reading variable values. Implement the logic for variable expressions by looking up the variable’s memory address and using the builder’s build\_load method to generate an LLVM load instruction, allowing variable values to be used in other expressions.


Of course! Let’s continue building your SimpliLang compiler.

You have masterfully implemented the “write” side of a variable’s life. By generating alloca and store instructions for let statements, you’ve taught your compiler how to create variables and initialize them with values. You also critically stored the memory address of each new variable in your variables map. That map is our “address book,” and we are now going to use it to look up those addresses and read the data stored there.

This task is all about implementing the “read” side of a variable’s life: loading its value so it can be used in expressions.

From Address to Value: The load Instruction

In the last task, you worked with a PointerValue. It’s crucial to remember that this is not the variable’s value but its address in memory. When the compiler encounters an expression like y = x + 5, it cannot simply add the address of x to 5. It first needs to retrieve the integer value that is currently stored at that address.

This is the job of the load instruction. It’s the direct counterpart to store.

  • store: Takes a value and a pointer, and writes the value to the memory at that address.
  • load: Takes a pointer, and reads the value from the memory at that address, placing it into a temporary virtual register for use in subsequent instructions.

Our strategy is simple and direct:

  1. When we encounter an Expr::Variable(name) node, we will look up name in our variables map to get its PointerValue.
  2. We will then pass this PointerValue to our builder’s build_load method.
  3. The build_load method will generate the load instruction and return a BasicValueEnum containing the loaded value. This is exactly the type our generate_expression method needs to return, allowing it to be seamlessly used by other parts of the expression generator.

Implementing Variable Value Loading

Let’s put this into practice by filling in the ast::Expr::Variable arm of your generate_expression method in src/codegen/generator.rs.

// src/codegen/generator.rs

// ... (use statements and CodeGen struct definition are unchanged)

impl<'ctx> CodeGen<'ctx> {
    // ... (new, map_type, generate, generate_function_definition, generate_statement are unchanged)

    fn generate_expression(&mut self, expr: &ast::Expr) -> Option<BasicValueEnum<'ctx>> {
        match expr {
            // Literal and Binary arms are unchanged.
            ast::Expr::Literal(value) => { /* ... */ },
            ast::Expr::Binary { left, op, right } => { /* ... */ },

            ast::Expr::Variable(name) => {
                // 1. Look up the variable's memory location (its `PointerValue`) in our map.
                //    This is the address we got from the `alloca` instruction in the previous task.
                let ptr = self.variables.get(name)
                    .expect("Semantic analysis should guarantee that all variables are declared.");

                // 2. Use the builder to create a `load` instruction.
                //    This reads the value from the memory address pointed to by `ptr`.
                //    The second argument is a name for the temporary register that will hold
                //    the loaded value, which is very useful for debugging the generated LLVM IR.
                //    The method returns a `BasicValueEnum`, which is exactly what we need.
                let loaded_val = self.builder.build_load(*ptr, name);

                // 3. Return the loaded value.
                Some(loaded_val)
            }

            // ... (rest of the unimplemented arms)
            ast::Expr::Unary { .. } => {
                unimplemented!("Code generation for unary expressions is not yet implemented.")
            }
            ast::Expr::If { .. } => {
                unimplemented!("Code generation for if-expressions is not yet implemented.")
            }
            ast::Expr::FunctionCall { .. } => {
                unimplemented!("Code generation for function calls is not yet implemented.")
            }
        }
    }
}

Dissecting the Implementation

This small addition is incredibly powerful and completes the variable management cycle.

  1. The Lookup: self.variables.get(name) is where we consult our “address book”. We take the variable name from the AST and find the corresponding memory pointer that we stored when we generated its let statement. We use .expect() here because our semantic analyzer has already proven that this variable must exist. If it doesn’t, it’s a bug in our compiler, not the user’s code, and we want to know immediately.
  2. build_load: This is the core of this task. self.builder.build_load(*ptr, name) generates an instruction like %x = load i64, i64* %x.addr in LLVM IR. It takes the PointerValue and the name, and returns the loaded value as a BasicValueEnum.
  3. Seamless Integration: The beauty of this design is how it composes. Consider the SimpliLang code let y: int = x + 5;.

    • The generate_statement for let y will call generate_expression for x + 5.
    • The generate_expression for the binary + operator recursively calls generate_expression on its left side, which is x.
    • Your newly implemented Variable arm is executed. It looks up x, gets its pointer, and calls build_load. It returns the BasicValueEnum holding the integer value of x.
    • This value is passed back to the Binary arm, which now has the value of x as its lhs. It then generates the code for 5 as its rhs, and finally builds the add instruction. The system works perfectly!

You have now fully implemented the lifecycle of a variable at the machine level. You can allocate memory for it, store a value in it, and now, crucially, load that value back to use it in further computations.

Next Steps

You have conquered expressions and local variables. The next major frontier in code generation is control flow. According to the project roadmap, the next task is to implement code generation for if/else blocks. This will introduce you to one of the most fundamental concepts in LLVM: Basic Blocks and Branching Instructions. You will learn how to structure your code into distinct blocks and create conditional jumps between them, which is how all decision-making is modeled at a low level.

Further Reading

Implement If/Else Control Flow with LLVM Basic Blocks

Mục tiêu: Extend a compiler to support conditional execution by implementing code generation for if/else expressions. This task involves using the LLVM API to create and connect basic blocks for the ‘then’, ‘else’, and ‘merge’ paths, using conditional and unconditional branching instructions to build a control flow graph.


Of course! Let’s continue building your SimpliLang compiler.

You’ve made remarkable progress, successfully teaching your compiler how to manage a linear flow of execution. It can create variables on the stack with alloca, write to them with store, and read from them with load. This is the complete lifecycle for data management. Now, you’re ready to move beyond a straight line and give your language the power of decision-making.

This task is about implementing code generation for if/else expressions, which will introduce you to the fundamental building blocks of all control flow in LLVM: Basic Blocks and Branching Instructions.

Beyond a Single Stream: The Control Flow Graph

So far, all the instructions you’ve generated have been in a single, sequential list within a function. But an if statement breaks this linear flow. Based on a condition, the program must jump to one of two different sections of code (the “then” block or the “else” block) and then rejoin at a common point afterward.

LLVM models this with a concept called a Control Flow Graph, which is built from Basic Blocks.

  • A Basic Block is a straightforward sequence of instructions that has exactly one entry point (the first instruction) and one exit point (the last instruction). There are no jumps or branches in the middle of a block.
  • The last instruction of a basic block must be a Terminator Instruction—an instruction that transfers control, such as a branch (br) or a return (ret).

For an if/else expression, we need to construct a mini-graph of four blocks:

  1. The Entry Block: The block containing the code before the if statement. It will calculate the condition.
  2. The Then Block: Contains the instructions for the if’s body.
  3. The Else Block: Contains the instructions for the else’s body.
  4. The Merge Block: A common block where both the “then” and “else” paths converge. Code after the if/else expression will be generated here.

The terminator of the Entry Block will be a conditional branch. Based on the boolean condition, it will jump to either the Then Block or the Else Block. The terminators of the Then and Else blocks will be unconditional branches that both jump to the Merge Block.

Implementing if/else Code Generation

Let’s put this into practice by filling in the ast::Expr::If arm of your generate_expression method. We will use the builder to create these blocks and the branching instructions that wire them together.

First, you’ll need a new use statement at the top of your src/codegen/generator.rs file.

// Add this to the existing use statements at the top of the file
use inkwell::values::BasicValue;

Now, let’s implement the logic. This is the most complex piece of code generation you’ve written so far, so we’ll break it down with detailed comments.

// in src/codegen/generator.rs

// ... (inside the `impl<'ctx> CodeGen<'ctx>` block)

fn generate_expression(&mut self, expr: &ast::Expr) -> Option<BasicValueEnum<'ctx>> {
    match expr {
        // ... (Literal, Binary, Variable arms are unchanged)

        ast::Expr::If { condition, then_block, else_block } => {
            // --- Step 1: Get the current function context ---
            // We need a reference to the `FunctionValue` we are currently building
            // so that we can append new basic blocks to it.
            let parent_function = self.builder.get_insert_block()
                .and_then(|b| b.get_parent())
                .expect("Builder is not positioned inside a function.");

            // --- Step 2: Generate the condition's code ---
            // Recursively call `generate_expression` to get the LLVM value for the condition.
            let condition_val = self.generate_expression(condition)?
                .into_int_value();

            // --- Step 3: Create the basic blocks ---
            // We create three new blocks: one for the 'then' branch, one for the 'else',
            // and one 'merge' block for the code that comes after the if/else.
            let then_bb = self.context.append_basic_block(parent_function, "then");
            let else_bb = self.context.append_basic_block(parent_function, "else");
            let merge_bb = self.context.append_basic_block(parent_function, "merge");

            // --- Step 4: Build the conditional branch ---
            // This is the terminator for the current block. It will jump to `then_bb` if
            // `condition_val` is `1` (true), and to `else_bb` otherwise.
            self.builder.build_conditional_branch(condition_val, then_bb, else_bb);

            // --- Step 5: Populate the 'then' block ---
            // We move the builder's insertion point to the end of the `then_bb`.
            // Any new instructions will now be added here.
            self.builder.position_at_end(then_bb);
            // We generate the code for the AST's `then_block`.
            self.generate_block(then_block);
            // IMPORTANT: A valid basic block must have a terminator. If the `then_block`
            // doesn't end with a `return`, we must add an unconditional branch to the merge block.
            if then_bb.get_terminator().is_none() {
                self.builder.build_unconditional_branch(merge_bb);
            }

            // --- Step 6: Populate the 'else' block ---
            // We do the same for the `else` branch.
            self.builder.position_at_end(else_bb);
            if let Some(else_b) = else_block {
                self.generate_block(else_b);
            }
            // Just like the 'then' block, the 'else' block must also terminate.
            if else_bb.get_terminator().is_none() {
                self.builder.build_unconditional_branch(merge_bb);
            }

            // --- Step 7: Continue in the merge block ---
            // Finally, we move the builder to the `merge_bb`. All subsequent code
            // will be generated here, correctly positioned after both branches
            // of the if/else have completed.
            self.builder.position_at_end(merge_bb);

            // NOTE on Language Design: As identified during semantic analysis, `if` in
            // our current version of SimpliLang is used for control flow, not for
            // producing a value (e.g., `let x = if ...`). Therefore, this expression
            // does not produce an LLVM value to be returned. We return `None`.
            // Implementing `if` as a value-producing expression would require a
            // more complex construction using LLVM's `phi` instruction.
            None
        }

        // ... (rest of the unimplemented arms)
        ast::Expr::Unary { .. } => unimplemented!(),
        ast::Expr::FunctionCall { .. } => unimplemented!(),
    }
}

Dissecting the Implementation

This is a significant leap in complexity, so let’s review the key operations:

  1. parent_function: To add new basic blocks, we first need to know which function they belong to. self.builder.get_insert_block() gives us the current block, and .get_parent() gives us its owning function.
  2. context.append_basic_block(...): This is how we create new, empty basic blocks. We give them a name for readability in the final IR. They are appended to the end of the function’s block list.
  3. builder.build_conditional_branch(...): This is the core of the if. It creates a br i1 <cond>, label %then, label %else instruction. It consumes the i1 value from the condition and terminates the block it’s in.
  4. builder.position_at_end(...): This is how we navigate our control flow graph. We must explicitly tell the builder where to insert the next instruction. This is the most common source of bugs in LLVM code generation—forgetting to position the builder correctly.
  5. builder.build_unconditional_branch(...): This creates a br label %merge instruction. It is absolutely essential for ensuring that control flows correctly from the end of the then/else blocks to the merge point. Forgetting this step results in malformed LLVM IR because the blocks would lack terminators.

You have now given your language the power of conditional execution, one of the most fundamental features of any programming language. You’ve moved from simple, linear instruction lists to building a true graph of control flow.

Next Steps

You’ve conquered local control flow. The next logical step, and a major milestone, is to tackle the larger structure of the program: function definitions. In the upcoming tasks, you will implement the code generation for a FunctionDefinition AST node. This will involve:

  • Creating an LLVM FunctionValue.
  • Defining its signature (parameter types and return type).
  • Creating its entry basic block.
  • Generating code for its body.

Further Reading

Implement Code Generation for Function Definitions

Mục tiêu: Translate function definition nodes from the Abstract Syntax Tree (AST) into LLVM IR. This includes defining the function signature, creating an entry basic block, setting up parameters on the stack, and generating the code for the function’s body.


You have accomplished a truly significant feat by implementing control flow with if/else expressions. You’ve taught your compiler to navigate the forks in the road of a program’s logic, moving beyond simple, linear execution. This was a deep dive into the nature of Basic Blocks and Branching, the fundamental tools for building a Control Flow Graph.

With local control flow conquered, it’s time to zoom out and tackle the largest structural and organizational unit in SimpliLang: the function definition. While if/else constructs the pathways within a function, the function itself is the container for all that logic. It’s the primary unit of code that can be named, reused, and called from other parts of the program.

In this task, we will implement the code generation for a fn definition. This process involves creating a top-level FunctionValue in our LLVM module, defining its signature, setting up its initial “entry” block, and preparing the environment for the code in its body.

From AST Node to LLVM Function

In LLVM IR, a function is a first-class citizen. It has a name, a signature (parameter types and a return type), and a body composed of one or more basic blocks. Our goal is to translate the ast::FunctionDefinition struct into this LLVM representation.

The process is a sequence of carefully ordered steps:

  1. Define the Signature: First, we define the function’s “type.” This involves mapping the parameter and return types from our SimpliLang AST into their LLVM equivalents.
  2. Create the Function: We then add a function with this signature and a name to our LLVM Module.
  3. Establish the Entry Point: A function’s code must start somewhere. We’ll create an initial basic block, conventionally named “entry,” and attach it to our new function.
  4. Position the Builder: We’ll move our builder’s insertion point to the end of this new “entry” block. All subsequent instructions for the function body will be generated here.
  5. Process Parameters: Function parameters arrive as special values. To treat them like regular local variables (which is a best practice that simplifies code generation), we will allocate space for each one on the stack (alloca) and store the incoming parameter value into it.
  6. Generate the Body: With the setup complete, we will recursively call our existing generate_block method to generate the code for the function’s body.
  7. Ensure Termination: Finally, we’ll perform a crucial check. If a function is supposed to return void (no value) and the user’s code doesn’t end in an explicit return, the function’s last basic block will be left without a terminator. This is invalid LLVM IR. We must add an implicit return void to ensure correctness.

Implementing Function Definition Code Generation

Let’s put this plan into action by filling in the generate_function_definition method in src/codegen/generator.rs.

// in src/codegen/generator.rs

// ... (other use statements are unchanged)

impl<'ctx> CodeGen<'ctx> {
    // ... (new, map_type, generate, generate_statement, generate_expression are unchanged)

    // The main entry point for code generation.
    pub fn generate(&mut self, program: &ast::Program) {
        for function in &program.functions {
            // We now call our new, complete implementation.
            self.generate_function_definition(function);
        }
    }

    /// Generates code for a single function definition.
    fn generate_function_definition(&mut self, func: &ast::FunctionDefinition) {
        // --- Step 1: Define the function's signature ---
        let param_types: Vec<BasicTypeEnum<'ctx>> = func
            .parameters
            .iter()
            .map(|(_, ty)| self.map_type(ty))
            .collect();

        let fn_type = if let Some(ret_type) = &func.return_type {
            // We have a return type, so we use the corresponding LLVM type.
            self.map_type(ret_type).fn_type(&param_types, false)
        } else {
            // This is a "void" function that doesn't return a value.
            self.context.void_type().fn_type(&param_types, false)
        };

        // --- Step 2: Create the function in the module ---
        // `add_function` creates the `FunctionValue` but no basic blocks yet.
        let function = self.module.add_function(&func.name, fn_type, None);

        // Register the function in our map for later calls.
        self.functions.insert(func.name.clone(), function);

        // --- Step 3 & 4: Create the entry block and position the builder ---
        let entry_block = self.context.append_basic_block(function, "entry");
        self.builder.position_at_end(entry_block);

        // --- Step 5: Process parameters ---
        // IMPORTANT: Before generating the body, we must clear the `variables` map
        // from any previous function's context.
        self.variables.clear();

        for (i, (param_name, param_type)) in func.parameters.iter().enumerate() {
            // Get the LLVM value for the incoming parameter.
            let param_val = function.get_nth_param(i as u32).unwrap();

            // Allocate stack space for the parameter.
            let alloca = self.builder.build_alloca(self.map_type(param_type), param_name);

            // Store the incoming value into the allocated stack space.
            self.builder.build_store(alloca, param_val);

            // Register the parameter in our variables map so it can be looked up.
            self.variables.insert(param_name.clone(), alloca);
        }

        // --- Step 6: Generate the function body ---
        self.generate_block(&func.body);

        // --- Step 7: Ensure proper termination for void functions ---
        // If the function has no return type and the last block doesn't already have
        // a terminator (like a `return` instruction), we must add one.
        if func.return_type.is_none() {
            if let Some(last_block) = self.builder.get_insert_block() {
                if last_block.get_terminator().is_none() {
                    self.builder.build_return(None);
                }
            }
        }
    }

    // ... (generate_block and other methods)
}

Dissecting the Implementation

You have just built the machinery to construct the largest units of executable code in your language.

  1. Signature Creation: We use map to transform our AST parameter types into a Vec of LLVM types. The use of .fn_type() is the core of creating a function signature. We branch on whether our function has a return type, using void_type() for functions that don’t return anything.
  2. module.add_function: This is the official entry point for creating a new function within our LLVM module. It takes the name, the signature (fn_type), and an optional linkage type (we use None for the default).
  3. variables.clear(): This is a subtle but critical step. The local variables of one function must not be visible inside another. By clearing the variables map at the start of each new function’s generation, we ensure a clean slate and prevent cross-function contamination.
  4. Parameter Handling: The loop over func.parameters is a crucial best practice. By allocating stack space for each parameter and storing its initial value, we make parameters behave exactly like local variables declared with let. This means our Expr::Variable code generator, which uses build_load, will work on parameters without any modification. It unifies the concept of “named value” within a function.
  5. Implicit return void: The final check is a matter of correctness. A basic block must end with a terminator instruction. For a function that doesn’t return a value, if the code just “falls off the end” without an explicit return;, our compiler must insert the necessary ret void instruction to make the IR valid.

You have now successfully taught your compiler to translate the entire structure of a SimpliLang function into a valid, well-formed LLVM FunctionValue, complete with its signature, entry point, and parameter setup.

Next Steps

You’ve defined functions; now it’s time to call them! With your CodeGen struct’s functions map now being populated with FunctionValues, you have everything you need to implement code generation for Expr::FunctionCall nodes. In the next task, you will look up a function by name and use the builder.build_call instruction to generate the code that invokes it.

Further Reading

Implement Code Generation for Function Calls

Mục tiêu: Implement the logic for generating LLVM IR for function call expressions. This involves looking up the function, recursively generating code for its arguments, using the builder.build\_call method to create the call instruction, and handling the function’s return value.


Of course! Let’s dive into generating code for function calls.

You’ve just completed a massive and crucial step: teaching your compiler to understand and generate the entire structure of a function definition. In that process, you critically populated your CodeGen’s functions map, creating a directory that links every function’s name to its low-level FunctionValue representation. This “address book” of callable code is the essential prerequisite for this task. Now, we will use it to bring your language to life by implementing function calls.

A function call is the primary mechanism for code abstraction and reuse. When the compiler sees add(5, 10), its job is to generate the specific machine-level instructions that:

  1. Place the values 5 and 10 in the correct locations for the add function to find them.
  2. Transfer control of the program to the first instruction of the add function.
  3. Receive the result that add calculates and make it available for further use.

LLVM, via inkwell, makes this complex process remarkably straightforward with its call instruction.

The Strategy: Look Up, Prepare, and Call

Our strategy to generate a call instruction is a direct translation of the process described above:

  1. Look Up: We’ll take the function’s name from the AST and look it up in our functions map to get the FunctionValue we need to call.
  2. Prepare Arguments: A function call can have arguments, which are themselves expressions. We will recursively call our powerful generate_expression method for each argument to compute their LLVM values.
  3. Build the Call: With the target FunctionValue and a list of argument BasicValueEnums in hand, we will use our builder’s build_call method to generate the call instruction.
  4. Handle the Return: The call instruction itself produces a value—the function’s return value. We will capture this and return it, allowing the result of a function call to be used in other expressions, like let result = add(5, 10) * 2;.

Let’s implement this logic by filling in the ast::Expr::FunctionCall arm of your generate_expression method.

Implementing Function Call Code Generation

First, we need to add a couple of new use statements to the top of src/codegen/generator.rs to handle the return value of a call instruction.

// Add these to the use statements at the top of src/codegen/generator.rs
use inkwell::values::CallSiteValue;
use either::Either::{Left, Right};

Now, let’s update the generate_expression method with the core logic for this task.

// in src/codegen/generator.rs

// ... (inside the `impl<'ctx> CodeGen<'ctx>` block)

fn generate_expression(&mut self, expr: &ast::Expr) -> Option<BasicValueEnum<'ctx>> {
    match expr {
        // ... (Literal, Binary, Variable, and If arms are unchanged)

        ast::Expr::FunctionCall { fn_name, arguments } => {
            // 1. Look up the function's `FunctionValue` in our map.
            // We can use `.expect()` because the semantic analyzer has already
            // guaranteed that this function is defined and valid.
            let function = self.functions.get(fn_name)
                .expect("Semantic analysis ensures function exists.");

            // 2. Recursively generate the LLVM value for each argument expression.
            // We use a `collect` that can handle the `Option` returned by `generate_expression`.
            // If any argument fails to generate, the whole result becomes `None`.
            let args_llvm: Option<Vec<BasicValueEnum<'ctx>>> = arguments
                .iter()
                .map(|arg_expr| self.generate_expression(arg_expr))
                .collect();

            // If argument generation failed, propagate the error.
            let args_llvm = match args_llvm {
                Some(args) => args,
                None => return None,
            };

            // 3. Use the builder to create the `call` instruction.
            // The builder takes the function to call, a slice of the argument values,
            // and a name for the temporary register that will hold the return value.
            let call_site = self.builder.build_call(*function, &args_llvm, "calltmp");

            // 4. Handle the return value from the call.
            // `try_as_basic_value()` returns an `Either` enum.
            // `Left` contains a `BasicValueEnum` if the function returns a value.
            // `Right` contains an `InstructionValue` if the function is `void`.
            match call_site.try_as_basic_value() {
                // The function returned a value. We can return this from our
                // `generate_expression` call, allowing it to be used by other expressions.
                Left(basic_value) => Some(basic_value),

                // The function was void (did not return a value). Therefore, this
                // function call expression itself has no value. We return `None`.
                Right(_) => None,
            }
        }

        // ... (rest of the unimplemented arms)
        ast::Expr::Unary { .. } => unimplemented!(),
    }
}

Dissecting the Implementation

You have just taught your compiler how to orchestrate the most fundamental form of abstraction in programming.

  1. Function Lookup: self.functions.get(fn_name) is us consulting our “address book.” This is the direct payoff from the work you did in the previous task to populate this map during function definition.
  2. Argument Generation: The map and collect combination is a concise and idiomatic Rust way to handle the recursive generation. It attempts to generate code for every argument. If even one of them fails (e.g., it contains an undeclared variable, which shouldn’t happen post-semantic-analysis but is good to handle defensively), the entire collection becomes None, and we gracefully stop processing this call.
  3. builder.build_call: This is the star of the show. It’s a high-level inkwell method that abstracts away the complexities of the underlying machine’s calling convention (the specific rules for how arguments are passed and return values are handled). It generates the call instruction in the IR, which looks something like %calltmp = call i64 @add(i64 5, i64 10).
  4. CallSiteValue and Either: This is a new and important concept. A function call doesn’t always produce a value that can be used in an expression. A void function, for instance, performs an action but doesn’t yield a result. The inkwell API models this duality with the either crate. The CallSiteValue returned by build_call can be resolved with try_as_basic_value().

    • If the function returns a value, we get Left(BasicValueEnum), which we can return.
    • If the function is void, we get Right(InstructionValue), indicating the call itself is an instruction but not one that produces a usable value. In this case, we correctly return None.

You have now connected the two most important parts of a program’s structure: the definition of a function and its invocation.

Next Steps

You’ve successfully generated code to call a function and handle its return value. The final piece of the function puzzle is to generate the code that provides that return value from within the function being called. In the next task, you will implement code generation for return statements using the builder.build_return instruction.

Further Reading

Implement Return Statement Code Generation in LLVM

Mục tiêu: Implement the code generation for the return statement in a Rust-based compiler. This involves translating the Return AST node into an LLVM ret instruction using the inkwell builder, which completes the function call lifecycle.


Of course! Let’s get this final piece of the code generation puzzle in place.

You have masterfully orchestrated the generation of function definitions and the call instructions that invoke them. You’ve created the “caller” and the “callee.” The final link in this chain is the mechanism by which the “callee” sends a value back to the “caller.” This task is all about implementing that crucial final step: the return statement.

The ret Instruction: Fulfilling the Contract

In the previous task, you generated a call instruction, which patiently waits for the function to complete and yield a result. The return statement is how the function provides that result. At the machine level, this is handled by the ret instruction.

The ret instruction does two critical things:

  1. It specifies the value to be returned to the caller.
  2. It transfers control of the program back to the instruction immediately following the call.

Crucially, ret is a terminator instruction. This means it definitively ends a basic block. No more instructions can be added to a block after a return. This is a fundamental rule in LLVM, and inkwell’s builder will enforce it. Any code a user writes after a return in the same block is effectively “dead code.”

Our strategy is simple and direct:

  1. When we encounter a Stmt::Return(expr) AST node, we will first recursively call our powerful generate_expression method on the expr. This will yield the BasicValueEnum that needs to be returned.
  2. We will then pass this value to our builder’s build_return method, which will generate the corresponding ret instruction.

Implementing return Statement Code Generation

Let’s update the generate_statement method in your src/codegen/generator.rs file. The change is focused entirely on the ast::Stmt::Return arm.

Here is the updated code block. We are only changing the one match arm.

// in src/codegen/generator.rs

// ... inside the `impl<'ctx> CodeGen<'ctx>` block
fn generate_statement(&mut self, stmt: &ast::Stmt) {
    match stmt {
        ast::Stmt::Let { var_name, var_type, initializer } => {
            // This logic is unchanged from the previous task.
            /* ... */
        }
        ast::Stmt::Return(expr) => {
            // 1. Recursively generate the LLVM value for the expression to be returned.
            //    Our semantic analyzer has already guaranteed that this expression is valid
            //    and its type matches the function's return signature. Therefore, we can
            //    confidently `.expect()` a valid value here.
            let return_val = self.generate_expression(expr)
                .expect("Semantic analysis ensures the return expression is valid and typed.");

            // 2. Use the builder to create the `ret` instruction.
            //    We pass `Some(&return_val)` to generate `ret <type> <value>`.
            //    This instruction terminates the current basic block.
            self.builder.build_return(Some(&return_val));
        }
        ast::Stmt::Expression(expr) => {
            // This logic is unchanged.
            self.generate_expression(expr);
        }
    }
}

Dissecting the Implementation

This small addition is the capstone of your function generation logic.

  1. Trusting the Semantic Analyzer: The line self.generate_expression(expr).expect(...) embodies the compiler’s layered architecture. The code generation phase operates under the assumption that the semantic analysis phase has done its job correctly. We don’t need to re-check the types here. We can confidently expect that generate_expression will return Some(value), and if it doesn’t, it signifies a bug in our compiler, which the .expect() will correctly catch.
  2. builder.build_return: This is the core of the implementation. It’s a clean, high-level wrapper around LLVM’s ret instruction. By passing Some(&return_val), you are explicitly telling inkwell to generate a return instruction that includes a value. This single method call correctly terminates the current basic block and wires up the return value for the caller.

Putting It All Together: The Function Call Lifecycle

With this change, you have now implemented the complete round-trip of a function call:

  1. Definition (generate_function_definition): You create a FunctionValue, set up its entry block, and prepare its parameters on the stack.
  2. Invocation (generate_expression for FunctionCall): You look up the FunctionValue and use build_call to transfer control to the function’s entry block, passing along the arguments.
  3. Execution (various generators): The instructions inside the function body execute, performing calculations and manipulating variables.
  4. Return (generate_statement for Return): You now use build_return to send a value back and transfer control back to the caller. The call instruction receives this value, which can then be used in subsequent computations.

This is the complete, fundamental cycle of procedural abstraction, now fully implemented in your compiler!

Next Steps

This task marks the successful completion of the entire core code generation step! You have taught your compiler to translate every feature of the SimpliLang AST—literals, expressions, variables, control flow, and functions—into valid LLVM Intermediate Representation. This is a monumental milestone.

The LLVM IR you generate is functional, but it’s likely not as efficient as it could be. One of LLVM’s greatest strengths is its vast suite of optimization capabilities. The next major step in your compiler journey is Step 7: Leverage LLVM’s powerful optimization capabilities. You will learn how to create an LLVM PassManager and apply a set of standard optimization passes (like mem2reg, constant folding, and instruction combining) to your generated IR, transforming it from correct code into highly efficient code.

Further Reading