11
\$\begingroup\$

In constructing an answer to this question, I wrote a small debugger/simulator for the MAC-1 instruction set.

There are a number of simulators already out there, but most are either too old to be compiled and run using a modern version of Java or C or C++ or they intended to exercise the machine down to the internal register level which was not of interest for this particular question.

My goals in creating this (other than just to have some fun) were to allow for the simple extension or modification of the instruction set and to have a functional if very simple debugging environment. Those old enough to remember the ddt debugger from CP/M back in the 1970s will notice some similarities.

A non-goal was run-time performance, so in particular, the search mechanism for instructions is a simple linear search. With only a handful of instructions, there seemed little point in optimizing this.

mac1.cpp

#include <iostream>
#include <iomanip>
#include <cstdint>
#include <algorithm>
#include <sstream>
#include <initializer_list>
#include <stdexcept>

constexpr unsigned MEMSIZE = 4096;

uint16_t extract(std::string &str) {
    uint16_t word = 0;
    try {
        word = std::stoul(str, 0, 16);
    } 
    catch (std::invalid_argument) {
    }
    return word;
}

static std::string hex4(uint16_t x) {
    std::stringstream s;
    s << std::setfill('0') << std::setw(4) << std::hex << x;
    return s.str();
}


struct Regs {
    Regs() :
        halt{true},
        ac{0},
        pc{0},
        sp{0},
        mem{0}
    {}
    uint16_t &m(uint16_t offset) {
        offset &= (MEMSIZE -1);
        return mem[offset];
    }
    const uint16_t &m(uint16_t offset) const {
        offset &= (MEMSIZE -1);
        return mem[offset];
    }
    bool halt;     // currently halted if true
    uint16_t ac;   // accumulator 
    uint16_t pc;   // program counter
    uint16_t sp;   // stack pointer
    uint16_t mem[MEMSIZE];  // memory
};

class Instruction {
    public:
    Instruction(std::string name, std::string bits, std::string desc, void (*action)(Regs &r, uint16_t &x))
    :   mnemonic_{name},
        description_{desc},
        action_{action},
        argmask_{0}
    {
        bitsToMask(bits);
    }
    friend std::ostream &operator<<(std::ostream &out, const Instruction& inst) {
        return out 
            << inst.mnemonic_ << '\t' 
            << inst.maskToBits() << '\t'
            << inst.description_ << '\n';
    }
    bool match(uint16_t word) const {
        return (word & mask_) == pattern_;
    }
    bool name(std::string &name) const {
        return name == mnemonic_;
    }
    bool exec(Regs &r, uint16_t &x) const {
        action_(r, x);
        return true;
    }
    bool needsArg() const {
        return argmask_;
    }
    void list(uint16_t &w) const {
        std::cout << mnemonic_;
        if (argmask_) {
            std::cout << " 0x" << hex4(argmask_ & w);
        }
        std::cout << '\n';
    }
    uint16_t assemble(uint16_t &w) const {
        return (w & ~mask_) | pattern_;
    }
    private:
        void bitsToMask(const std::string &bits) {
            uint16_t maskval = 1u << 15;
            for (int ch : bits) {
                switch(ch) {
                    case '1': 
                        pattern_ |= maskval;
                        // drop through to '0' case
                    case '0': 
                        mask_ |= maskval;
                        maskval >>= 1;
                        break;
                    case 'x':
                        maskval >>= 1;
                        argmask_ = 0xfff;
                        break;
                    case 'y':
                        argmask_ = 0xff;
                        maskval >>= 1;
                        break;
                    case ' ':
                        break;
                    default:
                        std::cout << "Can't understand '" << ch << "' in mask for " << mnemonic_ << "\n";
                }
            }
        }
        std::string maskToBits() const {
            std::stringstream s;
            for (uint16_t maskval = (1u << 15); maskval; maskval >>= 1) {
                if (mask_ & maskval) {
                    s << ((mask_ & pattern_) ? '1' : '0');
                } else {
                    s << 'x';
                }
            }
            s << '\n';
            return s.str();
        }

        std::string mnemonic_;
        std::string description_;
        void (*action_)(Regs &r, uint16_t &x);
        uint16_t argmask_;
        uint16_t mask_;
        uint16_t pattern_;
};

