Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 9: Expression Evaluation

In this chapter, we’ll implement the expression evaluator that computes numeric values from AST expressions.

Why Expressions Matter

Expressions allow powerful constructs in assembly:

.equ BUFFER      0x0200
.equ BUFFER_SIZE 256

lda #BUFFER_SIZE - 1     ; Compute at assembly time
ldx #<BUFFER             ; Low byte of address
ldy #>BUFFER             ; High byte of address
.dw BUFFER + BUFFER_SIZE ; End of buffer
jmp $                    ; Jump to current address

The Evaluator

The evaluator recursively walks an Expression tree and computes the result:

#![allow(unused)]
fn main() {
impl Assembler {
    pub fn evaluate(&self, expr: &Expression) -> Result<i64, CodeGenError> {
        self.evaluate_at(expr, self.current_address, Location::default())
    }

    fn evaluate_at(
        &self,
        expr: &Expression,
        current_addr: u16,
        location: Location,
    ) -> Result<i64, CodeGenError> {
        match expr {
            Expression::Number(n) => Ok(*n),

            Expression::CurrentAddress => Ok(current_addr as i64),

            Expression::Identifier(name) => {
                self.symbols.lookup_value(name).ok_or_else(|| {
                    CodeGenError::UndefinedSymbol {
                        name: name.clone(),
                        location,
                    }
                })
            }

            Expression::LocalIdentifier(name) => {
                // Try direct lookup
                if let Some(value) = self.symbols.lookup_value(name) {
                    return Ok(value);
                }
                // Try qualified lookup
                let qualified = self.symbols.qualify_local_label(name);
                self.symbols.lookup_value(&qualified).ok_or_else(|| {
                    CodeGenError::UndefinedSymbol {
                        name: name.clone(),
                        location,
                    }
                })
            }

            Expression::Binary { left, op, right } => {
                let l = self.evaluate_at(left, current_addr, location)?;
                let r = self.evaluate_at(right, current_addr, location)?;
                self.apply_binary_op(l, *op, r, location)
            }

            Expression::Unary { op, operand } => {
                let value = self.evaluate_at(operand, current_addr, location)?;
                self.apply_unary_op(*op, value)
            }
        }
    }
}
}

Binary Operations

#![allow(unused)]
fn main() {
fn apply_binary_op(
    &self,
    left: i64,
    op: BinaryOp,
    right: i64,
    location: Location,
) -> Result<i64, CodeGenError> {
    match op {
        BinaryOp::Add => Ok(left.wrapping_add(right)),
        BinaryOp::Sub => Ok(left.wrapping_sub(right)),
        BinaryOp::Mul => Ok(left.wrapping_mul(right)),
        BinaryOp::Div => {
            if right == 0 {
                Err(CodeGenError::EvaluationError {
                    message: "division by zero".to_string(),
                    location,
                })
            } else {
                Ok(left / right)
            }
        }
    }
}
}

Examples

ExpressionResult
10 + 515
20 - 317
4 * 832
100 / 1010
0x1000 + 0x1000x1100

Unary Operations

#![allow(unused)]
fn main() {
fn apply_unary_op(&self, op: UnaryOp, value: i64) -> Result<i64, CodeGenError> {
    match op {
        UnaryOp::Neg => Ok(-value),
        UnaryOp::LoByte => Ok(value & 0xFF),
        UnaryOp::HiByte => Ok((value >> 8) & 0xFF),
    }
}
}

Lo-Byte and Hi-Byte Operators

These are essential for working with 16-bit addresses on an 8-bit processor:

.equ SCREEN 0x1234

lda #<SCREEN    ; Low byte: 0x34
sta ptr
lda #>SCREEN    ; High byte: 0x12
sta ptr+1
ExpressionValueResult
<0x12340x12340x34
>0x12340x12340x12
<0xFF000xFF000x00
>0x00FF0x00FF0x00

The Current Address ($)

The $ symbol represents the current address during assembly:

