enthusiasm

This document provides a comprehensive guide on how to execute assembly code using the EnthusiASM framework. Follow the steps below to get started.

Once the architecture is implemented, you can use the core library to assemble and execute assembly code. The steps to follow are:

  1. Parse the assembly code using the architecture-specific parser.
  2. Assemble the parsed instructions into machine code using the architecture-specific assembler.
  3. Set up the cpu and memory for the architecture.
  4. Load the assembled machine code into memory using the load_program function.
  5. Execute the program using the cpu’s step function.

Parsing Assembly Code

I’m using the code from the example described in the Implementing a New Architecture document.

Imagine that we want to run this code:

addi r0, r0, 5
addi r1, r0, 10
add  r0, r0, r1

First of all, we need to run the parser to convert the assembly code into an intermediate representation.

    const PROGRAM: &str = r#"
addi r0, r0, 5
addi r1, r0, 10
add  r0, r0, r1
"#;

fn main() {
    let parser = SimpleParser;
    let mut lines = vec![];
    for (lineno, line) in PROGRAM.lines().enumerate() {
        let parsed = parser.parse(line, lineno).expect("program parse error");
        for instruction in parsed.tokens {
            lines.push(instruction);
        }
    }
}

Assembling Instructions

Then we need to assemble the parsed instructions into machine code.

fn main() {
    // ... previous code ...

    let program = SimpleAssembler.assemble(lines).expect("failed to assemble");
}

Setting Up CPU and Memory

Next, we set up the CPU and memory for our architecture.

In our case, the default constructor is sufficient to also set up the memory.

fn main() {
    // ... previous code ...

    let mut cpu = SimpleCPU::default();
}

Loading the Program into Memory

enthusiasm-core comes with a very simple function to load a program into memory, called load_program.

/// Loads a [`Program`](crate::prelude::Program) into the [`Memory`](crate::prelude::Memory)
/// of the given [`CPU`](crate::prelude::Cpu) starting at the specified base address.
///
/// It also sets the program counter to the program's entry point.
pub fn load_program<C, M, A, R>(cpu: &mut C, base: A, program: &Program<A>) -> EnthusiasmResult<()>
where
    C: Cpu<Memory=M, Registers=R>,
    M: Memory<Address=A>,
    A: Into<u64> + TryFrom<u64> + Copy + std::fmt::Debug + std::fmt::Display + Eq + PartialEq,
    R: Registers<Address=A>,
{
    let memory = cpu.memory_mut();
    for (i, byte) in program.bytes.iter().enumerate() {
        let addr =
            A::try_from(base.into() + i as u64).map_err(|_| EnthusiasmError::AddressOverflow)?;
        memory.write8(addr, *byte)?;
    }

    // set program counter to entry point
    let entry_point = A::try_from(base.into() + program.entry_point.into())
        .map_err(|_| EnthusiasmError::AddressOverflow)?;
    cpu.registers_mut().set_program_counter(entry_point);

    Ok(())
}

What it does is to take the assembled program and a base address, and it writes the program bytes into memory starting from that address. It also sets the CPU’s program counter to the entry point of the program + base address.

Once loaded, the CPU is ready to execute the program.

fn main() {
    // ... previous code ...

    load_program(&mut cpu, 0u64, &program).expect("failed to load program");
}

Executing the Program

Finally, we can execute the program by stepping through the instructions.

fn main() {
    // ... previous code ...

    for _ in 0..3 {
        cpu.step().expect("failed to execute instruction");
    }

    println!("Register r0: {}", cpu.registers().get(0));
    println!("Register r1: {}", cpu.registers().get(1));
}

In this case we are executing three instructions, that’s why we loop three times calling cpu.step().

In real scenarios, you would typically have a more complex loop that continues executing until a certain condition is met, such as reaching a specific instruction, an error, or a halt condition.

In particular, the general condition should be to run until EnthusiasmError::Cpu(CpuError::Halted)) is returned from the step function.

Reading Register Values

Whenever you need to read the value of a register, you can use the registers() method of the CPU to get a reference to the registers, and then use the read method to read the value of a specific register

Reading Memory Values

Similarly, to read a value from memory, you can use the memory() method of the CPU to get a reference to the memory, and then use the appropriate read method (e.g., read8, read16, read32, etc.) to read the value at a specific address.

Conclusion

By following the steps outlined in this guide, you can successfully execute assembly code using the EnthusiASM framework.