static const Instruction instructions[]{
    Instruction("LODD","0000 xxxx xxxx xxxx","load direct",     [](Regs &r, uint16_t &x){r.ac = r.m(x);}),
    Instruction("STOD","0001 xxxx xxxx xxxx","store direct",    [](Regs &r, uint16_t &x){r.m(x) = r.ac;}),
    Instruction("ADDD","0010 xxxx xxxx xxxx","add direct",      [](Regs &r, uint16_t &x){r.ac = r.ac + r.m(x);}),
    Instruction("SUBD","0011 xxxx xxxx xxxx","subtract direct", [](Regs &r, uint16_t &x){r.ac = r.ac - r.m(x);}),
    Instruction("JPOS","0100 xxxx xxxx xxxx","jump positive",   [](Regs &r, uint16_t &x){if (static_cast<int16_t>(r.ac) >= 0) r.pc = x;}),
    Instruction("JZER","0101 xxxx xxxx xxxx","jump zero",       [](Regs &r, uint16_t &x){if (r.ac == 0) r.pc = x;}),
    Instruction("JUMP","0110 xxxx xxxx xxxx","jump always",     [](Regs &r, uint16_t &x){r.pc = x;}),
    Instruction("LOCO","0111 xxxx xxxx xxxx","load constant",   [](Regs &r, uint16_t &x){r.ac = x;}),
    Instruction("LODL","1000 xxxx xxxx xxxx","load local",      [](Regs &r, uint16_t &x){r.ac = r.m(r.sp + x);}),
    Instruction("STOL","1001 xxxx xxxx xxxx","store local",     [](Regs &r, uint16_t &x){r.m(x + r.sp) = r.ac;}),
    Instruction("ADDL","1010 xxxx xxxx xxxx","add local",       [](Regs &r, uint16_t &x){r.ac = r.ac + r.m(r.sp + x);}),
    Instruction("SUBL","1011 xxxx xxxx xxxx","subtract local",  [](Regs &r, uint16_t &x){r.ac = r.ac - r.m(r.sp + x);}),
    Instruction("JNEG","1100 xxxx xxxx xxxx","jump negative",   [](Regs &r, uint16_t &x){if (static_cast<int16_t>(r.ac) < 0) r.pc = x;}),
    Instruction("JNZE","1101 xxxx xxxx xxxx","jump nonzero",    [](Regs &r, uint16_t &x){if (r.ac != 0) r.pc = x;}),
    Instruction("CALL","1110 xxxx xxxx xxxx","call a procedure",[](Regs &r, uint16_t &x){r.sp = r.sp - 1; r.m(r.sp) = r.pc; r.pc = x;}),
    Instruction("PSHI","1111 0000 0000 0000","push indirect",   [](Regs &r, uint16_t &){r.sp = r.sp - 1; r.m(r.sp) = r.m(r.ac);}),
    Instruction("POPI","1111 0010 0000 0000","pop indirect",    [](Regs &r, uint16_t &){r.m(r.ac) = r.m(r.sp++);}),
    Instruction("PUSH","1111 0100 0000 0000","push onto stack", [](Regs &r, uint16_t &){r.m(--r.sp) = r.ac;}),
    Instruction("POP ","1111 0110 0000 0000","pop from stack",  [](Regs &r, uint16_t &){r.ac = r.m(r.sp); r.sp++;}),
    Instruction("RETN","1111 1000 0000 0000","return from a procedure",[](Regs &r, uint16_t &){r.pc = r.m(r.sp++);}),
    Instruction("SWAP","1111 1010 0000 0000","swap ac and sp",  [](Regs &r, uint16_t &){std::swap(r.ac, r.sp);}),
    Instruction("INSP","1111 1100 yyyy yyyy","increment sp",    [](Regs &r, uint16_t &x){r.sp += (x & 0xff);}),
    Instruction("DESP","1111 1110 yyyy yyyy","decrement sp",    [](Regs &r, uint16_t &x){r.sp -= (x & 0xff);}),
    Instruction("HALT","1111 1111 yyyy yyyy","halt",            [](Regs &r, uint16_t &){r.halt = true;}),
};

