This post is about riscv python model. The doc describes it as:

This is a python model of the RISC-V ISA. It is intended to be a resource for Python-based automated testing and verification. It is under development and not very useful yet, but can be used to generate random assembler codeThis is a python model of the RISC-V ISA. It is intended to be a resource for Python-based automated testing and verification. It is under development and not very useful yet, but can be used to generate random assembler code

It provides many utils for generating, encoding and decoding riscv instructions. For example, It can generate random rv instructions.

$ riscv-random-asm 100 -i add -i or -i slti
add x19, x5, x31
or x24, x19, x3
slti x4, x31, 1436
add x19, x15, x30
or x28, x25, x14
or x26, x19, x19
slti x26, x26, -1747

Another utility is decoding back opcode to asm

$ riscv-machinsn-decode hexstring 0x007938b3
sltu x17, x18, x7

Cool! Right?

But the most interesting part is “RISCV Model” which can work as golden mode.

Starting with Simulator which defines the top level methods to load programs into program memory and initial memory contents and eventually run to reset model and start running instructions from program memory.

class Simulator:
  def __init__(self, model):
    self.model = model
    self.program = []

  def load_program(self, program, *, address=0):
    self.program = [i for i in program]

  def load_data(self, data = "", *, address=0):
    mem = self.model.state.memory.memory
    for a in range(int(len(data)/4)):
      mem[a] = struct.unpack("<L", data[a*4:(a+1)*4])[0]

  def run(self, *, pc=0):
    self.model.reset(pc=pc)
    cnt = 0
    while True:
      try:
        self.model.issue(self.program[int(self.model.state.pc)>>2])

In model.py, Model creates important component State

    def issue(self, insn):
        self.state.pc += 4
        expected_pc = self.state.pc
        insn.execute(self)

        trace = self.state.changes()
        if self.verbose is not False:
            self.verbose_file.write(self.asm_tpl.format(str(insn), ", ".join([str(t) for t in trace])))
        self.state.commit()

And State defines two methods commit and change which handle the update to register file and memory

    def changes(self):
        c = self.intreg.changes()
        if self.pc_update.value != self.pc.value + 4:
            c.append(TracePC(self.pc_update.value))
        c += self.memory.changes()
        return c

    def commit(self):
        self.intreg.commit()
        self.pc.set(self.pc_update.value)
        self.memory.commit()

and Memory does the actual memory update

    def commit(self):
        for update in self.memory_updates:
            address = update.addr
            base = address >> 2
            offset = address & 0x3
            if base not in self.memory:
                self.memory[base] = randrange(0, 1 << 32)
            data = update.data
            if update.gran == TraceMemory.GRANULARITY.BYTE:
                mask = ~(0xFF << (offset*8)) & 0xFFFFFFFF
                data = (self.memory[base] & mask) | (data << (offset*8))
            self.memory[base] = data

        self.memory_updates = []

And for completeness, RegisterFile updates registers with commit

    def commit(self):
        for t in self.regs_updates:
            self.regs[t.id].set(t.value)
        self.regs_updates.clear()