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 10: Implementing Directives

In this chapter, we’ll implement handlers for all ByteASM directives.

Directive Overview

DirectivePurposeExample
.orgSet assembly address.org 0x8000
.dbDefine bytes.db 0x01, "Hi", 0
.dwDefine words.dw 0x1234, label
.equDefine constant.equ SCREEN 0x1000
.includeInclude file.include "utils.s"

The .org Directive

.org (origin) sets the current assembly address.

Pass 1 Handling

#![allow(unused)]
fn main() {
DirectiveStmt::Org { address, .. } => {
    let addr = self.evaluate(address)? as u16;
    self.origin = addr;
    self.current_address = addr;
}
}

Pass 2 Handling

#![allow(unused)]
fn main() {
DirectiveStmt::Org { address, .. } => {
    let addr = self.evaluate(address)? as u16;

    // If moving forward, pad with zeros
    if addr > self.current_address {
        self.pad_to(addr);
    }

    self.current_address = addr;
}
}

Usage Examples

.org 0x8000         ; Start code at 0x8000
reset:
    ; ... code ...

.org 0xFFFC         ; Jump to reset vector location
.dw reset           ; Write reset vector

Multiple .org Directives

.org 0x8000
    lda #0x01       ; At 0x8000

.org 0x8100         ; Skip to 0x8100 (gap filled with zeros)
    lda #0x02       ; At 0x8100

The .db Directive

.db (define bytes) emits raw bytes and strings.

Pass 1 Handling

Calculate size without emitting:

#![allow(unused)]
fn main() {
DirectiveStmt::Db { values, .. } => {
    for value in values {
        match value {
            DataValue::Byte(_) => {
                self.current_address = self.current_address.wrapping_add(1);
            }
            DataValue::String(s) => {
                self.current_address = self.current_address.wrapping_add(s.len() as u16);
            }
        }
    }
}
}

Pass 2 Handling

Emit the actual bytes:

#![allow(unused)]
fn main() {
DirectiveStmt::Db { values, location } => {
    for value in values {
        match value {
            DataValue::Byte(expr) => {
                let byte = self.evaluate_byte(expr, *location)?;
                self.emit_byte(byte);
            }
            DataValue::String(s) => {
                for byte in s.bytes() {
                    self.emit_byte(byte);
                }
            }
        }
    }
}
}

Usage Examples

; Single bytes
.db 0x00, 0xFF, 0x42

; Expressions
.db 10 + 5, CONSTANT - 1

; Strings
.db "Hello, World!", 0x0A, 0

; Mixed
.db "Score: ", 0x30, 0  ; "Score: 0\0"

Strings and Escape Sequences

The scanner handles escape sequences, so:

.db "Line 1\nLine 2\0"

Emits: 4C 69 6E 65 20 31 0A 4C 69 6E 65 20 32 00

The .dw Directive

.dw (define words) emits 16-bit values in little-endian format.

Pass 1 Handling

#![allow(unused)]
fn main() {
DirectiveStmt::Dw { values, .. } => {
    self.current_address = self.current_address
        .wrapping_add((values.len() * 2) as u16);
}
}

Pass 2 Handling

#![allow(unused)]
fn main() {
DirectiveStmt::Dw { values, location } => {
    for value in values {
        let word = self.evaluate_word(value, *location)?;
        self.emit_word(word);
    }
}
}

Usage Examples

; Numeric values
.dw 0x1234, 0x5678      ; Emits: 34 12 78 56

; Labels (addresses)
.dw start, main_loop    ; Emits addresses

; Interrupt vectors
.org 0xFFFC
.dw reset               ; Reset vector
.dw irq_handler         ; IRQ vector

The .equ Directive

.equ defines a constant that can be used throughout the program.

Pass 1 Handling

#![allow(unused)]
fn main() {
DirectiveStmt::Equ { name, value, location } => {
    let val = self.evaluate(value)?;
    self.symbols.define_constant(name, val, *location)?;
}
}

Pass 2 Handling

#![allow(unused)]
fn main() {
DirectiveStmt::Equ { .. } => {
    // Constants are handled in pass 1, nothing to emit
}
}

Usage Examples

; Hardware addresses
.equ VID_PTR    0xFD
.equ INPUT      0xFF