class Mac1
{
public:
    Mac1() :
        verbose{true},
        regs()
    {}
    friend std::ostream& operator<<(std::ostream &out, const Mac1& mic) {
        return out 
            << "PC = 0x" << hex4(mic.regs.pc) 
            << "  SP = 0x" << hex4(mic.regs.sp) 
            << "  AC = 0x" << hex4(mic.regs.ac) 
            << '\n';
    }
    void setPC(uint16_t word) {
        regs.pc = word;
    }
    void setAC(uint16_t word) {
        regs.ac = word;
    }
    void setSP(uint16_t word) {
        regs.sp = word;
    }
    void dumpmem(uint16_t loc=0, size_t sz=MEMSIZE, std::ostream& out = std::cout) const {
        if ((loc > MEMSIZE) || (loc+sz > MEMSIZE))
            return;
        for (size_t i = loc; i < sz+loc; ++i) {
            if (i%8 == 0) {
                out << "\n" << hex4(i) << ": ";
            }
            out << hex4(regs.m(i)) << ' ';
        }
        out << '\n';
    }
    const Instruction *match(uint16_t word) const {
        for (const auto &inst : instructions) {
            if (inst.match(word)) {
                return &inst;
            }
        }
        return nullptr;
    }
    void step() {
        uint16_t word = regs.m(regs.pc++);
        uint16_t x = word & 0xfff;
        const Instruction *inst = match(word);
        if (inst != nullptr) {
            inst->exec(regs, x);
            if (verbose) {
                inst->list(word);
            }
            return;
        }
        // no instruction found, so halt
        regs.halt = true;
    }
    void run() {
        bool oldverbose = verbose;
        verbose = false;
        for (regs.halt = false; !regs.halt; step())
        {}
        verbose = oldverbose;
    }
    void list(uint16_t ptr, unsigned n) {
        for ( ; n; --n, ++ptr) {
            std::cout << hex4(ptr) << ": ";
            uint16_t word = regs.m(ptr);
            const Instruction *inst = match(word);
            if (inst != nullptr) {
                inst->list(word);
            } else {
                std::cout << "??? 0x" << hex4(word);
            }
        }
    }
    void modifymem(uint16_t ptr) {
        std::cout << "Modifying memory starting at address 0x" << hex4(ptr) << ". Enter `q` to quit\n";
        std::string cmd;
        while (std::cin >> cmd && cmd[0] != 'q') {
            regs.m(ptr++) = extract(cmd);
        }
    }
    void assemble(uint16_t ptr) {
        std::string cmd; 
        std::string arg; 
        std::cout << "Assembling starting at address 0x" << hex4(ptr) << ". Enter `q` to quit\n";
        uint16_t word;
        while (std::cin >> cmd && cmd.length() > 1) {
            if (cmd[0] == 'q') { // quit
                return;
            }
            for (const auto &inst : instructions) {
                if (inst.name(cmd)) {
                    if (inst.needsArg()) {
                        std::cin >> arg;
                        word = extract(arg);
                    } else {
                        word = 0u;
                    }
                    regs.m(ptr) = inst.assemble(word);
                    std::cout << hex4(ptr) << ": ";
                    inst.list(word);
                    ++ptr;
                }
            }
        }
    }
    void load(uint16_t loc, size_t sz, uint16_t *data) {
        if ((loc > MEMSIZE) || (data == nullptr) || (loc+sz > MEMSIZE))
            return;
        std::copy(data, &data[sz], &regs.m(loc));
    }

private:
    bool verbose;
    Regs regs;
};

void help() {
    std::cout << "Commands: [h]elp, [?], [a]ssemble, [d]ump, [g]o, [l]ist, [m]emory, [r]egisters, [s]tep, [q]uit\n"
        "rpc XXXX, rsp XXXX, rac XXXX, dXXXX, lXXXX\n";
}


int main()
{
    Mac1 mac1;
    uint16_t word;

    help();
    std::cout << "> ";
    std::string command;
    while (std::cin >> command) {
        switch(std::tolower(command[0])) {
            case 'h': 
            case '?': 
                help();
                break;
            case 'a':  // assemble
                command = command.substr(1);
                word = extract(command);
                mac1.assemble(word);
                break;
            case 'g':  // go
                mac1.run();
                break;
            case 'd':  // dump memory
                command = command.substr(1);
                word = extract(command);
                mac1.dumpmem(word, 0x20, std::cout);
                break;
            case 'l': // list
                command = command.substr(1);
                word = extract(command);
                mac1.list(word, 16);
                break;
            case 'm':  // modify memory
                command = command.substr(1);
                word = extract(command);
                mac1.modifymem(word);
                break;
            case 's':
                mac1.step();
                // fall through to 'r'
            case 'r':
                if (command == "rpc") {
                    std::string arg;
                    std::cin >> arg;
                    word = extract(arg);
                    mac1.setPC(word);
                } else if (command == "rac") {
                    std::string arg;
                    std::cin >> arg;
                    word = extract(arg);
                    mac1.setAC(word);
                } else if (command == "rsp") {
                    std::string arg;
                    std::cin >> arg;
                    word = extract(arg);
                    mac1.setSP(word);
                } 
                std::cout << mac1;
                break;
            case 'q':
                return 0;
            default:
                std::cout << "Sorry, I don't know '" << command << "'\n";
        }
        std::cout << "> ";
    }
}

