A simple 6502 assembler written in Rust
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

234 lines
7.5 KiB

nyasm, a simple 6502 assembler written in Rust
Copyright (C) 2021 tromino <trominode@pm.me>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
extern crate clap;
use std::fs;
use std::process;
use std::io;
use std::io::{Read, Write};
use std::collections::HashMap;
mod dot;
mod assembly;
mod util;
fn main() {
// Some variables that get passed around and used in various places
let mut did_error = false; // Output files will not be written to if this ever gets set to true
let mut org: Box<u16> = Box::new(0u16); // The current origin address, set with .org in the source ASM
let mut labels: HashMap<String, u16> = HashMap::new(); // Whenever a label is declared, it will be added here
let mut label_requests: Vec<(u16, String, bool)> = vec![]; // The locations in the assembled binary where the address of a label should be inserted
// Setting up CLI arguments processing
let arg_input = clap::Arg::with_name("INPUT")
let arg_output = clap::Arg::with_name("output")
let cli_args = clap::App::new("nyasm")
let matches = cli_args.get_matches();
let input_filename = matches.value_of("INPUT").unwrap_or("-");
let output_filename = matches.value_of("output").unwrap_or("-");
let mut input_asm = String::new(); // Holds the source assembly in text form
if input_filename == "-" {
let read_result = io::stdin().read_to_string(&mut input_asm);
if read_result.is_err() {
util::assembler_error("Failed to read from standard input", input_filename, &mut did_error);
} else {
input_asm = handle_input_error(fs::read_to_string(input_filename), input_filename, &mut did_error);
let line_array = input_asm.lines(); // The ASM text, but as an array of strings split by lines
let mut asm_array: Vec<[String; 2]> = vec![]; // An array of lines, but this time they're formatted in a consistent way and separated into the instruction and the parameters
// Now it's time to populate asm_array
for line in line_array {
// First we strip out comments and unnecessary whitespace
let line_without_comments = line.split(";").collect::<Vec<&str>>()[0];
let line_trimmed_spaces = line_without_comments.trim();
// If the line is empty then we don't need to worry about it anymore
if line_trimmed_spaces != "" {
let line_lowercase = line_trimmed_spaces.to_lowercase();
let line_as_array: [String; 2]; // This is where we'll split the instruction name from its parameters into
if &line_lowercase[line_lowercase.len() - 1 ..] == ":" {
// If we're defining a label, then convert the syntax into a ".label" directive
let label_name = line_lowercase[.. line_lowercase.len() - 1].split_whitespace().collect::<Vec<&str>>()[0];
line_as_array = [String::from(".label"), String::from(label_name)];
} else {
let line_as_vector: Vec<&str> = line_trimmed_spaces.splitn(2, " ").collect();
let instruction = line_as_vector[0];
if line_as_vector.len() > 1 {
// If the instruction has any parameters, then we need to process those too
let parameters = line_as_vector[1].trim_start();
if !(
&String::from(parameters)[.. 1] == "\"" &&
String::from(parameters).len() - 1 ..
] == "\""
) {
// If the parameters section isn't wrapped in quotes, then we can make it lowercase
line_as_array = [
} else {
// But if it is, cut out the quotes and keep the casing
line_as_array = [
1 .. String::from(parameters).len() - 1
} else {
// No parameters, we don't need to worry about it
line_as_array = [String::from(instruction.to_lowercase()), String::from("")];
// We're done with that line, ship it
let mut output_bytes: Vec<u8> = vec![]; // The output binary as an array of bytes
// Process each line and append its result to the binary data
for op in asm_array {
let mut op_bin = process_op(
&mut org,
&mut labels,
&mut label_requests,
output_bytes.len() as u16,
&mut did_error
output_bytes.append(&mut op_bin);
// Now we just need to insert the required addresses for the labels into the assembled binary
for request in label_requests {
if labels.contains_key(&request.1) {
if request.2 {
let mut address_to_make_relative: util::AssNumber = ("", 0, 0, false);
address_to_make_relative.0 = "address";
address_to_make_relative.1 = *labels.get(&request.1).unwrap() as u8;
address_to_make_relative.2 = (*labels.get(&request.1).unwrap() >> 8) as u8;
address_to_make_relative.3 = true;
output_bytes[request.0 as usize] = util::make_relative(address_to_make_relative, request.0, &mut org, &mut did_error).1;
} else {
output_bytes[request.0 as usize] = (*labels.get(&request.1).unwrap() + *org) as u8;
output_bytes[(request.0 + 1) as usize] = ((*labels.get(&request.1).unwrap() + *org) >> 8) as u8;
} else {
util::assembler_error("Invalid value or label", &request.1, &mut did_error);
if did_error {
eprintln!("Halted due to errors in input");
// If everything was okay, write to the file
if output_filename == "-" {
let write_result = io::stdout().write_all(&output_bytes);
if write_result.is_err() {
util::assembler_error("Failed to write to standard output", output_filename, &mut did_error);
} else {
let write_result = fs::write(output_filename, output_bytes);
if write_result.is_err() {
util::assembler_error("Failed to write to file", output_filename, &mut did_error);
println!("Assembled successfully");
// Function to process a single pre-formatted line, returns any binary data to be appended
fn process_op(
op: [String; 2],
org: &mut Box<u16>,
labels: &mut HashMap<String, u16>,
label_requests: &mut Vec<(u16, String, bool)>,
counter: u16,
did_error: &mut bool
) -> Vec<u8> {
let output: Vec<u8>;
let op_as_str_array = [&*op[0], &*op[1]];
// Process line depending on if it's a directive or an instruction
if &op_as_str_array[0][0 .. 1] == "." {
// For assembler directives
output = dot::process_op_dot(
} else {
// For ASM instructions
output = assembly::process_op_assembly(
return output;
fn handle_input_error(res: Result<String, io::Error>, input_filename: &str, did_error: &mut bool) -> String {
if res.is_ok() {
return res.unwrap();
} else {
util::assembler_error("Failed to read from file", input_filename, did_error);
return String::from("");