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 12: The Command-Line Interface

In this chapter, we’ll build a usable command-line tool for our assembler.

CLI Design

byte_asm [OPTIONS] <input.s>

OPTIONS:
    -o, --output <file>   Output binary file (default: a.out)
    -v, --verbose         Show assembly progress
    --hex                 Output as hex dump instead of binary
    -h, --help            Show help message

Argument Parsing

We’ll parse arguments manually for simplicity:

#![allow(unused)]
fn main() {
struct Args {
    input: PathBuf,
    output: PathBuf,
    verbose: bool,
    hex_dump: bool,
}

fn parse_args() -> Result<Args, String> {
    let args: Vec<String> = std::env::args().collect();

    if args.len() < 2 {
        return Err(format!(
            "Usage: {} [OPTIONS] <input.s>\n\n\
             OPTIONS:\n\
             -o, --output <file>   Output binary file (default: a.out)\n\
             -v, --verbose         Show assembly progress\n\
             --hex                 Output as hex dump",
            args[0]
        ));
    }

    let mut input: Option<PathBuf> = None;
    let mut output = PathBuf::from("a.out");
    let mut verbose = false;
    let mut hex_dump = false;

    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "-o" | "--output" => {
                i += 1;
                if i >= args.len() {
                    return Err("Expected output file after -o".to_string());
                }
                output = PathBuf::from(&args[i]);
            }
            "-v" | "--verbose" => verbose = true,
            "--hex" => hex_dump = true,
            "-h" | "--help" => {
                return Err(format!(
                    "ByteASM - 6502 Assembler\n\n\
                     Usage: {} [OPTIONS] <input.s>\n\n\
                     OPTIONS:\n\
                     -o, --output <file>   Output file (default: a.out)\n\
                     -v, --verbose         Show progress\n\
                     --hex                 Output hex dump\n\
                     -h, --help            Show help",
                    args[0]
                ));
            }
            arg if arg.starts_with('-') => {
                return Err(format!("Unknown option: {}", arg));
            }
            _ => {
                input = Some(PathBuf::from(&args[i]));
            }
        }
        i += 1;
    }

    let input = input.ok_or("No input file specified")?;

    Ok(Args { input, output, verbose, hex_dump })
}
}

Main Function

fn main() {
    let args = match parse_args() {
        Ok(args) => args,
        Err(msg) => {
            eprintln!("{}", msg);
            std::process::exit(1);
        }
    };

    if args.verbose {
        eprintln!("Assembling: {}", args.input.display());
    }

    // Read source file
    let source = match fs::read_to_string(&args.input) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("Error reading {}: {}", args.input.display(), e);
            std::process::exit(1);
        }
    };

    // Parse
    let program = match parser::parse(&source) {
        Ok(p) => p,
        Err(errors) => {
            for error in errors {
                eprintln!("{}", format_error(&error, &source, &args.input));
            }
            std::process::exit(1);
        }
    };

    if args.verbose {
        eprintln!("Parsed {} statements", program.statements.len());
    }

    // Assemble
    let mut assembler = Assembler::new();
    let binary = match assembler.assemble(&program) {
        Ok(b) => b,
        Err(e) => {
            eprintln!("{}", format_assembler_error(&e, &source, &args.input));
            std::process::exit(1);
        }
    };

    if args.verbose {
        eprintln!("Generated {} bytes", binary.len());
        eprintln!("Defined {} symbols", assembler.symbols().len());
    }

    // Output
    if args.hex_dump {
        print_hex_dump(&binary);
    } else {
        if let Err(e) = fs::write(&args.output, &binary) {
            eprintln!("Error writing {}: {}", args.output.display(), e);
            std::process::exit(1);
        }

        if args.verbose {
            eprintln!("Wrote: {}", args.output.display());
        }
    }
}

Hex Dump Output

For debugging, we can output a hex dump instead of binary:

#![allow(unused)]
fn main() {
fn print_hex_dump(binary: &[u8]) {
    const BYTES_PER_LINE: usize = 16;

    for (i, chunk) in binary.chunks(BYTES_PER_LINE).enumerate() {
        // Address
        print!("{:04X}  ", i * BYTES_PER_LINE);

        // Hex bytes
        for (j, byte) in chunk.iter().enumerate() {
            print!("{:02X} ", byte);
            if j == 7 { print!(" "); }  // Extra space in middle
        }

        // Padding for incomplete lines
        if chunk.len() < BYTES_PER_LINE {
            let missing = BYTES_PER_LINE - chunk.len();
            for j in 0..missing {
                print!("   ");
                if chunk.len() + j == 7 { print!(" "); }
            }
        }

        // ASCII representation
        print!(" |");
        for byte in chunk {
            if *byte >= 0x20 && *byte < 0x7F {
                print!("{}", *byte as char);
            } else {
                print!(".");
            }
        }
        println!("|");
    }
}
}

Example Output

$ byte_asm --hex example.s

0000  A9 42 85 00 4C 00 80 00  00 00 00 00 00 00 00 00  |.B..L...........|
0010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

Integration with Byte Emulator

The output binary is ready to load into the emulator:

# Assemble
cargo run -p byte_asm -- game.s -o game.bin

# Run
cargo run -p byte_emu -- game.bin

Setting Up the Reset Vector

The emulator reads the reset vector from 0xFFFC-0xFFFD:

.equ VID_PTR  0xFD
.equ VID_PAGE 0x01       ; Video page 1 (0x1000 >> 12)

.org 0x8000
reset:
    ; Initialize hardware
    lda #VID_PAGE
    sta VID_PTR         ; Set video page

main_loop:
    ; Game logic
    rti

; Set up vectors
.org 0xFFFC
.dw reset           ; Reset vector - where to start
.dw main_loop       ; IRQ vector - called on VBLANK

Verbose Output Example

$ byte_asm -v game.s -o game.bin

Assembling: game.s
Parsed 42 statements
Generated 156 bytes
Defined 12 symbols
Wrote: game.bin

Error Output Example

$ byte_asm broken.s

error: undefined symbol 'sprit_x'
  --> broken.s:15:9
   |
15 |     lda sprit_x
   |         ^^^^^^^ symbol not defined

error: branch target out of range
  --> broken.s:42:5
   |
42 |     bne far_label
   |     ^^^^^^^^^^^^^ offset -150, must be -128 to +127

Found 2 errors.

Usage Workflow

A typical workflow:

# Edit source
vim game.s

# Assemble
byte_asm game.s -o game.bin

# Test in emulator
byte_emu game.bin

# Debug with hex dump
byte_asm game.s --hex | less

# Verbose build
byte_asm -v game.s -o game.bin

Exit Codes

#![allow(unused)]
fn main() {
// Success
std::process::exit(0);

// Error (parse, assembly, I/O)
std::process::exit(1);
}

Summary

In this chapter, we built a command-line interface that:

  • Parses command-line arguments
  • Reads source files
  • Invokes the parser and assembler
  • Outputs binary or hex dump
  • Reports errors with context
  • Integrates with the byte emulator

In the next chapter, we’ll write tests to verify our assembler works correctly.


Previous: Chapter 11 - Error Handling | Next: Chapter 13 - Testing