gpcas_forwardcom 0.1.1

ForwardCom instruction set architecture (ISA) properties for use with the General Purpose Core Architecture Simulator (GPCAS).
Documentation
// Filename: emulator.rs
// Author:	 Kai Rese
// Version:	 0.22
// Date:	 14-09-2022 (DD-MM-YYYY)
// Library:  gpcas_forwardcom
//
// Copyright (c) 2022 Kai Rese
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser 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
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program. If not, see
// <https://www.gnu.org/licenses/>.

//! This module contains the actual emulator.
//!
//! Many components in this module are public so components in submodules can use them in a public
//! interface.

pub mod constants;
mod core;
pub mod instructions;

use self::core::OperandType;
use constants::{default_values, operand_indices, register_indices};
use instructions::{SimpleInstructionFunction, VectorMode, NO_MEMORY};

use gpcas_isa::{format_c_string, instruction_flags, Emulator, Instruction, OperandStorage};
use std::sync::atomic::{AtomicUsize, Ordering};

/// Emulates the given ForwardCom program and feeds data to the simulator.
pub struct ForwardComEmulator {
    /// The current instruction pointer value.
    ip: usize,
    /// The instruction pointer value upon program start.
    start_address: usize,
    /// The count of emulation calls.
    instruction_count: AtomicUsize,
    /// The count of emulated non-speculative instructions.
    retired_instruction_count: AtomicUsize,
    /// The memory data of the program image.
    memory: Vec<u8>,
    /// The list of return addresses.
    ///
    /// As ForwardCom uses its own stack for this, it has its own data field in this struct.
    call_stack: Vec<usize>,
    /// Holds information needed for generating sub-instructions of an multi-instruction.
    decoder: core::Decoder,
    /// A container for the operands of an instruction.
    ///
    /// Is defined inside the emulator as opposed to inside the instruction because it enables
    /// instructions to be newly generated, while avoiding the heap allocation from the operand
    /// storage.
    operands: OperandStorage,
    /// Stores the current register values.
    registers: RegisterFile,
    /// Buffers the output of STDOUT for the emulation.
    output_print_buffer: String,
}

/// Atmomic instruction of the simulator.
///
/// Contains the information that get sent to the simulator, as well as additional fields needed
/// for executing the instruction. Does not include actual operand values, these can be found in the
/// [`ForwardComEmulator`].
pub struct EmulatorInstruction {
    /// The part that gets sent to the simulator.
    pub core: Instruction,
    /// The execution function that gets called by the emulator.
    pub function: SimpleInstructionFunction,
    /// How the instruction function operates on vectors.
    pub vector_mode: VectorMode,
    /// The type of the operand data.
    pub operand_type: OperandType,
    /// The total operand size of the instruction in bytes.
    pub operand_size: usize,
    /// Additional custom option bitfield for the execution function.
    pub option_bits: u8,
    /// As the simulator currently doesn't support addressing special registers, this signals the
    /// output register being a special register.
    pub special_output: bool,
    /// If the instruction can be committed.
    pub valid: bool,
}

/// Holds all register values.
pub struct RegisterFile {
    /// The general purpose registers of the ForwardCom specification.
    pub general_purpose: [u64; 32],
    /// The data of the vector registers, collapsed into one block of continuous block of memory.
    pub vector: Vec<u8>,
    /// The used length of each vector register.
    pub vector_lengths: [usize; 32],
    /// Special registers as defined in the ForwardCom specification.
    pub special: [u64; 32],
    /// The maximum length of a vector in bytes.
    max_vector_size: usize,
}

/// Function ID for a system call to exit the program.
const SYS_EXIT: u32 = 0x10;
/// Function ID for a system call to print something to stdout.
const SYS_PRINT: u32 = 0x103;