This is all compiled with g++ as:

g++ -Wall -Wextra -pedantic -std=c++14 mac1.cpp -o mac1

A sample test script is this:

test.mac

m 0 3 50 109 q
a50 LODD 1 PUSH LODD 2 PUSH LODD 3 PUSH CALL 100 INSP 3 STOD 0 HALT FF q
a100 LODL 1 SUBL 2 JNEG 105 LODL 2 STOL 1 LODL 1 SUBL 3 JNEG 10A LODL 3 RETN LODL 1 RETN q
rpc 50
rsp 20
g
d
r
q

This puts three numbers, 0x3, 0x50 and 0x109 into locations 1, 2 and 3 in memory. It then assembles a short program at location 0x50 which calls a subroutine at location 0x100 which finds the smallest of the three passed numbers and returns it in the ac register. The script runs this short program, displays the first few bytes of memory (memory location 0 will contain the smallest of the three numbers), displays the register contents and then quits.

The resulting output should look like this:

Commands: [h]elp, [?], [a]ssemble, [d]ump, [g]o, [l]ist, [m]emory, [r]egisters, [s]tep, [q]uit
rpc XXXX, rsp XXXX, rac XXXX, dXXXX, lXXXX
> Modifying memory starting at address 0x0000. Enter `q` to quit
> Assembling starting at address 0x0050. Enter `q` to quit
0050: LODD 0x0001
0051: PUSH
0052: LODD 0x0002
0053: PUSH
0054: LODD 0x0003
0055: PUSH
0056: CALL 0x0100
0057: INSP 0x0003
0058: STOD 0x0000
0059: HALT 0x00ff
> Assembling starting at address 0x0100. Enter `q` to quit
0100: LODL 0x0001
0101: SUBL 0x0002
0102: JNEG 0x0105
0103: LODL 0x0002
0104: STOL 0x0001
0105: LODL 0x0001
0106: SUBL 0x0003
0107: JNEG 0x010a
0108: LODL 0x0003
0109: RETN
010a: LODL 0x0001
010b: RETN
> PC = 0x0050  SP = 0x0000  AC = 0x0000
> PC = 0x0050  SP = 0x0020  AC = 0x0000
> > 
0000: 0003 0003 0050 0109 0000 0000 0000 0000 
0008: 0000 0000 0000 0000 0000 0000 0000 0000 
0010: 0000 0000 0000 0000 0000 0000 0000 0000 
0018: 0000 0000 0000 0000 0057 0050 0050 0003 
> PC = 0x005a  SP = 0x0020  AC = 0x0003
> 

Note that commands are not echoed, so this only shows the program's responses. Comments welcome.

Here's a live link where you can try the program.

\$\endgroup\$
1
\$\begingroup\$
  • Memory is register?

    Schematics (see the first link) clearly indicates that memory is not a part of the controller. I'd rather dedicate the private address/data/access mode registers to CPU, and let the RAM be simulated on its own.

    Schematics also doesn't specify the address bus width, and/or the semantics of misaligned access. The simulation seems to assume the host behaviour; comments are welcome.

  • static_cast

    Is it warranted? r.ac & (1 <<(mem_width - 1)) seems to convey the same semantics in a more explicit manner.

  • Inconsistency

    Actions for CALL and PSHI instructions explicitly decrement r.sp = r.sp - 1;, whereas RET and PUSH, POP and POPI use ++/-- operators. Not that it affects the behaviour, but surely brings attention.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the review. I wasn't interested in modelling the internal design (other sims already do that). My goal was to model the machine at the macro-assembly level, hence not much point in modelling RAM separately. For the static cast, I used it deliberately to highlight the only two instructions where the AC register is treated as signed. For the sp decrement, I agree entirely. I thought I had changed it already. \$\endgroup\$ – Edward Dec 28 '15 at 11:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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