7
\$\begingroup\$

This is a simple machine language simulator that handles basic operations such as read, write, load, store, add, subtract, multiply, divide, modulus, branch, branch if negative, branch if zero.

Inputs are gotten from the user in hexadecimal, the memory is simulated as a built-in array of integers which can hold a maximum of 1 word.

A word consist of 4 digits, the first two represents the operand code (sml instruction code), the last two represents the operand (location in memory) The simulator also reads and output string literals.

Here is the code.

constants.h

constexpr unsigned read = 0xA; // Read a word(int) from the keyboard into a specific location in memory
constexpr unsigned write = 0xB; // Write a word(int) from a specific location in memory to the screen
constexpr unsigned read_str = 0xC; // Read a word(string) from the keyboard into a specific location in memory
constexpr unsigned write_str = 0xD; // Write a word(string) from a specific location in memory to the screen
constexpr unsigned load = 0x14; // Load a word from a specific location in memory to the accumulator
constexpr unsigned store = 0x15; // Store a word from the accumulator into a specific location in memory
constexpr unsigned add = 0x1E; /* Add a word from a specific location in memory to the word in the accumulator; store the 
                                 result in the accumulator */
constexpr unsigned subtract = 0x1F;
constexpr unsigned multiply = 0x20;
constexpr unsigned divide = 0x21;
constexpr unsigned modulo = 0x22;
constexpr unsigned branch = 0x28; // Branch to a specific location in the memory
constexpr unsigned branchneg = 0x29; // Branch if accumulator is negative
constexpr unsigned branchzero = 0x2A; // Branch if accumulator is zero
constexpr unsigned halt = 0x2B; // Halt the program when a task is completed
constexpr unsigned newline = 0x32; // Insert a new line
 
constexpr unsigned end = -0x1869F; // End the program execution
constexpr unsigned memory_size = 1000;
constexpr unsigned sml_debug = 0x2C; // SML debug

registers.h

int accumulator = 0; 
unsigned instruction_counter = 0;
unsigned instruction_register = 0;
unsigned operation_code = 0;
unsigned operand = 0;

sml.h

#include "constants.h"

void memory_dump( int memory[memory_size], const unsigned &mem_size, const int &acc, const unsigned &ins_reg, \
    const unsigned &ins_cnt, const unsigned &opr_code, const unsigned &opr ); 

void execute( int memory[memory_size], int &acc, unsigned &ins_reg, unsigned &ins_cnt, unsigned &opr_code, unsigned &opr ); // executes the statement in sequential manner

void evaluate( int memory[memory_size], int &acc, unsigned &ins_reg, unsigned &ins_cnt, unsigned &opr_code, unsigned &opr );

void display_welcome_message();

bool division_by_zero( int memory[ memory_size ], unsigned operand );

sml.cpp

#include <iostream>
#include <iomanip>
#include <string>
#include "sml.h"

int temp_cnt = 0;  // holds instruction_counter when performing branch operation
std::string temp_str; // holds the string before it is written into the memory
bool debug = false;

