blob: 1549e1a744ef3b41cda183dfda3347ca177c6dfe [file] [log] [blame]
#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
from enum import Enum
from math import inf
from typing import Optional, Tuple
from .control_flow import (ControlGraph, Cycle, Ecall, ImemEnd, LoopEnd,
LoopStart, Ret, program_control_graph,
subroutine_control_graph)
from .decode import OTBNProgram
class StopPoint(Enum):
LOOP_END = 'loop end'
RET = 'ret'
ECALL = 'ecall'
def _get_insn_count_range(program: OTBNProgram, graph: ControlGraph,
start_pc: int,
stop_at: StopPoint) -> Tuple[int, int]:
'''Return minimum and maximum instruction counts across control paths.
In the presence of control-flow cycles or loops with a non-constant
number of iterations, the maximum number of iterations will be
None, because there's no clear maximum number of times the loop/cycle
could run.
The `stop_at` parameter indicates the expected end point of the control
flow path(s) from this starting point. It is not currently supported for
control flow paths from a single start point to have different endings --
for instance, for one branch to end the program with `ecall` and another to
return to the caller with `ret`. This is because, for instance, if this
function is called recursively on a subroutine (stop_at = RET), then the
caller needs to know the min/max instruction count *after the subroutine
call returns*, and it would require substantial additional tracking to
account for some additional, separate path that ends the program entirely.
In all cases, the function will raise an error if it encounters one of the
other stopping points; for instance, if an `ecall` instruction appears when
`stop_at` = RET, then there will be an error. The function will return the
min/max instruction counts across *all control-flow paths* from the given
start point to the stopping point.
'''
section, edges = graph.get_entry(start_pc)
sec_count = len(section.get_insn_sequence(program))
# Special case for when we're in a loop; if we have the loop end and one
# other edge, this represents the option to either continue the loop or
# stop. In this case, we don't want to follow the "stop" branch, but rather
# simply return the min/max from ending the loop here.
if any([isinstance(e, LoopEnd) for e in edges]):
assert stop_at == StopPoint.LOOP_END
# At a loop end, we expect exactly two edges; one to end the loop and
# one to go back to the start and do another iteration.
assert len(edges) == 2
return (sec_count, sec_count)
# Find the minimum/maximum instruction counts for all next edges.
min_count = inf
max_count = sec_count
for loc in edges:
if isinstance(loc, Ecall) or isinstance(loc, ImemEnd):
assert stop_at == StopPoint.ECALL
loc_min, loc_max = 0, 0
elif isinstance(loc, Ret):
assert stop_at == StopPoint.RET
loc_min, loc_max = 0, 0
elif isinstance(loc, LoopEnd):
# All LoopEnds should have been handled above!
assert False, f'Unexpected loop end at PC {section.end:#x}'
elif isinstance(loc, LoopStart):
# Calculate the number of iterations if possible.
insn = program.get_insn(section.end)
if insn.mnemonic == 'loopi':
op_vals = program.get_operands(section.end)
num_iterations = op_vals['iterations']
loop_min, loop_max = _get_insn_count_range(
program, graph, loc.loop_start_pc, StopPoint.LOOP_END)
loop_min *= num_iterations
loop_max *= num_iterations
else:
# Cannot determine # iterations statically.
loop_min, loop_max = 0, inf
# Calculate the instruction count range after the loop.
post_loop_min, post_loop_max = _get_insn_count_range(
program, graph, loc.loop_end_pc + 4, stop_at)
loc_min = loop_min + post_loop_min
loc_max = loop_max + post_loop_max
elif isinstance(loc, Cycle):
# For cycles, the minimum is 0 (if the cycle is never traversed)
# and the maximum is None (because we can't easily calculate
# statically how many times the cycle will be traversed).
loc_min, loc_max = 0, inf
else:
insn = program.get_insn(section.end)
operands = program.get_operands(section.end)
if insn.mnemonic == 'jal' and operands['grd'] == 1:
# Jumping to another subroutine; count the range for the
# subroutine itself.
jump_min, jump_max = _get_insn_count_range(
program, graph, loc.pc, StopPoint.RET)
# Calculate the instruction count range after returning from
# the jump.
post_jump_min, post_jump_max = _get_insn_count_range(
program, graph, section.end + 4, stop_at)
loc_min = jump_min + post_jump_min
loc_max = jump_max + post_jump_max
else:
# If not a jump, then this is just a normal PC (i.e. a branch).
# Follow the branch to get the min/max range.
loc_min, loc_max = _get_insn_count_range(
program, graph, loc.pc, stop_at)
# Merge the min/max for this location into the final result
min_count = min(min_count, sec_count + loc_min)
max_count = max(max_count, sec_count + loc_max)
return (min_count, max_count)
def program_insn_count_range(
program: OTBNProgram) -> Tuple[int, Optional[int]]:
'''Return minimum and maximum instruction counts for the program.
Wrapper for `_get_insn_count_range` that works on the full program; it
starts at graph.start and returns the instruction counts for all paths that
lead to the end of the program.
'''
graph = program_control_graph(program)
min_count, max_count = _get_insn_count_range(program, graph, graph.start,
StopPoint.ECALL)
if max_count == inf:
max_count = None
return min_count, max_count
def subroutine_insn_count_range(program: OTBNProgram,
subroutine: str) -> Tuple[int, Optional[int]]:
'''Return minimum and maximum instruction counts for the subroutine.
Wrapper for `_get_insn_count_range` that works on a subroutine; it starts
at graph.start and returns the instruction counts for all paths that lead
to a return to the original caller. If a path leads to the program ending
(i.e. an `ecall` instruction), then there will be an error.
'''
graph = subroutine_control_graph(program, subroutine)
min_count, max_count = _get_insn_count_range(program, graph, graph.start,
StopPoint.RET)
if max_count == inf:
max_count = None
return min_count, max_count