; Constants
.equ SCREEN_WIDTH  64
.equ SCREEN_HEIGHT 64
.equ PIXEL_COUNT   SCREEN_WIDTH * SCREEN_HEIGHT

; Usage
lda INPUT
sta VID_PTR
ldx #SCREEN_WIDTH

Constants vs Labels

Constants differ from labels:

  • Labels: Get their value from their position in code
  • Constants: Have values assigned directly
.equ CONST 0x42     ; CONST = 0x42 (assigned)

label:              ; label = current address (computed)
    nop

The .include Directive

.include inserts another source file at the current position.

Implementation (Simplified)

#![allow(unused)]
fn main() {
DirectiveStmt::Include { path, location } => {
    // Read the include file
    let source = std::fs::read_to_string(path)
        .map_err(|_| CodeGenError::Internal {
            message: format!("cannot read file: {}", path),
        })?;

    // Parse it
    let program = parser::parse(&source)?;

    // Recursively assemble
    for stmt in program.statements {
        self.process_statement(&stmt)?;
    }
}
}

Usage Examples

; main.s
.include "constants.s"
.include "macros.s"

.org 0x8000
reset:
    jsr init_screen
    ; ...
; constants.s
.equ VID_PTR 0xFD
.equ INPUT   0xFF
.equ VRAM    0x1000

Circular Include Detection

We need to detect and prevent circular includes:

; a.s includes b.s
; b.s includes a.s → Error!

Track included files and error if we see a repeat.

Directive Processing Flow

Complete Pass 1 Handler

#![allow(unused)]
fn main() {
pub fn pass1_directive(&mut self, directive: &DirectiveStmt) -> Result<(), CodeGenError> {
    match directive {
        DirectiveStmt::Org { address, .. } => {
            let addr = self.evaluate(address)? as u16;
            self.origin = addr;
            self.current_address = addr;
        }

        DirectiveStmt::Db { values, .. } => {
            for value in values {
                match value {
                    DataValue::Byte(_) => {
                        self.current_address = self.current_address.wrapping_add(1);
                    }
                    DataValue::String(s) => {
                        self.current_address =
                            self.current_address.wrapping_add(s.len() as u16);
                    }
                }
            }
        }

        DirectiveStmt::Dw { values, .. } => {
            self.current_address = self
                .current_address
                .wrapping_add((values.len() * 2) as u16);
        }

        DirectiveStmt::Equ { name, value, location } => {
            let val = self.evaluate(value)?;
            self.symbols.define_constant(name, val, *location)?;
        }

        DirectiveStmt::Include { .. } => {
            // Handle includes
        }
    }

    Ok(())
}
}

Complete Pass 2 Handler

#![allow(unused)]
fn main() {
pub fn emit_directive(&mut self, directive: &DirectiveStmt) -> Result<(), CodeGenError> {
    match directive {
        DirectiveStmt::Org { address, .. } => {
            let addr = self.evaluate(address)? as u16;
            if addr > self.current_address {
                self.pad_to(addr);
            }
            self.current_address = addr;
        }

        DirectiveStmt::Db { values, location } => {
            for value in values {
                match value {
                    DataValue::Byte(expr) => {
                        let byte = self.evaluate_byte(expr, *location)?;
                        self.emit_byte(byte);
                    }
                    DataValue::String(s) => {
                        for byte in s.bytes() {
                            self.emit_byte(byte);
                        }
                    }
                }
            }
        }

        DirectiveStmt::Dw { values, location } => {
            for value in values {
                let word = self.evaluate_word(value, *location)?;
                self.emit_word(word);
            }
        }

        DirectiveStmt::Equ { .. } => {
            // Nothing to emit
        }

        DirectiveStmt::Include { .. } => {
            // Handle includes
        }
    }

    Ok(())
}
}

Summary

In this chapter, we implemented all ByteASM directives:

  • .org: Sets the assembly address, pads with zeros
  • .db: Emits bytes and strings
  • .dw: Emits 16-bit words (little-endian)
  • .equ: Defines named constants
  • .include: Includes other source files

In the next chapter, we’ll implement comprehensive error handling and reporting.


Previous: Chapter 9 - Expression Evaluation | Next: Chapter 11 - Error Handling