void memory_dump( int memory[memory_size], const unsigned &mem_size, const int &acc, const unsigned &ins_reg, \
    const unsigned &ins_cnt, const unsigned &opr_code, const unsigned &opr ) 
{
    std::cout << "\nREGISTERS:\n";

    std::cout << std::setw( 25 ) << std::left << std::setfill( ' ' ) << "accumulator" << std::showpos
        << std::setw( 5 ) << std::setfill( '0' ) << std::internal << acc << '\n';

    std::cout << std::setw( 28 ) << std::left << std::setfill( ' ' ) 
    << "instruction counter" << std::noshowpos <<  std::setfill( '0' ) 
    << std::right << std::setw( 2 ) << ins_cnt << '\n';

    std::cout << std::setw( 25 ) << std::left << std::setfill( ' ' ) 
    << "instruction register" << std::showpos << std::setw( 5 ) << std::setfill( '0' ) 
    << std::internal << ins_reg << '\n';

    std::cout << std::setw( 28 ) << std::left << std::setfill( ' ' ) 
    << "operation code" << std::noshowpos <<  std::setfill( '0' ) 
     << std::right << std::setw( 2 ) << opr_code << '\n';

    std::cout << std::setw( 28 ) << std::left << std::setfill( ' ' ) 
    << "operand" << std::noshowpos <<  std::setfill( '0' ) 
     << std::right << std::setw( 2 ) << opr << '\n';

    std::cout << "\n\nMEMORY:\n";
    std::cout << "  ";

    for( int i = 0; i != 10; ++i ) 
        std::cout << std::setw( 6 ) << std::setfill( ' ') << std::right << i;
    for( size_t i = 0; i != mem_size; ++i ) 
    {
        if( i % 10 == 0 )
            std::cout << "\n" << std::setw( 3 ) << std::setfill( ' ' ) << i << " ";
        std::cout << std::setw( 5 ) << std::setfill( '0' ) << std::showpos << std::internal << memory[ i ] << " ";
    }
    std::cout << std::endl;
}

void execute( int memory[memory_size], int &acc, unsigned &ins_reg, \
    unsigned &ins_cnt, unsigned &opr_code, unsigned &opr ) 
{
    int divisor; 
    while( memory[ ins_cnt ] != 0  ) 
    {
        ins_reg = memory[ ins_cnt++ ];

        if( ins_reg < 1000 ) divisor = 0x10; 
        else if( ins_reg >= 1000 && ins_reg < 10000 ) divisor =  0x100; 
        else if( ins_reg >= 10000 && ins_reg < 100000 ) divisor =  0x1000; 

        opr_code = ins_reg /  divisor;
        opr = ins_reg %  divisor ;

        if( opr_code == halt )
            break;
        evaluate( memory, acc, ins_reg, ins_cnt, opr_code, opr );
        if( debug )
            memory_dump( memory, memory_size, acc, ins_reg, ins_cnt, \
        opr_code, opr );
    }
}

void evaluate( int memory[memory_size], int &acc, unsigned &ins_reg, \
    unsigned &ins_cnt, unsigned &opr_code, unsigned &opr ) 
{
    switch ( opr_code ) 
    {
        case read:
            std::cin >> memory[ opr ];
            break;
        case read_str:
            std::cin >> temp_str;
            memory[ opr ] = temp_str.size();
            for( int i = 1; i != temp_str.size() + 1; ++i )
                memory[ opr + i ] = int( temp_str[ i - 1 ] );
            break;
        case write:
            std::cout << memory[ opr ] << " ";
            break;
        case write_str:
            for( int i = 0; i != memory[ opr ] + 1; ++i ) {
                std::cout << char( memory[ opr + i ]);
            }
            break;
        case load:
            acc = memory[ opr ];
            break;
        case store:
            memory[ opr ] = acc;
            break;
        case add:
            acc +=  memory[ opr ];
            break;
        case subtract:
            acc -= memory[ opr ];
            break;
        case multiply:
            acc *= memory[ opr ];
            break;
        case divide:
            if ( division_by_zero( memory, opr ) ) 
            {
                memory_dump( memory, memory_size, acc, ins_reg, ins_cnt, opr_code, opr );
                exit( EXIT_FAILURE );      
            }
            else 
            {
                acc /= memory[ opr ];
                break;
            }
        case modulo:
            if( division_by_zero( memory, opr ) )
            {
                memory_dump( memory, memory_size, acc, ins_reg, ins_cnt, opr_code, opr );
                exit( EXIT_FAILURE );  
            }
            else
            {
                acc %= memory[ opr ];
                break;
            }
        case branch:
            temp_cnt = ins_cnt;
            ins_cnt = opr;
            execute( memory, acc, ins_reg, ins_cnt, opr_code, opr );
            ins_cnt = temp_cnt;  
            break;
        case branchneg:
            if( acc < 0 ) 
            {
                temp_cnt = ins_cnt;
                ins_cnt = opr;
                execute( memory, acc, ins_reg, ins_cnt, opr_code, opr );
                ins_cnt = temp_cnt;  
            }
            break;
        case branchzero:
            if( acc == 0 ) 
            {
                temp_cnt = ins_cnt;
                ins_cnt = opr;
                execute( memory, acc, ins_reg, ins_cnt, opr_code, opr );
                ins_cnt = temp_cnt;  
            }
            break;
        case newline:
            std::cout << '\n' << std::flush;
            break;
        case sml_debug:
            if ( opr == 1 ) debug = true;
            else if ( opr == 0 ) debug = false;
            else 
            {
                std::cout <<  std::setw( 5 ) << std::setfill( ' ') << std::left << "***" 
                << "Invalid debug mode" 
                << std::setw( 5 ) << std::right << "***\n";
            }
            break;
        default:
            break;
    }
}