impl ForwardComEmulator {
    /// Constructs a new emulator instance.
    ///
    /// For ease of use, this also takes care of loading the program.
    /// `max_vector_size` is the maximum **bit** size that fits into a vector.
    pub fn new(executable_data: Vec<u8>, max_vector_size: usize) -> ForwardComEmulator {
        debug_assert!(max_vector_size >= 128);
        debug_assert!(max_vector_size % 8 == 0);

        let program = crate::program::load_forwardcom_elf(&executable_data).unwrap();
        // bit -> byte conversion
        let mut registers = RegisterFile::new(max_vector_size / 8);
        registers.general_purpose[register_indices::STACK_POINTER] = program.stack_address as u64;
        registers.special[register_indices::NUMCONTR] = default_values::NUMCONTR;
        registers.special[register_indices::THREADP] = program.threadp;
        registers.special[register_indices::DATAP] = program.datap;

        // TODO initialize all special registers

        ForwardComEmulator {
            ip: program.start_address,
            start_address: program.start_address,
            instruction_count: AtomicUsize::new(0),
            retired_instruction_count: AtomicUsize::new(0),
            memory: program.data,
            call_stack: Vec::new(),
            registers,
            decoder: core::Decoder::default(),
            output_print_buffer: String::new(),
            // bit -> byte conversion
            operands: OperandStorage::new(max_vector_size / 8, 32),
        }
    }

    /// Execute a system call.
    ///
    /// If available, input 1 is the address of a shared memory block and input 2 the size of said
    /// block. If both are the stack pointer, the whole application memory is shared.
    /// The last input contains the module ID at an offset of the element size and the function ID
    /// at an offset of zero.
    pub fn handle_sys_call<const MEMORY: usize>(
        &mut self,
        instruction: &mut EmulatorInstruction,
        _offset: usize,
    ) {
        let id_offset = if MEMORY == NO_MEMORY { 0 } else { 2 };
        let function_id = if let OperandType::I64 = instruction.operand_type {
            self.operands.get_u32(operand_indices::INPUT1 + id_offset)
        } else {
            self.operands.get_u16(operand_indices::INPUT1 + id_offset) as u32
        };

        match function_id {
            SYS_EXIT => {
                log::info!(
                    "Program exit with code {}",
                    self.registers.general_purpose[0]
                );
                instruction.core.flags |= instruction_flags::TERMINATE;
            }
            SYS_PRINT => {
                let string_start = self.registers.general_purpose[0] as usize;
                if let Some(index) = self.memory[string_start..]
                    .iter()
                    .position(|&number| number == 0)
                {
                    self.output_print_buffer.push_str(
                        format_c_string(
                            &String::from_utf8_lossy(
                                self.memory[string_start..string_start + index as usize]
                                    .to_vec()
                                    .as_slice(),
                            ),
                            &self.memory[self.registers.general_purpose[1] as usize..],
                        )
                        .unwrap_or_else(|e| format!("Error printing: {}", e))
                        .as_str(),
                    );
                } else {
                    log::error!(
                        "The string to print is not zero-terminated, terminating emulation."
                    );
                    instruction.valid = false;
                }
            }
            id => {
                log::error!("Unrecognized system call (ID {:#X}), terminating.", id);
                instruction.valid = false;
            }
        }
    }
}

impl Emulator for ForwardComEmulator {
    #[inline]
    fn emulate_instruction(&mut self, address: usize, commit: bool) -> Option<Instruction> {
        debug_assert!(
            !commit | (address == self.ip),
            "Control flow failure, expected {:#X}, got {:#X}",
            self.ip,
            address
        );
        log::debug!(
            "Processing ip {:#X} ({:#X})",
            (address >> 2).wrapping_sub(0x56),
            address
        );
        let (instruction, has_follow_up) =
            self.decoder
                .get_next_instruction(address, &self.memory, &self.registers);
        // Some instructions need the ip as register
        self.registers.special[register_indices::IP] = address as u64 + instruction.size as u64;

        let mut instruction = core::prepare(self, instruction, has_follow_up);

        if commit {
            core::execute(&mut instruction, self);
            core::write_results(
                &mut self.memory,
                &mut self.registers,
                &self.operands,
                &instruction,
            );
            self.ip = core::set_next_ip(&mut self.call_stack, &mut instruction);
            self.retired_instruction_count
                .fetch_add(1, Ordering::Relaxed);
        }

        log::trace!("");
        self.instruction_count.fetch_add(1, Ordering::Relaxed);
        if (instruction.core.flags & instruction_flags::TERMINATE) > 0 {
            None
        } else {
            Some(instruction.core)
        }
    }