.org 0x8000
loop:
    jmp $       ; Jump to self (infinite loop) - jumps to 0x8000
    .dw $       ; Store current address

When evaluating $, we use the address at the start of the instruction, not after emitting bytes:

#![allow(unused)]
fn main() {
Expression::CurrentAddress => Ok(current_addr as i64),
}

This is why we pass instr_start when evaluating operands.

Range Checking

We provide variants that check the result fits in the required size:

#![allow(unused)]
fn main() {
pub fn evaluate_byte(
    &self,
    expr: &Expression,
    location: Location,
) -> Result<u8, CodeGenError> {
    let value = self.evaluate_with_location(expr, location)?;

    // Allow -128 to 255 (signed or unsigned byte)
    if value < -128 || value > 255 {
        return Err(CodeGenError::ValueOutOfRange {
            value,
            max: 255,
            location,
        });
    }

    Ok(value as u8)
}

pub fn evaluate_word(
    &self,
    expr: &Expression,
    location: Location,
) -> Result<u16, CodeGenError> {
    let value = self.evaluate_with_location(expr, location)?;

    if value < -32768 || value > 65535 {
        return Err(CodeGenError::ValueOutOfRange {
            value,
            max: 65535,
            location,
        });
    }

    Ok(value as u16)
}
}

Evaluation with Custom Address

For correct $ handling during code generation:

#![allow(unused)]
fn main() {
pub fn evaluate_byte_at(
    &self,
    expr: &Expression,
    location: Location,
    current_addr: u16,
) -> Result<u8, CodeGenError> {
    let value = self.evaluate_at(expr, current_addr, location)?;

    if value < -128 || value > 255 {
        return Err(CodeGenError::ValueOutOfRange {
            value,
            max: 255,
            location,
        });
    }

    Ok(value as u8)
}
}

Complex Expression Examples

Computing Buffer End

.equ BUFFER_START 0x0200
.equ BUFFER_SIZE  0x0100

; BUFFER_END = BUFFER_START + BUFFER_SIZE = 0x0300
lda #>BUFFER_START + BUFFER_SIZE    ; Error? No, it's 0x03
lda #>(BUFFER_START + BUFFER_SIZE)  ; Same: 0x03

Table Offsets

.equ SPRITE_SIZE 4
.equ SPRITE_COUNT 8

; Total size = 4 * 8 = 32 bytes
.equ SPRITE_TABLE_SIZE SPRITE_SIZE * SPRITE_COUNT

; Offset to sprite N: N * SPRITE_SIZE
lda sprite_table + 2 * SPRITE_SIZE  ; Third sprite

Relative Jumps

    beq $ + 3   ; Skip next 1-byte instruction if equal
    nop
    rts

Here $ + 3 evaluates to current_address + 3.

Evaluation Order

Expressions follow standard mathematical precedence:

  1. Parentheses () - highest
  2. Unary operators -, <, >
  3. Multiplication and division *, /
  4. Addition and subtraction +, - - lowest

So 2 + 3 * 4 evaluates as 2 + (3 * 4) = 14, not (2 + 3) * 4 = 20.

Error Messages

When evaluation fails, we provide helpful messages:

error: undefined symbol 'sprite_ptr'
  --> game.s:42:5
   |
42 |     lda sprite_ptr
   |         ^^^^^^^^^^ symbol not defined

error: value 300 out of range (max 255)
  --> game.s:15:9
   |
15 |     lda #300
   |         ^^^^ value too large for byte

Summary

In this chapter, we implemented expression evaluation:

  • Numeric literals: Direct values
  • Identifiers: Symbol table lookups
  • Binary operations: +, -, *, /
  • Unary operations: negation, lo-byte, hi-byte
  • Current address: $ symbol
  • Range checking: Ensure values fit in bytes/words

In the next chapter, we’ll implement directive handlers for .org, .db, .dw, etc.


Previous: Chapter 8 - Code Generation | Next: Chapter 10 - Implementing Directives