void display_welcome_message () {
    std::cout << "***" <<  " WELCOME TO SIMPLETRON! " << "***\n\n";
    std::cout <<  std::setw( 5 ) << std::left << "***" 
        << "Please enter your program one instruction" 
        << std::setw( 5 ) << std::right << "***\n";

    std::cout << std::setw( 5 ) << std::left  << "***" 
    << "(or data word) at a time. I will type the" 
    << std::setw( 5 ) << std::right << "***\n"; 

    std::cout << std::setw( 5 ) << std::left << "***" 
    << "location number and a question mark (?)."  
    << std::setw( 6 ) << std::right << "***\n";

    std::cout << std::setw( 5 )  << std::left << "***"
    << "You then type the word for that location"  
    << std::setw( 6 ) << std::right  << "***\n";

    std::cout << std::setw( 5 )  << std::left << "***" 
    << "Type the sentinel -0x1869F to stop entering"  
    << std::setw( 5 ) << std::right << "***\n";

    std::cout << std::setw( 5 )  << std::left << "***" 
    << "your program"  
    << std::setw( 5 ) << std::right << "***";

    std::cout << "\n\n" << std::flush;
}

bool division_by_zero( int memory[ memory_size ], unsigned operand )
{
    if ( memory[ operand ] == 0 ) 
        {
            std::cout <<  std::setw( 5 ) << std::left << "***" 
                << "Attempting division by zero" 
                << std::setw( 5 ) << std::right << "***\n";
            std::cout <<  std::setw( 5 ) << std::left << "***" 
                << "Program terminated abnormally" 
                << std::setw( 5 ) << std::right << "***\n";
            std::cout << "\n";    
            return true;
        }
   return false;
}

main.cpp

#include <iostream>
#include <iomanip>
#include "registers.h"
#include "sml.h"

int main() 
{
    int memory[ memory_size ]{};
    size_t memory_size = sizeof( memory )/ sizeof( memory[ 0 ] );
    int temp;

    display_welcome_message();

    while( instruction_counter != memory_size ) 
    {
        std::cout << std::setw( 2 ) << std::setfill( '0' )
            << instruction_counter << " ? ";
        std::cin >> std::hex >> temp;
        if( temp == end ) {
            break;
        }   
        if( temp >= -0xB3E8 && temp < 0xB3E8 )
            memory[ instruction_counter++ ] = temp;
        else 
            continue;
    }

    instruction_counter = 0;
    std::cout << std::setfill( ' ' );
    std::cout <<  std::setw( 5 ) << std::left << "***" 
        << "Program loaded into memory" 
        << std::setw( 5 ) << std::right << "***\n";
    
    std::cout <<  std::setw( 5 ) << std::left << "***" 
        << "Program execution starts..." 
        << std::setw( 5 ) << std::right << "***\n";

    execute( memory, accumulator, instruction_register, instruction_counter, operation_code, operand );
    std::cout << std::endl;
}
\$\endgroup\$

2 Answers 2

7
\$\begingroup\$

General Observations

This particular kind of problem is always interesting to solve.