    #[inline]
    fn get_instruction_count(&self) -> usize {
        self.instruction_count.load(Ordering::Relaxed)
    }

    #[inline]
    fn get_current_ip(&self) -> usize {
        self.ip
    }

    #[inline]
    fn get_printed_output(&self) -> &str {
        self.output_print_buffer.as_str()
    }

    #[inline]
    fn get_start_address(&self) -> usize {
        self.start_address
    }
}

// TODO support for all special function registers
impl RegisterFile {
    /// Creates a new register file. All registers are initialized to zero.
    ///
    /// `max_vector_size` is the maximum **byte** size that fits into a vector.
    pub fn new(max_vector_size: usize) -> Self {
        RegisterFile {
            general_purpose: [0; 32],
            vector: vec![0; 32 * max_vector_size],
            vector_lengths: [0; 32],
            special: [0; 32],
            max_vector_size,
        }
    }

    /// Returns the vector register of the specified number.
    ///
    /// The slice has the length of the actual vector.
    #[inline]
    pub fn get_vector(&self, index: usize) -> &[u8] {
        let base = index * self.max_vector_size;
        &self.vector.as_slice()[base..base + self.vector_lengths[index]]
    }

    /// Sets the vector register of the specified number to the provided value.
    ///
    /// The length is taken from the length of the slice.
    #[inline]
    pub fn set_vector(&mut self, index: usize, value: &[u8]) {
        let base = index * self.max_vector_size;
        self.vector.as_mut_slice()[base..base + value.len()].copy_from_slice(value);
        self.vector_lengths[index] = value.len();
    }
}

#[cfg(test)]
mod tests {
    use super::ForwardComEmulator;
    use super::RegisterFile;
    use crate::emulator::constants::{default_values, register_indices};

    use gpcas_isa::{Emulator, OperandStorage};
    use std::convert::TryInto;

    #[test]
    fn push_pop() {
        let mut memory = vec![0u8; 96];
        // push registers 16-23
        memory[0..4].copy_from_slice(&u32::to_le_bytes(0x471f_f017_u32)[..]);
        // pop registers 16-23
        memory[4..8].copy_from_slice(&u32::to_le_bytes(0x473f_f017_u32)[..]);

        let mut emulator = get_emulator(memory);
        for i in 0..8 {
            emulator.emulate_instruction(0x0, true);
            emulator.registers.general_purpose[i + 16] = 0;
            assert_eq!(
                u64::from_le_bytes(
                    emulator.memory
                        [emulator.memory.len() - 8 * (i + 1)..emulator.memory.len() - 8 * i]
                        .try_into()
                        .unwrap()
                ),
                i as u64 + 17,
                "Register {} at address {}",
                i + 16,
                emulator.memory.len() - 8 * (i + 1),
            );
        }
        emulator.emulate_instruction(0x0, true);
        for i in 0..8 {
            emulator.emulate_instruction(0x4, true);
            assert_eq!(
                emulator.registers.general_purpose[24 - i - 1],
                24 - i as u64,
                "Register {} had the wrong value!",
                24 - i - 1,
            );
        }
        emulator.emulate_instruction(0x4, true);
    }

    fn get_emulator(data: Vec<u8>) -> ForwardComEmulator {
        let mut registers = RegisterFile::new(2);

        for i in 0..31 {
            registers.general_purpose[i] = i as u64 + 1;
        }
        registers.general_purpose[register_indices::STACK_POINTER] = data.len() as u64;
        registers.special[register_indices::NUMCONTR] = default_values::NUMCONTR;

        ForwardComEmulator {
            ip: 0,
            start_address: 0,
            instruction_count: Default::default(),
            retired_instruction_count: Default::default(),
            memory: data,
            call_stack: Vec::new(),
            decoder: Default::default(),
            operands: OperandStorage::new(16, 32),
            registers,
            output_print_buffer: String::new(),
        }
    }
}