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], ®s.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.