blob: ffbc634c80f988d8baabd778d287a11c5f0a0a8a [file] [log] [blame]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
import math
import random
from typing import Dict, List, Optional, Set, Tuple
from shared.operand import (OperandType,
ImmOperandType, OptionOperandType, RegOperandType)
from .known_mem import KnownMem
from .program import ProgInsn
class CallStack:
'''An abstract model of the x1 call stack'''
def __init__(self) -> None:
self._min_depth = 0
self._max_depth = 0
self._elts_at_top = [] # type: List[Optional[int]]
def copy(self) -> 'CallStack':
'''Return a deep copy of the call stack'''
ret = CallStack()
ret._min_depth = self._min_depth
ret._max_depth = self._max_depth
ret._elts_at_top = self._elts_at_top.copy()
return ret
def merge(self, other: 'CallStack') -> None:
self._min_depth = min(self._min_depth, other._min_depth)
self._max_depth = max(self._max_depth, other._max_depth)
new_top = []
for a, b in zip(reversed(self._elts_at_top),
reversed(other._elts_at_top)):
if a == b:
new_top.append(a)
else:
break
new_top.reverse()
self._elts_at_top = new_top
assert self._min_depth <= self._max_depth
assert len(self._elts_at_top) <= self._max_depth
def empty(self) -> bool:
assert 0 <= self._min_depth
return self._min_depth == 0
def full(self) -> bool:
assert self._max_depth <= 8
return self._max_depth == 8
def pop(self) -> None:
assert 0 < self._min_depth
self._min_depth -= 1
self._max_depth -= 1
if self._elts_at_top:
self._elts_at_top.pop()
def peek(self) -> Optional[int]:
assert 0 < self._min_depth
return self._elts_at_top[-1] if self._elts_at_top else None
def write(self, value: Optional[int], update: bool) -> None:
'''Write a value to the call stack.
The update flag works as described for Model.write_reg
'''
if update:
# If we're updating a write to x1, check that the new value refines
# the top of the call stack.
assert self._min_depth > 0
if self._elts_at_top:
assert self._elts_at_top[-1] in [None, value]
self._elts_at_top[-1] = value
else:
self._elts_at_top.append(value)
else:
assert not self.full()
self._min_depth += 1
self._max_depth += 1
self._elts_at_top.append(value)
class Model:
'''An abstract model of the processor and memories
This definitely doesn't try to act as a simulator. Rather, it tracks what
registers and locations in memory are guaranteed have defined values after
following the instruction stream to this point.
'''
def __init__(self, dmem_size: int, reset_addr: int, fuel: int) -> None:
assert fuel >= 0
self.initial_fuel = fuel
self.fuel = fuel
self.dmem_size = dmem_size
# Known values for registers. This is a dictionary mapping register
# type to a dictionary of known registers of that type. The register
# type is a string matching the formats in RegOperandType.TYPE_FMTS.
# The value for a type is another dictionary, mapping register index to
# an Optional[int]. If the value is a number, the register value is
# known to currently equal that number. If it is None, the register
# value is unknown (but the register does have an architectural value).
#
# Note that x1 behaves a bit strangely because of the call stack rules,
# so we don't store it in _known_regs but instead in _call_stack.
self._known_regs = {} # type: Dict[str, Dict[int, Optional[int]]]
# Set x0 (the zeros register)
self._known_regs['gpr'] = {0: 0}
# A call stack, representing the contents of x1. The top of the stack
# is at the end (position -1), to match Python's list.pop function. A
# entry of None means an entry with an architectural value, but where
# we don't actually know what it is (usually a result of some
# arithmetic operation that got written to x1).
self._call_stack = CallStack()
# Known values for memory, keyed by memory type ('dmem', 'csr', 'wsr').
csrs = KnownMem(4096)
wsrs = KnownMem(4096)
self._known_mem = {
'dmem': KnownMem(dmem_size),
'csr': csrs,
'wsr': wsrs
}
# Valid CSRs and WSRs
csrs.touch_addr(0x7c0) # FG0
csrs.touch_addr(0x7c1) # FG1
csrs.touch_addr(0x7c8) # FLAGS
csrs.touch_range(0x7d0, 8) # MOD0 - MOD7
csrs.touch_addr(0xfc0) # RND
wsrs.touch_addr(0x0) # MOD
wsrs.touch_addr(0x1) # RND
wsrs.touch_addr(0x2) # ACC
# The current PC (the address of the next instruction that needs
# generating)
self.pc = reset_addr
def copy(self) -> 'Model':
'''Return a deep copy of the model'''
ret = Model(self.dmem_size, self.pc, self.initial_fuel)
ret.fuel = self.fuel
ret._known_regs = {n: regs.copy()
for n, regs in self._known_regs.items()}
ret._call_stack = self._call_stack.copy()
ret._known_mem = {n: mem.copy()
for n, mem in self._known_mem.items()}
return ret
def merge(self, other: 'Model') -> None:
'''Merge in values from another model'''
assert self.initial_fuel == other.initial_fuel
self.fuel = min(self.fuel, other.fuel)
assert self.dmem_size == other.dmem_size
reg_types = self._known_regs.keys() | other._known_regs.keys()
for reg_type in reg_types:
sregs = self._known_regs.get(reg_type)
oregs = other._known_regs.get(reg_type)
if sregs is None:
# If sregs is None, we have no registers that are known to have
# architectural values.
continue
if oregs is None:
# If oregs is None, other has no registers with architectural
# values. Thus the merged model shouldn't have any either.
del self._known_regs[reg_type]
continue
# Both register files have at least some architectural values.
# Build a new, merged version.
merged = {} # type: Dict[int, Optional[int]]
for reg_name, svalue in sregs.items():
ovalue = oregs.get(reg_name, 'missing')
if ovalue == 'missing':
# The register is missing from oregs. This means it might
# not have an architectural value, so we should skip it
# from sregs too.
pass
elif ovalue is None:
# The register has an architectural value in other, but not
# one we know. Make sure it's unknown here too.
merged[reg_name] = None
else:
assert isinstance(ovalue, int)
if svalue is None:
# The register has an unknown architectural value in
# self and a known value in other. So we don't know its
# value (but it is still architecturally specified): no
# change.
merged[reg_name] = None
else:
# self and other both have a known value for the
# register. Do they match? If so, take that value.
# Otherwise, make it unknown.
merged[reg_name] = None if svalue != ovalue else svalue
self._known_regs[reg_type] = merged
self._call_stack.merge(other._call_stack)
for mem_type, self_mem in self._known_mem.items():
self_mem.merge(other._known_mem[mem_type])
assert self.pc == other.pc
def read_reg(self, reg_type: str, idx: int) -> None:
'''Update the model for a read of the given register
This is mostly ignored, but has an effect for x1, which pops from the
call stack on a read.
'''
if reg_type == 'gpr' and idx == 1:
self._call_stack.pop()
def write_reg(self,
reg_type: str,
idx: int,
value: Optional[int],
update: bool) -> None:
'''Mark a register as having an architectural value
If value is not None, it is the actual value that the register has.
Writes to the zeros register x0 are ignored.
The update flag is normally False. If set, it means that other code has
already updated the model with a write of a value to the register for
this instruction, and we should replace that value with the given one,
which refines the previous value. This is irrelevant for idempotent
registers, but matters for x1.
'''
if reg_type == 'gpr':
if idx == 0:
# Ignore writes to x0
return
if idx == 1:
# Special-case writes to x1
self._call_stack.write(value, update)
return
self._known_regs.setdefault(reg_type, {})[idx] = value
def get_reg(self, reg_type: str, idx: int) -> Optional[int]:
'''Get a register value, if known.'''
if reg_type == 'gpr' and idx == 1:
return self._call_stack.peek()
return self._known_regs.setdefault(reg_type, {}).get(idx)
def touch_mem(self, mem_type: str, base: int, width: int) -> None:
'''Mark {base .. base+width} as known for given memory type'''
assert mem_type in self._known_mem
self._known_mem[mem_type].touch_range(base, width)
def pick_operand_value(self, op_type: OperandType) -> Optional[int]:
'''Pick a random value for an operand
The result will always be non-negative: if the operand is a signed
immediate, this is encoded as 2s complement.
'''
if isinstance(op_type, RegOperandType):
return self.pick_reg_operand_value(op_type)
op_rng = op_type.get_op_val_range(self.pc)
if op_rng is None:
# If we don't know the width, the only immediate that we *know*
# is going to be valid is 0.
return 0
if isinstance(op_type, ImmOperandType):
shift = op_type.shift
else:
shift = 0
align = 1 << shift
lo, hi = op_rng
sh_lo = (lo + align - 1) // align
sh_hi = hi // align
op_val = random.randint(sh_lo, sh_hi) << shift
return op_type.op_val_to_enc_val(op_val, self.pc)
def pick_reg_operand_value(self, op_type: RegOperandType) -> Optional[int]:
'''Pick a random value for a register operand
Returns None if there's no valid value possible.'''
if op_type.is_src():
# This operand needs an architectural value. Pick a register
# from the indices in _known_regs[op_type.reg_type].
known_regs = self._known_regs.get(op_type.reg_type)
if not known_regs:
return None
known_list = list(known_regs)
if op_type.reg_type == 'gpr':
# Add x1 if to the list of known registers (if it has an
# architectural value). This won't appear in known_regs,
# because we don't track x1 there.
assert 1 not in known_regs
if not self._call_stack.empty():
known_list.append(1)
return random.choice(known_list)
# This operand isn't treated as a source. Pick any register, but "roll
# again" if we pick x1 and the call stack is full.
assert op_type.width is not None
while True:
idx = random.getrandbits(op_type.width)
if ((idx == 1 and
op_type.reg_type == 'gpr' and
self._call_stack.full())):
continue
return idx
def regs_with_known_vals(self, reg_type: str) -> List[Tuple[int, int]]:
'''Find registers whose values are known
Returns a list of pairs (idx, value) where idx is the register index
and value is its value.
'''
ret = []
known_regs = self._known_regs.setdefault(reg_type, {})
for reg_idx, reg_val in known_regs.items():
if reg_val is not None:
ret.append((reg_idx, reg_val))
# Handle x1, which has a known value iff the top of the call stack is
# not None
if reg_type == 'gpr':
assert 1 not in known_regs
if not self._call_stack.empty():
x1 = self._call_stack.peek()
if x1 is not None:
ret.append((1, x1))
return ret
def regs_with_architectural_vals(self, reg_type: str) -> List[int]:
'''List registers that have an architectural value'''
known_regs = self._known_regs.setdefault(reg_type, {})
arch_regs = list(known_regs.keys())
# Handle x1, which has an architectural (and known) value iff the call
# stack is not empty.
if reg_type == 'gpr':
assert 1 not in arch_regs
if not self._call_stack.empty():
arch_regs.append(1)
return arch_regs
def pick_lsu_target(self,
mem_type: str,
loads_value: bool,
known_regs: Dict[str, List[Tuple[int, int]]],
imm_rng: Tuple[int, int],
imm_shift: int,
byte_width: int) -> Optional[Tuple[int,
int,
Dict[str, int]]]:
'''Try to pick an address for a naturally-aligned LSU operation.
mem_type is the type of memory (which must a key of self._known_mem).
If loads_value, this address needs to have an architecturally defined
value.
known_regs is a map from operand name to a list of pairs (idx, value)
with index and known value for this register operand. Any immediate
operand will have a value in the range imm_rng (including endpoints)
and a shift of imm_shift. byte_width is the number of contiguous
addresses that the LSU operation touches.
Returns None if we can't find an address. Otherwise, returns a tuple
(addr, imm_val, reg_vals) where addr is the target address, imm_val is
the value of any immediate operand and reg_vals is a map from operand
name to the index picked for that register operand.
'''
assert mem_type in self._known_mem
assert imm_rng[0] <= imm_rng[1]
assert 0 <= imm_shift
# A "general" solution to this needs constraint solving, but we expect
# imm_rng to cover most of the address space most of the time. So we'll
# do something much simpler: pick a value for each register, then pick
# a target address that can be reached from the "sum so far" plus the
# range of the immediate.
reg_indices = {}
reg_sum = 0
# The base address should be aligned to base_align (see the logic in
# KnownMem.pick_lsu_target), otherwise we'll fail to find anything.
base_align = math.gcd(byte_width, 1 << imm_shift)
for name, indices in known_regs.items():
aligned_regs = [(idx, value)
for idx, value in indices
if value % base_align == 0]
# If there are no known aligned indices for this operand, give up now.
if not aligned_regs:
return None
# Otherwise, pick an index and value.
idx, value = random.choice(aligned_regs)
reg_sum += value
reg_indices[name] = idx
known_mem = self._known_mem[mem_type]
addr = known_mem.pick_lsu_target(loads_value,
reg_sum,
imm_rng,
1 << imm_shift,
byte_width,
byte_width)
# If there was no address we could use, give up.
if addr is None:
return None
return (addr, addr - reg_sum, reg_indices)
def update_for_lui(self, prog_insn: ProgInsn) -> None:
'''Update model state after a LUI
A lui instruction looks like "lui x2, 80000" or similar. This operation
is easy to understand, so we can actually update the model registers
appropriately.
'''
insn = prog_insn.insn
op_vals = prog_insn.operands
assert insn.mnemonic == 'lui'
assert len(insn.operands) == len(op_vals)
exp_shape = (len(insn.operands) == 2 and
isinstance(insn.operands[0].op_type, RegOperandType) and
insn.operands[0].op_type.reg_type == 'gpr' and
insn.operands[0].op_type.is_dest() and
isinstance(insn.operands[1].op_type, ImmOperandType) and
not insn.operands[1].op_type.signed)
if not exp_shape:
raise RuntimeError('LUI instruction read from insns.yml is '
'not the shape expected by '
'Model.update_for_lui.')
self._generic_update_for_insn(prog_insn)
assert op_vals[1] >= 0
self.write_reg('gpr', op_vals[0], op_vals[1] << 12, True)
def update_for_addi(self, prog_insn: ProgInsn) -> None:
'''Update model state after an ADDI
If the source register happens to have a known value, we can do the
addition and store the known result.
'''
insn = prog_insn.insn
op_vals = prog_insn.operands
assert insn.mnemonic == 'addi'
assert len(insn.operands) == len(op_vals)
exp_shape = (len(insn.operands) == 3 and
isinstance(insn.operands[0].op_type, RegOperandType) and
insn.operands[0].op_type.reg_type == 'gpr' and
insn.operands[0].op_type.is_dest() and
isinstance(insn.operands[1].op_type, RegOperandType) and
insn.operands[1].op_type.reg_type == 'gpr' and
not insn.operands[1].op_type.is_dest() and
isinstance(insn.operands[2].op_type, ImmOperandType) and
insn.operands[2].op_type.signed)
if not exp_shape:
raise RuntimeError('ADDI instruction read from insns.yml is '
'not the shape expected by '
'Model.update_for_addi.')
src_val = self.get_reg('gpr', op_vals[1])
if src_val is None:
result = None
else:
# op_vals[2] is the immediate, but is already "encoded" as an unsigned
# value. Turn it back into the signed operand that actually gets added.
imm_op = insn.operands[2]
imm_val = imm_op.op_type.enc_val_to_op_val(op_vals[2], self.pc)
assert imm_val is not None
result = (src_val + imm_val) & ((1 << 32) - 1)
self._generic_update_for_insn(prog_insn)
self.write_reg('gpr', op_vals[0], result, True)
def _inc_gpr(self,
gpr: int,
gpr_val: Optional[int],
delta: int,
mask: int) -> None:
'''Mark gpr as having a value and increment it if known
This passes update=False to self.write_reg: it should be used for
registers that haven't already been marked as updated by the
instruction.
'''
new_val = (gpr_val + delta) & mask if gpr_val is not None else None
self.write_reg('gpr', gpr, new_val, False)
def update_for_bnlid(self, prog_insn: ProgInsn) -> None:
'''Update model state after an BN.LID
We need this special case code because of the indirect access to the
wide-side register file.
'''
insn = prog_insn.insn
op_vals = prog_insn.operands
assert insn.mnemonic == 'bn.lid'
assert len(insn.operands) == len(op_vals)
grd_op, grs1_op, offset_op, grs1_inc_op, grd_inc_op = insn.operands
exp_shape = (
# grd
isinstance(grd_op.op_type, RegOperandType) and
grd_op.op_type.reg_type == 'gpr' and
not grd_op.op_type.is_dest() and
# grs1
isinstance(grs1_op.op_type, RegOperandType) and
grs1_op.op_type.reg_type == 'gpr' and
not grs1_op.op_type.is_dest() and
# offset
isinstance(offset_op.op_type, ImmOperandType) and
offset_op.op_type.signed and
# grs1_inc
isinstance(grs1_inc_op.op_type, OptionOperandType) and
# grd_inc
isinstance(grd_inc_op.op_type, OptionOperandType)
)
if not exp_shape:
raise RuntimeError('Unexpected shape for bn.lid')
grd, grs1, offset, grs1_inc, grd_inc = op_vals
grd_val = self.get_reg('gpr', grd)
grs1_val = self.get_reg('gpr', grs1)
self._generic_update_for_insn(prog_insn)
if grd_val is not None:
self.write_reg('wdr', grd_val & 31, None, False)
if grs1_inc:
self._inc_gpr(grs1, grs1_val, 32, (1 << 32) - 1)
elif grd_inc:
self._inc_gpr(grd, grd_val, 1, 31)
def update_for_bnsid(self, prog_insn: ProgInsn) -> None:
'''Update model state after an BN.SID'''
insn = prog_insn.insn
op_vals = prog_insn.operands
assert insn.mnemonic == 'bn.sid'
assert len(insn.operands) == len(op_vals)
grs1_op, grs2_op, offset_op, grs1_inc_op, grs2_inc_op = insn.operands
exp_shape = (
# grs1
isinstance(grs1_op.op_type, RegOperandType) and
grs1_op.op_type.reg_type == 'gpr' and
not grs1_op.op_type.is_dest() and
# grs2
isinstance(grs2_op.op_type, RegOperandType) and
grs2_op.op_type.reg_type == 'gpr' and
not grs2_op.op_type.is_dest() and
# offset
isinstance(offset_op.op_type, ImmOperandType) and
offset_op.op_type.signed and
# grs1_inc
isinstance(grs1_inc_op.op_type, OptionOperandType) and
# grs2_inc
isinstance(grs2_inc_op.op_type, OptionOperandType)
)
if not exp_shape:
raise RuntimeError('Unexpected shape for bn.sid')
grs1, grs2, offset, grs1_inc, grs2_inc = op_vals
grs1_val = self.get_reg('gpr', grs1)
grs2_val = self.get_reg('gpr', grs2)
self._generic_update_for_insn(prog_insn)
if grs1_inc:
self._inc_gpr(grs1, grs1_val, 32, (1 << 32) - 1)
elif grs2_inc:
self._inc_gpr(grs2, grs2_val, 1, 31)
def update_for_bnmovr(self, prog_insn: ProgInsn) -> None:
'''Update model state after an BN.MOVR'''
insn = prog_insn.insn
op_vals = prog_insn.operands
assert insn.mnemonic == 'bn.movr'
assert len(insn.operands) == len(op_vals)
grd_op, grs_op, grd_inc_op, grs_inc_op = insn.operands
exp_shape = (
# grd
isinstance(grd_op.op_type, RegOperandType) and
grd_op.op_type.reg_type == 'gpr' and
not grd_op.op_type.is_dest() and
# grs
isinstance(grs_op.op_type, RegOperandType) and
grs_op.op_type.reg_type == 'gpr' and
not grs_op.op_type.is_dest() and
# grd_inc
isinstance(grd_inc_op.op_type, OptionOperandType) and
# grs_inc
isinstance(grs_inc_op.op_type, OptionOperandType)
)
if not exp_shape:
raise RuntimeError('Unexpected shape for bn.movr')
grd, grs, grd_inc, grs_inc = op_vals
grd_val = self.get_reg('gpr', grd)
grs_val = self.get_reg('gpr', grs)
self._generic_update_for_insn(prog_insn)
if grd_val is not None:
self.write_reg('wdr', grd_val & 31, None, False)
if grd_inc:
self._inc_gpr(grd, grd_val, 1, 31)
elif grs_inc:
self._inc_gpr(grs, grs_val, 1, 31)
def _generic_update_for_insn(self, prog_insn: ProgInsn) -> None:
'''Update registers and memory for prog_insn
Apply side-effecting reads (relevant for x1) then mark any destination
operand as having an architectural value. Finally, apply any memory
changes.
This is called by update_for_insn, either by the specialized updater if
there is one or on its own if there's none.
'''
seen_writes = [] # type: List[Tuple[str, int]]
seen_reads = set() # type: Set[Tuple[str, int]]
insn = prog_insn.insn
assert len(insn.operands) == len(prog_insn.operands)
for operand, op_val in zip(insn.operands, prog_insn.operands):
op_type = operand.op_type
if isinstance(op_type, RegOperandType):
if op_type.is_dest():
seen_writes.append((op_type.reg_type, op_val))
else:
seen_reads.add((op_type.reg_type, op_val))
for op_reg_type, op_val in seen_reads:
self.read_reg(op_reg_type, op_val)
for reg_type, op_val in seen_writes:
self.write_reg(reg_type, op_val, None, False)
# If this is an LSU operation, we've either loaded a value (in which
# case, the memory hopefully had a value already) or we've stored
# something. In either case, we mark the memory as having a value now.
if prog_insn.lsu_info is not None:
assert insn.lsu is not None
mem_type, addr = prog_insn.lsu_info
self.touch_mem(mem_type, addr, insn.lsu.idx_width)
def consume_fuel(self) -> None:
'''Consume one item of fuel, but bottom out at fuel == 1'''
self.fuel = max(1, self.fuel - 1)
def update_for_insn(self, prog_insn: ProgInsn) -> None:
# If this is a sufficiently simple operation that we understand the
# result, or a complicated instruction where we have to do something
# clever, actually set the destination register with a value.
updaters = {
'lui': self.update_for_lui,
'addi': self.update_for_addi,
'bn.lid': self.update_for_bnlid,
'bn.sid': self.update_for_bnsid,
'bn.movr': self.update_for_bnmovr
}
updater = updaters.get(prog_insn.insn.mnemonic)
if updater is not None:
updater(prog_insn)
else:
self._generic_update_for_insn(prog_insn)
self.consume_fuel()