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
| Expression | Result |
|---|---|
10 + 5 | 15 |
20 - 3 | 17 |
4 * 8 | 32 |
100 / 10 | 10 |
0x1000 + 0x100 | 0x1100 |
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
| Expression | Value | Result |
|---|---|---|
<0x1234 | 0x1234 | 0x34 |
>0x1234 | 0x1234 | 0x12 |
<0xFF00 | 0xFF00 | 0x00 |
>0x00FF | 0x00FF | 0x00 |
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:
- Parentheses
()- highest - Unary operators
-,<,> - Multiplication and division
*,/ - 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