Since your 4th question you seem to be avoiding classes. In C++ classes are your entryway to object oriented programming and class provide great tools. As @G.Sliepen stated in their review the simulator would be much better if it was a class. There wouldn't be any need for global variables if the simulator was implemented as a class. The public interfaces for execute(), evaluate(), and memory_dump() would be much simpler since the memory array and the registers would be private variables and there would be no need to pass them into the function.

To make the program more friendly add a line editor that allows the user to modify the simulator program. That way the program doesn't need to exit if the simulator dumps memory. The execution of the simulator can stop, the user can edit the line and then start the simulation again. Use exceptions rather than exit(EXIT_FAILURE); to return the program to a known state.

You might want to look at the answers on this question for more information.

Avoid Global Variables

Currently there are at least 8 global variables in the program, in registers.h:

int accumulator = 0;
unsigned instruction_counter = 0;
unsigned instruction_register = 0;
unsigned operation_code = 0;
unsigned operand = 0;

in sml.cpp:

int temp_cnt = 0;  // holds instruction_counter when performing branch operation
std::string temp_str; // holds the string before it is written into the memory
bool debug = false;

It is very difficult to read, write, debug and maintain programs that use global variables. Global variables can be modified by any function within the program and therefore require each function to be examined before making changes in the code. In C and C++ global variables impact the namespace and they can cause linking errors if they are defined in multiple files. The answers in this stackoverflow question provide a fuller explanation.

Most or all of these global variables could be private variables if the simulator was implemented as a class.

The registers could be implemented as an array indexed by an enun.

typedef enum
{
    ACCUMULATOR = 0,
    INSTRUCTION_COUNTER = 1,
    INSTRUCTION_REGISTER = 2,
    OPERATION_CODE = 3,
    OPERAND = 4,
    REGISTER_COUNT = 5
} REGISTERS;

    unsigned registers[static_cast<unsigned>(REGISTER_COUNT)];
    registers[ACCUMULATOR] = 0;

If the code in sml.cpp isn't converted to a class, then it would be better to make each of those variable static so that their scope is only that of the sml.cpp file itself, right now they could be accessed in other .cpp files such as main.cpp.

The registers global variables should be declared in sml.cpp since they aren't necessary to other parts of the program such as main.cpp.

Include Guards

In C++ as well as the C programming language the code import mechanism #include FILE actually copies the code into a temporary file generated by the compiler. Unlike some other modern languages C++ (and C) will include a file multiple times. To prevent this programmers use include guards which can have 2 forms:

the more portable form is to embed the code in a pair of pre-processor statements

#ifndef SYMBOL
#define SYMBOL
// All other necessary code
#endif // SYMBOL

A popular form that is supported by most but not all C++ compilers is to put #pragma once at the top of the header file.  

Using one of the 2 methods above to prevent the contents of a file from being included multiple times is a best practice for C++ programming. This can improve compile times if the file is included multiple times, it can also prevent compiler errors and linker errors.

Complexity

The function evaluate() is too complex (does too much) and the performance can be improved. If the opcode values defined constants.h were in order and starting at zero an array of functions could be used to implement each of the opcodes. Then each opcode can be evaluated by simplify indexing into that array by opcode. This would greatly reduce the amount of code in the function. It will perform faster because indexing into an array is faster than going through multiple if statements in the assembly code generated. This also makes it easier to expand the instruction set.

