The Problem

Preparation

i
    def __init__(self, writer=sys.stdout):
        """Set up memory."""
        self.writer = writer
        self.initialize([])
i
    def write(self, *args):
        msg = "".join(args) + "\n"
        self.writer.write(msg)

Getting Input

i
    def __init__(self, reader=input, writer=sys.stdout):
        super().__init__(writer)
        self.reader = reader
i
    def read(self, prompt):
        return self.reader(prompt).strip()

Enumerating State

i
class VMState(Enum):
    """Virtual machine states."""
    FINISHED = 0
    STEPPING = 1
    RUNNING = 2

Running

i
    def run(self):
        self.state = VMState.STEPPING
        while True:
            if self.state == VMState.STEPPING:
                self.interact(self.ip)
            if self.state == VMState.FINISHED:
                break
            instruction = self.ram[self.ip]
            self.ip += 1
            op, arg0, arg1 = self.decode(instruction)
            self.execute(op, arg0, arg1)

Interaction Cases

  1. Empty line: go around again

  2. Disassemble current instruction or show memory: do that and go around again

  3. Quit: change state to FINISHED.

  4. Run normally: change state to RUNNING

  5. Single-step: exit loop without changing state

Disassembling

i
OPS_LOOKUP = {value["code"]: key for key, value in OPS.items()}
Building a consistent lookup table
Figure 1: Building a consistent lookup table.

Capturing Output

i
class Writer:
    def __init__(self):
        self.seen = []

    def write(self, *args):
        self.seen.extend(args)

Providing Input

i
class Reader:
    def __init__(self, *args):
        self.commands = args
        self.index = 0

    def __call__(self, prompt):
        assert self.index < len(self.commands)
        self.index += 1
        return self.commands[self.index - 1]

Testing Disassembly

i
def test_disassemble():
    source = """
    hlt
    """
    reader = Reader("d", "q")
    writer = Writer()
    execute(source, reader, writer)
    assert writer.seen == ["hlt | 0 | 0\n"]
  1. Create program (just a hlt instruction)
  2. Create a Reader with the commands "d" and "q"
  3. Create a Writer to capture output
  4. Run the program
  5. Check that the output is correct

Is It Worth It?

i
def test_print_two_values():
    source = """
    ldc R0 55
    prr R0
    ldc R0 65
    prr R0
    hlt
    """
    reader = Reader("s", "s", "s", "q")
    writer = Writer()
    execute(source, reader, writer)
    assert writer.seen == [
        "000037\n"
    ]

Other Tools

Extensibility

i
    def _do_memory(self, addr):
        self.show()
        return True
i
    def _do_step(self, addr):
        self.state = VMState.STEPPING
        return False

Extensibility

i
    def interact(self, addr):
        prompt = "".join(sorted({key[0] for key in self.handlers}))
        interacting = True
        while interacting:
            try:
                command = self.read(f"{addr:06x} [{prompt}]> ")
                if not command:
                    continue
                elif command not in self.handlers:
                    self.write(f"Unknown command {command}")
                else:
                    interacting = self.handlers[command](self.ip)
            except EOFError:
                self.state = VMState.FINISHED
                interacting = False

Extensibility

i
    def __init__(self, reader=input, writer=sys.stdout):
        super().__init__(reader, writer)
        self.handlers = {
            "d": self._do_disassemble,
            "dis": self._do_disassemble,
            "i": self._do_ip,
            "ip": self._do_ip,
            "m": self._do_memory,
            "memory": self._do_memory,
            "q": self._do_quit,
            "quit": self._do_quit,
            "r": self._do_run,
            "run": self._do_run,
            "s": self._do_step,
            "step": self._do_step,
        }

Stop Here

Breakpoint Sets

Storing breakpoint addresses beside the program
Figure 2: Storing breakpoints beside the program.

What Hardware Does

Inserting breakpoint instructions
Figure 3: Inserting breakpoints into a program.

Add Commands

i
    def __init__(self):
        super().__init__()
        self.breaks = {}
        self.handlers |= {
            "b": self._do_add_breakpoint,
            "break": self._do_add_breakpoint,
            "c": self._do_clear_breakpoint,
            "clear": self._do_clear_breakpoint,
        }

Setting and Clearing

i
    def _do_add_breakpoint(self, addr):
        if self.ram[addr] == OPS["brk"]["code"]:
            return True
        self.breaks[addr] = self.ram[addr]
        self.ram[addr] = OPS["brk"]["code"]
        return True
i
    def _do_clear_breakpoint(self, addr):
        if self.ram[addr] != OPS["brk"]["code"]:
            return True
        self.ram[addr] = self.breaks[addr]
        del self.breaks[addr]
        return True

Running

i
    def run(self):
        self.state = VMState.STEPPING
        while self.state != VMState.FINISHED:
            instruction = self.ram[self.ip]
            op, arg0, arg1 = self.decode(instruction)

            if op == OPS["brk"]["code"]:
                original = self.breaks[self.ip]
                op, arg0, arg1 = self.decode(original)
                self.interact(self.ip)
                self.ip += 1
                self.execute(op, arg0, arg1)

            else:
                if self.state == VMState.STEPPING:
                    self.interact(self.ip)
                self.ip += 1
                self.execute(op, arg0, arg1)

Summary

Concept map for debugger
Figure 4: Concept map.