blob: 43820695872e03ee3014b7045fcf6a514772cca2 [file]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
import random
from typing import List, Optional, Tuple
from shared.insn_yaml import Insn, InsnsFile
from shared.operand import ImmOperandType, RegOperandType, OperandType
from .jump import Jump
from .straight_line_insn import StraightLineInsn
from ..config import Config
from ..program import ProgInsn, Program
from ..model import Model
from ..snippet import LoopSnippet, Snippet
from ..snippet_gen import GenCont, GenRet, SimpleGenRet, SnippetGen
class Loop(SnippetGen):
'''A generator that generates a LOOP / LOOPI'''
# The shape of a loop that's being generated. The triple is (opval,
# num_iters, bodysize) where opval is the encoded value for the operand,
# num_iters is the number of iterations and bodysize is the size of the
# loop body.
Shape = Tuple[int, int, int]
# The individual pieces of a generated loop. The tuple is (shape, hd_insn,
# body_snippet, model_afterwards)
Pieces = Tuple[Shape, ProgInsn, Snippet, Model]
def __init__(self, cfg: Config, insns_file: InsnsFile) -> None:
super().__init__()
self.jump_gen = Jump(cfg, insns_file)
self.sli_gen = StraightLineInsn(cfg, insns_file)
self.loop = self._get_named_insn(insns_file, 'loop')
self.loopi = self._get_named_insn(insns_file, 'loopi')
# loop expects operands: grs, bodysize
if not (len(self.loop.operands) == 2 and
isinstance(self.loop.operands[0].op_type, RegOperandType) and
self.loop.operands[0].op_type.reg_type == 'gpr' and
isinstance(self.loop.operands[1].op_type, ImmOperandType) and
not self.loop.operands[1].op_type.signed):
raise RuntimeError('LOOP instruction from instructions file is not '
'the shape expected by the Loop generator.')
# loopi expects operands: iterations, bodysize
if not (len(self.loopi.operands) == 2 and
isinstance(self.loopi.operands[0].op_type, ImmOperandType) and
not self.loopi.operands[0].op_type.signed and
isinstance(self.loopi.operands[1].op_type, ImmOperandType) and
not self.loopi.operands[1].op_type.signed):
raise RuntimeError('LOOPI instruction from instructions file is not '
'the shape expected by the Loop generator.')
self.loopi_prob = 0.5
loop_weight = cfg.insn_weights.get('loop')
loopi_weight = cfg.insn_weights.get('loopi')
sum_weights = loop_weight + loopi_weight
if sum_weights == 0:
self.disabled = True
else:
self.loopi_prob = loopi_weight / sum_weights
def _pick_loop_iterations(self,
max_iters: int,
model: Model) -> Optional[Tuple[int, int]]:
'''Pick the number of iterations for a LOOP loop
To do this, we pick a register whose value we know and which doesn't
give us a ridiculous number of iterations (checking model.fuel).
max_iters is the maximum number of iterations possible, given how much
fuel we have left.
Returns the register index, together with the number of iterations that
implies.
'''
# Never generate more than 10 iterations (because the instruction
# sequences would be booooring). Obviously, we'll need to come back to
# this when filling coverage holes.
#
# In general, we'll weight by 1/(1 + abs(iters - 2)). This makes 2 the
# most likely iteration count (which is good, because 1 iteration is
# boring).
max_iters = min(max_iters, 10)
# Iterate over the known registers, trying to pick a weight
poss_pairs = [] # type: List[Tuple[int, int]]
weights = [] # type: List[float]
for idx, value in model.regs_with_known_vals('gpr'):
if 0 < value <= max_iters:
poss_pairs.append((idx, value))
# Weight higher iteration counts smaller (1 / count)
weights.append(1 / (1 + abs(value - 2)))
if not poss_pairs:
return None
return random.choices(poss_pairs, weights=weights)[0]
def _pick_loopi_iterations(self,
max_iters: int,
op_type: OperandType,
model: Model) -> Optional[Tuple[int, int]]:
'''Pick the number of iterations for a LOOPI loop
max_iters is the maximum number of iterations possible, given how much
fuel we have left.
Returns the encoded and decoded number of iterations.
'''
assert isinstance(op_type, ImmOperandType)
iters_range = op_type.get_op_val_range(model.pc)
assert iters_range is not None
iters_lo, iters_hi = iters_range
# Very occasionally, generate iters_hi iterations (the maximum number
# representable) if we've got fuel for it. We don't do this often,
# because the instruction sequence will end up just testing loop
# handling and be very inefficient for testing anything else.
if max_iters >= iters_hi and random.random() < 0.01:
enc_val = op_type.op_val_to_enc_val(iters_hi, model.pc)
# This should never fail, because iters_hi was encodable.
assert enc_val is not None
return (enc_val, iters_hi)
# The rest of the time, we don't usually (95%) generate more than 3
# iterations (because the instruction sequences are rather
# repetitive!). Also, don't generate 0 iterations here, even though
# it's encodable. That causes an error, so we'll do that in a separate
# generator.
if random.random() < 0.95:
tgt_max_iters = min(max_iters, 3)
else:
tgt_max_iters = 10000
ub = min(iters_hi, max_iters, tgt_max_iters)
lb = max(iters_lo, 1)
if ub < lb:
return None
# Otherwise, pick a value uniformly in [iters_lo, iters_hi]. No need
# for clever weighting: in the usual case, there are just 3
# possibilities!
num_iters = random.randint(lb, ub)
enc_val = op_type.op_val_to_enc_val(num_iters, model.pc)
# This should never fail: the choice should have been in the encodable
# range.
assert enc_val is not None
return (enc_val, num_iters)
def _pick_iterations(self,
op_type: OperandType,
bodysize: int,
model: Model) -> Optional[Tuple[int, int]]:
'''Pick the number of iterations for a loop
Returns the encoded value (register index or encoded number of
iterations), together with the number of iterations that implies.
'''
assert bodysize > 0
min_fuel_per_iter = 1 if bodysize == 1 else 2
# model.fuel - 2 is the fuel after executing the LOOP/LOOPI instruction
# and before executing the minimum-length single-instruction
# continuation.
max_iters = (model.fuel - 2) // min_fuel_per_iter
if isinstance(op_type, RegOperandType):
assert op_type.reg_type == 'gpr'
return self._pick_loop_iterations(max_iters, model)
else:
assert isinstance(op_type, ImmOperandType)
return self._pick_loopi_iterations(max_iters, op_type, model)
def _pick_loop_shape(self,
op0_type: OperandType,
op1_type: ImmOperandType,
space_here: int,
model: Model,
program: Program) -> Optional[Shape]:
'''Pick the size of loop and number of iterations
op_type is the type of the first operand (either 'grs' for loop or
'iterations' for loopi). space_here is the number of instructions'
space available at the current position.
'''
# The first upper bound on bodysize is that we've got to have an empty
# space for the loop body.
#
# Note: This doesn't allow us to generate a "loop" that encloses
# previously generated code. So, for example, we couldn't do something
# like
#
# loopi 10, 3
# jal x0, .+8
# jal x0, .+100 // an isolated instruction that ran earlier
# addi x0, 0 // end of loop
#
# Since we can generate jumps in the loop, we might "fill in
# the middle" afterwards. However, we'll never make a loop that
# "contains" instructions we executed before.
#
# To weaken this, we would need to just require that the end of the
# loop is not yet taken. But that sounds a bit hard: let's not worry
# about it for now.
assert 3 <= space_here
max_bodysize = space_here - 2
# Another upper bound comes from program.space. If bodysize is 2 or
# more, our body will need to generate at least 2 instructions (either
# a straight line of length bodysize, or a jump from the start and then
# a straight line instruction at the end). In this case, we need space
# for at least 3 instructions (including the LOOP/LOOPI instruction
# itself).
#
# We know that program.space is at least 2 (checked in gen()), but if
# it's 2, we can only have a bodysize of 1.
assert 2 <= program.space
if program.space == 2:
max_bodysize = min(max_bodysize, 1)
bodysize_range = op1_type.get_op_val_range(model.pc)
assert bodysize_range is not None
bs_min, bs_max = bodysize_range
if max_bodysize < max(1, bs_min):
return None
# Decide on the bodysize value. tail_pc is the address of the last
# instruction in the loop body.
bodysize = random.randint(max(1, bs_min), min(bs_max, max_bodysize))
tail_pc = model.pc + 4 * bodysize
assert program.get_insn_space_at(tail_pc) >= 2
iters = self._pick_iterations(op0_type, bodysize, model)
if iters is None:
return None
iter_opval, num_iters = iters
return (iter_opval, num_iters, bodysize)
def _gen_body(self,
bodysize: int,
single_iter: bool,
bogus_insn: ProgInsn,
cont: GenCont,
model: Model,
program: Program) -> Optional[SimpleGenRet]:
'''Generate the body of a loop
The model is currently sitting at the start of the loop body.
model.fuel is assumed to be the amount of fuel needed for a single
iteration of the loop. If model.fuel is 1, bodysize must also be 1.
This updates model and program unconditionally, trashing them if it
returns None.
'''
assert 1 <= bodysize
assert 1 <= model.fuel
assert bodysize == 1 or model.fuel > 1
match_addr = model.pc + 4 * bodysize
# If bodysize is at least 2, we need to generate at least 2
# instructions (either a straight line of length bodysize, or a jump
# from the start and then a straight line instruction at the end). We
# should definitely have space for that.
min_space_needed = 1 if bodysize == 1 else 2
assert min_space_needed <= program.space
# Pick the tail length. The tail is a sequence of straight-line
# instructions that leads up to the end of the loop body. There must be
# at least one (part of the spec for a loop).
#
# The maximum tail length depends on model.fuel: if bodysize is huge,
# but fuel is just 2 (say), we can generate a body consisting of a jump
# to the end and then a single instruction tail.
#
# This gives a bound on tail length of model.fuel - 1 (to allow an
# instruction for the jump), unless bodysize <= model.fuel, in which
# case we can generate a loop body that's just a tail.
if bodysize <= model.fuel:
max_tail_len = bodysize
else:
max_tail_len = min(bodysize, model.fuel - 1)
# program.space gives another bound on the tail length. If the bodysize
# is large enough that we'll need to jump to the tail, the tail can't
# be more than program.space - 1 in length. If we don't need to
# generate a jump instruction, it can be up to program.space.
if bodysize <= program.space:
max_tail_len = min(max_tail_len, program.space)
else:
assert 2 <= program.space
max_tail_len = min(max_tail_len, program.space - 1)
assert max_tail_len >= 1
tail_len = random.randint(1, max_tail_len)
# When we're generating the body of the loop, we mustn't update a
# register whose value we relied on. For example, we might know that x3
# contains 0x20 and use it as a load address, but then write 0x30 to
# it. This will go badly when we come around again!
#
# To avoid the problem, we explicitly pick the registers that we are
# going to leave unmolested and mark them as special in the model. We
# then write None to all other known registers (to model the fact that
# they have *some* architectural value; we just don't know what it is).
#
# Mark 50% of these registers as not-to-be-touched.
#
# This pass is skipped if we know we're doing exactly one iteration
const_token = model.push_const()
if not single_iter:
for rt, regs in model.all_regs_with_known_vals().items():
for reg_idx, _ in regs:
if random.random() < 0.5:
model.mark_const(rt, reg_idx)
else:
model.forget_value(rt, reg_idx)
# Unconditionally mark x1 as constant, to avoid unbalanced push/pop
# sequences in the loop.
#
# TODO: This means we don't use x1 inside loop bodies; we need to
# fix that.
model.mark_const('gpr', 1)
# If the tail isn't the entire loop, generate the first part of the
# loop body. While doing so, we constrain fuel (to save enough for the
# tail) and work with a program where we've inserted copies of a dummy
# instruction over all the addresses that tail will use, plus the first
# instruction after the loop.
head_snippet = None
assert tail_len <= bodysize
tail_start = model.pc + 4 * (bodysize - tail_len)
if tail_len < bodysize:
assert tail_len < model.fuel
model.fuel -= tail_len + 1
if model.fuel > 0:
# If there's some fuel for a head that isn't just a jump to the
# tail, generate it here.
prog0 = program.copy()
bogus_tail_insns = [bogus_insn] * (tail_len + 1)
prog0.add_insns(tail_start, bogus_tail_insns)
head_snippet, model = cont(model, prog0, False)
# Generation of the head might have failed, but that's actually
# ok: we'll just start the loop body with the jump to the tail.
# If it succeeded, add its instructions to program.
if head_snippet is not None:
head_snippet.insert_into_program(program)
# Add one back to model.fuel for the jump instruction we might need
# to get to the tail.
model.fuel += 1
if model.pc != tail_start:
# If model hasn't ended up at tail_start, insert a jump to get
# there. Use program rather than prog0 because prog0 has a
# dummy instruction in the way. Note that jump_gen.gen_tgt will
# insert the jump instruction that it generates into program.
jump_ret = self.jump_gen.gen_tgt(model, program, tail_start)
if jump_ret is None:
return None
jump_snippet, model = jump_ret
assert model.pc == tail_start
head_snippet = Snippet.cons_option(head_snippet, jump_snippet)
# Add tail_len fuel back to model (undoing the rest of the
# subtraction we did before we started generating the head)
model.fuel += tail_len
# We should always have generated at least something at this point
# (because the original value of model.pc can't have been
# tail_start if tail_len was less than bodysize).
#
# We've also updated the model as if it just ran the head and have
# inserted head_snippet into program.
assert head_snippet is not None
# At this point, we've generated any head snippet and model.pc now
# points at tail_start. Generate the remaining straight line
# instructions that we need.
assert model.pc == tail_start
tail_ret = self.sli_gen.gen_some(tail_len, model, program)
if tail_ret is None:
return None
tail_snippet, model = tail_ret
assert model.pc == match_addr
snippet = Snippet.cons_option(head_snippet, tail_snippet)
# Remove the const annotations that we added to the model
model.pop_const(const_token)
return (snippet, model)
def _pick_loop_insn(self) -> Insn:
'''Pick either LOOP or LOOPI'''
is_loopi = random.random() < self.loopi_prob
return self.loopi if is_loopi else self.loop
def _setup_body(self,
hd_insn: ProgInsn,
model: Model,
program: Program) -> Model:
'''Set up a Model for use in body; insert hd_insn into program'''
body_model = model.copy()
body_model.update_for_insn(hd_insn)
body_model.pc += 4
body_model.loop_depth += 1
assert body_model.loop_depth <= Model.max_loop_depth
program.add_insns(model.pc, [hd_insn])
return body_model
def _gen_pieces(self,
cont: GenCont,
model: Model,
program: Program) -> Optional[Pieces]:
'''Generate a loop and return its constituent pieces
This is useful for subclasses that alter the generated loop after the
fact.
As with gen(), if this function succeeds, it will modify program and
may modify model.
'''
# A loop or loopi sequence has a loop/loopi instruction, at least one
# body instruction (the last of which must be a straight line
# instruction) and then needs a following trampoline. That means we
# need at least 3 instructions' space at the current PC.
space_here = program.get_insn_space_at(model.pc)
if space_here < 3:
return None
# The smallest possible loop takes 2 instructions (the loop instruction
# plus the single-instruction loop body)
if program.space < 2:
return None
# Don't blow the loop stack
if model.loop_depth == Model.max_loop_depth:
return None
insn = self._pick_loop_insn()
# Pick a loop count
op0_type = insn.operands[0].op_type
op1_type = insn.operands[1].op_type
assert isinstance(op1_type, ImmOperandType)
lshape = self._pick_loop_shape(op0_type,
op1_type, space_here, model, program)
if lshape is None:
return None
iter_opval, num_iters, bodysize = lshape
# Generate the head instruction (which runs once, unconditionally) and
# clone model and program to add it
enc_bodysize = op1_type.op_val_to_enc_val(bodysize, model.pc)
assert enc_bodysize is not None
hd_insn = ProgInsn(insn, [iter_opval, enc_bodysize], None)
body_program = program.copy()
body_model = self._setup_body(hd_insn, model, body_program)
# Constrain fuel in body_model: subtract one (for the first instruction
# after the loop) and then divide by the number of iterations. When we
# picked num_iters, we made sure this was still positive.
#
# The minimum fuel to give is 1 if bodysize is 1 or 2 otherwise
# (because the shortest possible sequence is a jump to the last
# instruction, then a straight line instruction).
min_fuel_per_iter = 1 if bodysize == 1 else 2
max_fuel_per_iter = (body_model.fuel - 1) // num_iters
assert min_fuel_per_iter <= max_fuel_per_iter
fuel_per_iter = random.randint(min_fuel_per_iter, max_fuel_per_iter)
body_model.fuel = fuel_per_iter
body_ret = self._gen_body(bodysize,
num_iters == 1,
hd_insn,
cont, body_model, body_program)
if body_ret is None:
return None
body_snippet, body_model = body_ret
assert body_model.loop_depth == model.loop_depth + 1
body_model.loop_depth -= 1
# Calculate the actual amount of fuel that we used
body_fuel = fuel_per_iter - body_model.fuel
assert body_fuel > 0
fuel_afterwards = model.fuel - num_iters * body_fuel
# Update model to take the loop body into account. If we know we have
# exactly one iteration through the body, we can just take body_model.
# Otherwise, we merge the two after "teleporting" model to the loop
# match address.
assert body_model.pc == model.pc + 4 * (1 + bodysize)
if num_iters == 1:
model = body_model
else:
model.update_for_insn(hd_insn)
model.pc = body_model.pc
model.merge(body_model)
# Fix up model.fuel: the merge function will have taken the minimum
# between model and body_model, but we actually want it to be what
# we computed before.
model.fuel = fuel_afterwards
return (lshape, hd_insn, body_snippet, model)
def gen(self,
cont: GenCont,
model: Model,
program: Program) -> Optional[GenRet]:
hd_addr = model.pc
pieces = self._gen_pieces(cont, model, program)
if pieces is None:
return None
shape, hd_insn, body_snippet, model = pieces
snippet = LoopSnippet(hd_addr, hd_insn, body_snippet)
snippet.insert_into_program(program)
return (snippet, False, model)