\$\endgroup\$
6
  • \$\begingroup\$ @pacmanibw I have never seen typedef enum { ... } in C++, I have read that it makes somewhat a difference in C, what is the use here? \$\endgroup\$
    – user228914
    Nov 2, 2020 at 4:13
  • \$\begingroup\$ Great. Inasmuch as adding a vim-like interface would improve usability, I just want to keep it very simple now. \$\endgroup\$ Nov 2, 2020 at 4:45
  • \$\begingroup\$ @pacmanibw. Correct me if am wrong, I think classes are meant to be used in scenarios where multiple instances of a class are needed. In this case, only one instance of a class would ever be needed at a time. I know classes would improves the structure and readability, what are the other gains? Can it be done clearly using a different approach aside classes? \$\endgroup\$ Nov 2, 2020 at 4:49
  • 1
    \$\begingroup\$ @theProgrammer That's one of their uses, not the only use for classes. Once you start using classes and applying single-responsibility (1 function has 1 task and 1 task only), you'll see your code cleans up quite nicely. Your registers.h will become obsolete. \$\endgroup\$
    – Mast
    Nov 2, 2020 at 7:04
  • \$\begingroup\$ @theProgrammer One of the more important usages of Object Oriented Program is reuse. Well written classes following the SOLID programming principles can be used in other programs just by including the class files in a new program. That reduces development time and costs. Almost all programming in C++ is object oriented for this reason. The single-responsibility principle mentioned by Mast is the S in SOLID. \$\endgroup\$
    – pacmaninbw
    Nov 2, 2020 at 14:45
5
\$\begingroup\$

Digits and word sizes

I see code like this in your code:

constexpr unsigned read = 0xA;
int accumulator = 0;

This means you are tying word sizes to whatever the size of an int is on the machine you are compiling your code on. I would create new type aliases that explicitly defines the size of a signed and unsigned word in your simulated machine:

using sword = int32_t;
using uword = uint32_t;

Then there are the digits. You say that the input from the user is in hexadecimal, that words are 4 digits, but from the code it looks like those are decimal digits? That's not very consistent. Most computers would work with powers of two, and that also makes simulation much faster (division and modulo operations are quite expensive).

Create a class to hold the state of the machine

Instead of having lots of out-of-class functions to which you have to pass a lot of variables every time, it makes more sense to create a class that represents the simulated machine, and which contains member variables for the registers and the memory, like so:

class Machine {
    int accumulator = 0; 
    unsigned instruction_counter = 0;
    ...
    std::vector<int> memory(memory_size);

    void memory_dump();
    void evaluate();

public:
    void load_program();
    void execute();
};

You can also move all the constants inside class Machine, so they no longer pollute the global namespace, especially when you have names like read and write that shadow POSIX functions.

I would move everything from sml.cpp into class Machine, except display_welcome_message(), which should probably just be in main.cpp, as it does not relate to the functioning of the machine.

Avoid magic constants

You have proper names for all the constants, except -0xB3E8 and 0xB3E8. What's up with those? Give those a name as well.

Consider using a formatting library

Creating nicely formatted output using iostream functionality in C++ is very annoying. It requires a lot of code, mistakes are easily made, and the source code looks terrible. If you can use C++20 already, I strongly suggest you start using std::format(), but if you cannot, consider using fmtlib, which is the library that std::format() is based on, and will work with earlier versions of C++. This means you can rewrite your code like so:

std::cout << std::format("{:02} ? ", instruction_counter);
...
std::cout << std::format("{:*^40}\n", " Program loaded into memory ");
...
std::cout << std::format("{:*^40}\n", " Program execution starts... ");
\$\endgroup\$
4
  • \$\begingroup\$ Can you please elaborate on Most computers would work with powers of two, and that also makes simulation much faster (division and modulo operations are quite expensive). \$\endgroup\$ Nov 1, 2020 at 20:52
  • \$\begingroup\$ @theProgrammer See this question for an overview of the speed of native CPU instructions. In general, integer division takes tens of clock cycles, whereas a simple bitwise AND or shift instruction takes less than a cycle on average. So if you encode your instructions such that the opcode is always in, for example, the high 16 bits, you have the lower 16 bits for the instruction operand. So then opr_code = ins_reg >> 16; opr = ins_reg & 0xFFFF. \$\endgroup\$
    – G. Sliepen
    Nov 1, 2020 at 21:17
  • 1
    \$\begingroup\$ Nitpick: read and write are from POSIX, not from the C Standard Library. \$\endgroup\$ Nov 1, 2020 at 23:23
  • \$\begingroup\$ Does any compiler implementation support std::format already? Last time I checked, it was in the standard but nowhere implemented. \$\endgroup\$ Nov 2, 2020 at 16:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.