Example #1
0
def calculate_largest_power(a: int, num_bits: int, is_signed: bool) -> int:
    """
    For a given base `a`, compute the maximum power `b` that will not
    produce an overflow in the equation `a ** b`

    Arguments
    ---------
    a : int
        Base value for the equation `a ** b`
    num_bits : int
        The maximum number of bits that the resulting value must fit in
    is_signed : bool
        Is the operation being performed on signed integers?

    Returns
    -------
    int
        Largest possible value for `b` where the result does not overflow
        `num_bits`
    """
    if num_bits % 8:
        raise CompilerPanic("Type is not a modulo of 8")

    value_bits = num_bits - (1 if is_signed else 0)
    if a >= 2**value_bits:
        raise TypeCheckFailure("Value is too large and will always throw")
    elif a < -(2**value_bits):
        raise TypeCheckFailure("Value is too small and will always throw")

    a_is_negative = a < 0
    a = abs(a)  # No longer need to know if it's signed or not
    if a in (0, 1):
        raise CompilerPanic("Exponential operation is useless!")

    # NOTE: There is an edge case if `a` were left signed where the following
    #       operation would not work (`ln(a)` is undefined if `a <= 0`)
    b = int(
        decimal.Decimal(value_bits) /
        (decimal.Decimal(a).ln() / decimal.Decimal(2).ln()))
    if b <= 1:
        return 1  # Value is assumed to be in range, therefore power of 1 is max

    # Do a bit of iteration to ensure we have the exact number
    num_iterations = 0
    while a**(b + 1) < 2**value_bits:
        b += 1
        num_iterations += 1
        assert num_iterations < 10000
    while a**b >= 2**value_bits:
        b -= 1
        num_iterations += 1
        assert num_iterations < 10000

    # Edge case: If a is negative and the values of a and b are such that:
    #               (a) ** (b + 1) == -(2 ** value_bits)
    #            we can actually squeak one more out of it because it's on the edge
    if a_is_negative and (-a)**(b + 1) == -(2**value_bits):  # NOTE: a = abs(a)
        return b + 1
    else:
        return b  # Exact
Example #2
0
    def _get_target(self, target):
        # Check if we are doing assignment of an iteration loop.
        if isinstance(target, vy_ast.Subscript) and self.context.in_for_loop:
            raise_exception = False
            if isinstance(target.value, vy_ast.Attribute):
                if f"{target.value.value.id}.{target.value.attr}" in self.context.in_for_loop:
                    raise_exception = True

            if target.get("value.id") in self.context.in_for_loop:
                raise_exception = True

            if raise_exception:
                raise TypeCheckFailure("Failed for-loop constancy check")

        if isinstance(target,
                      vy_ast.Name) and target.id in self.context.forvars:
            raise TypeCheckFailure("Failed for-loop constancy check")

        if isinstance(target, vy_ast.Tuple):
            target = Expr(target, self.context).lll_node
            for node in target.args:
                if (node.location == "storage"
                        and self.context.is_constant()) or not node.mutable:
                    raise TypeCheckFailure("Failed for-loop constancy check")
            return target

        target = Expr.parse_variable_location(target, self.context)
        if (target.location == "storage"
                and self.context.is_constant()) or not target.mutable:
            raise TypeCheckFailure("Failed for-loop constancy check")
        return target
Example #3
0
    def __init__(self, node: vy_ast.VyperNode, context: Context) -> None:
        self.stmt = node
        self.context = context
        fn = getattr(self, f"parse_{type(node).__name__}", None)
        if fn is None:
            raise TypeCheckFailure(
                f"Invalid statement node: {type(node).__name__}")

        self.lll_node = fn()
        if self.lll_node is None:
            raise TypeCheckFailure("Statement node did not produce LLL")
Example #4
0
def calculate_largest_base(b: int, num_bits: int, is_signed: bool) -> int:
    """
    For a given power `b`, compute the maximum base `a` that will not produce an
    overflow in the equation `a ** b`

    Arguments
    ---------
    b : int
        Power value for the equation `a ** b`
    num_bits : int
        The maximum number of bits that the resulting value must fit in
    is_signed : bool
        Is the operation being performed on signed integers?

    Returns
    -------
    int
        Largest possible value for `a` where the result does not overflow
        `num_bits`
    """
    if num_bits % 8:
        raise CompilerPanic("Type is not a modulo of 8")
    if b < 0:
        raise TypeCheckFailure("Cannot calculate negative exponents")

    value_bits = num_bits - (1 if is_signed else 0)
    if b > value_bits:
        raise TypeCheckFailure("Value is too large and will always throw")
    elif b < 2:
        return 2**value_bits - 1  # Maximum value for type

    # CMC 2022-05-06 TODO we should be able to do this with algebra
    # instead of looping):
    # x ** b == 2**value_bits
    # b ln(x) == ln(2**value_bits)
    # ln(x) == ln(2**value_bits) / b
    # x == exp( ln(2**value_bits) / b)

    # Estimate (up to ~39 digits precision required)
    a = math.ceil(2**(decimal.Decimal(value_bits) / decimal.Decimal(b)))
    # Do a bit of iteration to ensure we have the exact number
    num_iterations = 0
    while (a + 1)**b < 2**value_bits:
        a += 1
        num_iterations += 1
        assert num_iterations < 10000
    while a**b >= 2**value_bits:
        a -= 1
        num_iterations += 1
        assert num_iterations < 10000
    return a
Example #5
0
    def __init__(self, node: vy_ast.VyperNode, context: Context) -> None:
        self.stmt = node
        self.context = context
        fn = getattr(self, f"parse_{type(node).__name__}", None)
        if fn is None:
            raise TypeCheckFailure(f"Invalid statement node: {type(node).__name__}")

        with context.internal_memory_scope():
            self.lll_node = fn()

        if self.lll_node is None:
            raise TypeCheckFailure("Statement node did not produce LLL")

        self.lll_node.annotation = self.stmt.get("node_source_code")
Example #6
0
    def _get_target(self, target):
        if isinstance(target, vy_ast.Name) and target.id in self.context.forvars:
            raise TypeCheckFailure("Failed for-loop constancy check")

        if isinstance(target, vy_ast.Tuple):
            target = Expr(target, self.context).lll_node
            for node in target.args:
                if (node.location == "storage" and self.context.is_constant()) or not node.mutable:
                    raise TypeCheckFailure("Failed for-loop constancy check")
            return target

        target = Expr.parse_variable_location(target, self.context)
        if (target.location == "storage" and self.context.is_constant()) or not target.mutable:
            raise TypeCheckFailure("Failed for-loop constancy check")
        return target
Example #7
0
def get_external_call_output(sig, context):
    if not sig.output_type:
        return 0, 0, []
    output_placeholder = context.new_internal_variable(typ=sig.output_type)
    output_size = get_size_of_type(sig.output_type) * 32
    if isinstance(sig.output_type, BaseType):
        returner = [0, output_placeholder]
    elif isinstance(sig.output_type, ByteArrayLike):
        returner = [0, output_placeholder + 32]
    elif isinstance(sig.output_type, TupleLike):
        # incase of struct we need to decode the output and then return it
        returner = ["seq"]
        decoded_placeholder = context.new_internal_variable(
            typ=sig.output_type)
        decoded_node = LLLnode(decoded_placeholder,
                               typ=sig.output_type,
                               location="memory")
        output_node = LLLnode(output_placeholder,
                              typ=sig.output_type,
                              location="memory")
        returner.append(abi_decode(decoded_node, output_node))
        returner.extend([0, decoded_placeholder])
    elif isinstance(sig.output_type, ListType):
        returner = [0, output_placeholder]
    else:
        raise TypeCheckFailure(f"Invalid output type: {sig.output_type}")
    return output_placeholder, output_size, returner
Example #8
0
def _parse_kwargs(call_expr, context):
    from vyper.codegen.expr import Expr  # TODO rethink this circular import

    def _bool(x):
        assert x.value in (0, 1), "type checker missed this"
        return bool(x.value)

    # note: codegen for kwarg values in AST order
    call_kwargs = {
        kw.arg: Expr(kw.value, context).ir_node
        for kw in call_expr.keywords
    }

    ret = _CallKwargs(
        value=unwrap_location(call_kwargs.pop("value", IRnode(0))),
        gas=unwrap_location(call_kwargs.pop("gas", IRnode("gas"))),
        skip_contract_check=_bool(
            call_kwargs.pop("skip_contract_check", IRnode(0))),
        default_return_value=call_kwargs.pop("default_return_value", None),
    )

    if len(call_kwargs) != 0:
        raise TypeCheckFailure(f"Unexpected keyword arguments: {call_kwargs}")

    return ret
Example #9
0
def _check_assign_list(left, right):
    def FAIL():  # pragma: nocover
        raise TypeCheckFailure(f"assigning {right.typ} to {left.typ}")

    if left.value == "multi":
        # Cannot do something like [a, b, c] = [1, 2, 3]
        FAIL()  # pragma: notest

    if isinstance(left, SArrayType):
        if not isinstance(right, SArrayType):
            FAIL()  # pragma: notest
        if left.typ.count != right.typ.count:
            FAIL()  # pragma: notest

        # TODO recurse into left, right if literals?
        check_assign(dummy_node_for_type(left.typ.subtyp),
                     dummy_node_for_type(right.typ.subtyp))

    if isinstance(left, DArrayType):
        if not isinstance(right, DArrayType):
            FAIL()  # pragma: notest

        if left.typ.count < right.typ.count:
            FAIL()  # pragma: notest

        # stricter check for zeroing
        if right.value == "~empty" and right.typ.count != left.typ.count:
            raise TypeCheckFailure(
                f"Bad type for clearing bytes: expected {left.typ} but got {right.typ}"
            )  # pragma: notest

        # TODO recurse into left, right if literals?
        check_assign(dummy_node_for_type(left.typ.subtyp),
                     dummy_node_for_type(right.typ.subtyp))
Example #10
0
    def __init__(self, node, context):
        self.expr = node
        self.context = context

        if isinstance(node, LLLnode):
            # TODO this seems bad
            self.lll_node = node
            return

        fn = getattr(self, f"parse_{type(node).__name__}", None)
        if fn is None:
            raise TypeCheckFailure(f"Invalid statement node: {type(node).__name__}")

        self.lll_node = fn()
        if self.lll_node is None:
            raise TypeCheckFailure(f"{type(node).__name__} node did not produce LLL")
Example #11
0
def _call_make_placeholder(stmt_expr, context, sig):
    if sig.output_type is None:
        return 0, 0, 0

    output_placeholder = context.new_placeholder(typ=sig.output_type)
    output_size = get_size_of_type(sig.output_type) * 32

    if isinstance(sig.output_type, BaseType):
        returner = output_placeholder
    elif isinstance(sig.output_type, ByteArrayLike):
        returner = output_placeholder
    elif isinstance(sig.output_type, TupleLike):
        # incase of struct we need to decode the output and then return it
        returner = ["seq"]
        decoded_placeholder = context.new_placeholder(typ=sig.output_type)
        decoded_node = LLLnode(decoded_placeholder,
                               typ=sig.output_type,
                               location="memory")
        output_node = LLLnode(output_placeholder,
                              typ=sig.output_type,
                              location="memory")
        returner.append(abi_decode(decoded_node, output_node))
        returner.extend([decoded_placeholder])
    elif isinstance(sig.output_type, ListType):
        returner = output_placeholder
    else:
        raise TypeCheckFailure(f"Invalid output type: {sig.output_type}")
    return output_placeholder, returner, output_size
Example #12
0
def safe_pow(x, y):
    num_info = x.typ._num_info
    if not is_integer_type(x.typ):
        # type checker should have caught this
        raise TypeCheckFailure("non-integer pow")

    if x.is_literal:
        # cannot pass 1 or 0 to `calculate_largest_power`
        if x.value == 1:
            return IRnode.from_list([1])
        if x.value == 0:
            return IRnode.from_list(["iszero", y])

        upper_bound = calculate_largest_power(x.value, num_info.bits,
                                              num_info.is_signed) + 1
        # for signed integers, this also prevents negative values
        ok = ["lt", y, upper_bound]

    elif y.is_literal:
        upper_bound = calculate_largest_base(y.value, num_info.bits,
                                             num_info.is_signed) + 1
        if num_info.is_signed:
            ok = ["and", ["slt", x, upper_bound], ["sgt", x, -upper_bound]]
        else:
            ok = ["lt", x, upper_bound]
    else:
        # `a ** b` where neither `a` or `b` are known
        # TODO this is currently unreachable, once we implement a way to do it safely
        # remove the check in `vyper/context/types/value/numeric.py`
        return

    return IRnode.from_list(["seq", ["assert", ok], ["exp", x, y]])
Example #13
0
    def _get_target(self, target):
        _dbg_expr = target

        if isinstance(target, vy_ast.Name) and target.id in self.context.forvars:
            raise TypeCheckFailure(f"Failed constancy check\n{_dbg_expr}")

        if isinstance(target, vy_ast.Tuple):
            target = Expr(target, self.context).lll_node
            for node in target.args:
                if (node.location == "storage" and self.context.is_constant()) or not node.mutable:
                    raise TypeCheckFailure(f"Failed constancy check\n{_dbg_expr}")
            return target

        target = Expr.parse_pointer_expr(target, self.context)
        if (target.location == "storage" and self.context.is_constant()) or not target.mutable:
            raise TypeCheckFailure(f"Failed constancy check\n{_dbg_expr}")
        return target
Example #14
0
    def parse_BoolOp(self):
        for value in self.expr.values:
            # Check for boolean operations with non-boolean inputs
            _expr = Expr.parse_value_expr(value, self.context)
            if not is_base_type(_expr.typ, "bool"):
                return

        def _build_if_lll(condition, true, false):
            # generate a basic if statement in LLL
            o = ["if", condition, true, false]
            return o

        jump_label = f"_boolop_{self.expr.src}"
        if isinstance(self.expr.op, vy_ast.And):
            if len(self.expr.values) == 2:
                # `x and y` is a special case, it doesn't require jumping
                lll_node = _build_if_lll(
                    Expr.parse_value_expr(self.expr.values[0], self.context),
                    Expr.parse_value_expr(self.expr.values[1], self.context),
                    [0],
                )
                return LLLnode.from_list(lll_node, typ="bool")

            # create the initial `x and y` from the final two values
            lll_node = _build_if_lll(
                Expr.parse_value_expr(self.expr.values[-2], self.context),
                Expr.parse_value_expr(self.expr.values[-1], self.context),
                [0],
            )
            # iterate backward through the remaining values
            for node in self.expr.values[-3::-1]:
                lll_node = _build_if_lll(
                    Expr.parse_value_expr(node, self.context), lll_node,
                    [0, ["goto", jump_label]])

        elif isinstance(self.expr.op, vy_ast.Or):
            # create the initial `x or y` from the final two values
            lll_node = _build_if_lll(
                Expr.parse_value_expr(self.expr.values[-2], self.context),
                [1, ["goto", jump_label]],
                Expr.parse_value_expr(self.expr.values[-1], self.context),
            )

            # iterate backward through the remaining values
            for node in self.expr.values[-3::-1]:
                lll_node = _build_if_lll(
                    Expr.parse_value_expr(node, self.context),
                    [1, ["goto", jump_label]],
                    lll_node,
                )
        else:
            raise TypeCheckFailure(
                f"Unexpected boolean operator: {type(self.expr.op).__name__}")

        # add `jump_label` at the end of the boolop
        lll_node = ["seq_unchecked", lll_node, ["label", jump_label]]
        return LLLnode.from_list(lll_node, typ="bool")
Example #15
0
 def is_valid_varname(self, name, pos):
     # Global context check first.
     if self.global_ctx.is_valid_varname(name, pos):
         check_valid_varname(
             name, custom_structs=self.structs, constants=self.constants, pos=pos,
         )
         # Local context duplicate context check.
         if any((name in self.vars, name in self.globals, name in self.constants)):
             raise TypeCheckFailure(f"Duplicate variable name: {name}")
     return True
Example #16
0
def _get_element_ptr_mapping(parent, key):
    assert isinstance(parent.typ, MappingType)
    subtype = parent.typ.valuetype
    key = unwrap_location(key)

    # TODO when is key None?
    if key is None or parent.location != STORAGE:
        raise TypeCheckFailure(f"bad dereference on mapping {parent}[{key}]")

    return IRnode.from_list(["sha3_64", parent, key],
                            typ=subtype,
                            location=STORAGE)
Example #17
0
    def __init__(self, node, context):
        self.expr = node
        self.context = context

        if isinstance(node, IRnode):
            # TODO this seems bad
            self.ir_node = node
            return

        fn = getattr(self, f"parse_{type(node).__name__}", None)
        if fn is None:
            raise TypeCheckFailure(
                f"Invalid statement node: {type(node).__name__}")

        self.ir_node = fn()
        if self.ir_node is None:
            raise TypeCheckFailure(
                f"{type(node).__name__} node did not produce IR. {self.expr}")

        self.ir_node.annotation = self.expr.get("node_source_code")
        self.ir_node.source_pos = getpos(self.expr)
Example #18
0
def get_external_interface_keywords(stmt_expr, context):
    from vyper.parser.expr import Expr

    value, gas = None, None
    for kw in stmt_expr.keywords:
        if kw.arg == "gas":
            gas = Expr.parse_value_expr(kw.value, context)
        elif kw.arg == "value":
            value = Expr.parse_value_expr(kw.value, context)
        else:
            raise TypeCheckFailure("Unexpected keyword argument")
    return value, gas
Example #19
0
    def from_declaration(cls, class_node, global_ctx):
        name = class_node.name
        pos = 0

        check_valid_varname(
            name,
            global_ctx._structs,
            global_ctx._constants,
            pos=class_node,
            error_prefix="Event name invalid. ",
            exc=EventDeclarationException,
        )

        args = []
        indexed_list = []
        if len(class_node.body) != 1 or not isinstance(class_node.body[0],
                                                       vy_ast.Pass):
            for node in class_node.body:
                arg_item = node.target
                arg = node.target.id
                typ = node.annotation

                if isinstance(typ,
                              vy_ast.Call) and typ.get("func.id") == "indexed":
                    indexed_list.append(True)
                    typ = typ.args[0]
                else:
                    indexed_list.append(False)
                check_valid_varname(
                    arg,
                    global_ctx._structs,
                    global_ctx._constants,
                    pos=arg_item,
                    error_prefix="Event argument name invalid or reserved.",
                )
                if arg in (x.name for x in args):
                    raise TypeCheckFailure(
                        f"Duplicate function argument name: {arg}")
                # Can struct be logged?
                parsed_type = global_ctx.parse_type(typ, None)
                args.append(VariableRecord(arg, pos, parsed_type, False))
                if isinstance(parsed_type, ByteArrayType):
                    pos += ceil32(typ.slice.value.n)
                else:
                    pos += get_size_of_type(parsed_type) * 32

        sig = (name + "(" + ",".join([
            canonicalize_type(arg.typ, indexed_list[pos])
            for pos, arg in enumerate(args)
        ]) + ")")  # noqa F812
        event_id = bytes_to_int(keccak256(bytes(sig, "utf-8")))
        return cls(name, args, indexed_list, event_id, sig)
Example #20
0
def get_external_call_output(sig, context):
    if not sig.output_type:
        return 0, 0, []
    output_placeholder = context.new_placeholder(typ=sig.output_type)
    output_size = get_size_of_type(sig.output_type) * 32
    if isinstance(sig.output_type, BaseType):
        returner = [0, output_placeholder]
    elif isinstance(sig.output_type, ByteArrayLike):
        returner = [0, output_placeholder + 32]
    elif isinstance(sig.output_type, TupleLike):
        returner = [0, output_placeholder]
    elif isinstance(sig.output_type, ListType):
        returner = [0, output_placeholder]
    else:
        raise TypeCheckFailure(f"Invalid output type: {sig.output_type}")
    return output_placeholder, output_size, returner
Example #21
0
def _get_special_kwargs(stmt_expr, context):
    from vyper.codegen.expr import Expr  # TODO rethink this circular import

    value, gas, skip_contract_check = None, None, None
    for kw in stmt_expr.keywords:
        if kw.arg == "gas":
            gas = Expr.parse_value_expr(kw.value, context)
        elif kw.arg == "value":
            value = Expr.parse_value_expr(kw.value, context)
        elif kw.arg == "skip_contract_check":
            skip_contract_check = kw.value.value
            assert isinstance(skip_contract_check,
                              bool), "type checker missed this"
        else:
            raise TypeCheckFailure("Unexpected keyword argument")

    # TODO maybe return a small dataclass to reduce verbosity
    return value, gas, skip_contract_check
Example #22
0
    def parse_BoolOp(self):
        for value in self.expr.values:
            # Check for boolean operations with non-boolean inputs
            _expr = Expr.parse_value_expr(value, self.context)
            if not is_base_type(_expr.typ, "bool"):
                return

        def _build_if_lll(condition, true, false):
            # generate a basic if statement in LLL
            o = ["if", condition, true, false]
            return o

        if isinstance(self.expr.op, vy_ast.And):
            # create the initial `x and y` from the final two values
            lll_node = _build_if_lll(
                Expr.parse_value_expr(self.expr.values[-2], self.context),
                Expr.parse_value_expr(self.expr.values[-1], self.context),
                [0],
            )
            # iterate backward through the remaining values
            for node in self.expr.values[-3::-1]:
                lll_node = _build_if_lll(
                    Expr.parse_value_expr(node, self.context), lll_node, [0])

        elif isinstance(self.expr.op, vy_ast.Or):
            # create the initial `x or y` from the final two values
            lll_node = _build_if_lll(
                Expr.parse_value_expr(self.expr.values[-2], self.context),
                [1],
                Expr.parse_value_expr(self.expr.values[-1], self.context),
            )

            # iterate backward through the remaining values
            for node in self.expr.values[-3::-1]:
                lll_node = _build_if_lll(
                    Expr.parse_value_expr(node, self.context), 1, lll_node)
        else:
            raise TypeCheckFailure(
                f"Unexpected boolean operator: {type(self.expr.op).__name__}")

        return LLLnode.from_list(lll_node, typ="bool")
Example #23
0
def _get_element_ptr_array(parent, key, array_bounds_check):

    assert isinstance(parent.typ, ArrayLike)

    if not is_integer_type(key.typ):
        raise TypeCheckFailure(f"{key.typ} used as array index")

    subtype = parent.typ.subtype

    if parent.value == "~empty":
        if array_bounds_check:
            # this case was previously missing a bounds check. codegen
            # is a bit complicated when bounds check is required, so
            # block it. there is no reason to index into a literal empty
            # array anyways!
            raise TypeCheckFailure("indexing into zero array not allowed")
        return IRnode.from_list("~empty", subtype)

    if parent.value == "multi":
        assert isinstance(key.value, int)
        return parent.args[key.value]

    ix = unwrap_location(key)

    if array_bounds_check:
        is_darray = isinstance(parent.typ, DArrayType)
        bound = get_dyn_array_count(parent) if is_darray else parent.typ.count
        # uclamplt works, even for signed ints. since two's-complement
        # is used, if the index is negative, (unsigned) LT will interpret
        # it as a very large number, larger than any practical value for
        # an array index, and the clamp will throw an error.
        # NOTE: there are optimization rules for this when ix or bound is literal
        ix = clamp("lt", ix, bound)

    if parent.encoding == Encoding.ABI:
        if parent.location == STORAGE:
            raise CompilerPanic("storage variables should not be abi encoded"
                                )  # pragma: notest

        member_abi_t = subtype.abi_type

        ofst = _mul(ix, member_abi_t.embedded_static_size())

        return _getelemptr_abi_helper(parent, subtype, ofst)

    if parent.location.word_addressable:
        element_size = subtype.storage_size_in_words
    elif parent.location.byte_addressable:
        element_size = subtype.memory_bytes_required
    else:
        raise CompilerPanic("unreachable")  # pragma: notest

    ofst = _mul(ix, element_size)

    if has_length_word(parent.typ):
        data_ptr = add_ofst(
            parent, parent.location.word_scale * DYNAMIC_ARRAY_OVERHEAD)
    else:
        data_ptr = parent

    return IRnode.from_list(add_ofst(data_ptr, ofst),
                            typ=subtype,
                            location=parent.location)
Example #24
0
 def _wrapped(*args, **kwargs):
     return_value = fn(*args, **kwargs)
     if return_value is None:
         raise TypeCheckFailure(f"{fn.__name__} did not return a value")
     return return_value
Example #25
0
 def FAIL():  # pragma: nocover
     raise TypeCheckFailure(
         f"assigning {right.typ} to {left.typ} {left} {right}")
Example #26
0
def calculate_largest_base(b: int, num_bits: int,
                           is_signed: bool) -> Tuple[int, int]:
    """
    For a given power `b`, compute the maximum base `a` that will not produce an
    overflow in the equation `a ** b`

    Arguments
    ---------
    b : int
        Power value for the equation `a ** b`
    num_bits : int
        The maximum number of bits that the resulting value must fit in
    is_signed : bool
        Is the operation being performed on signed integers?

    Returns
    -------
    Tuple[int, int]
        Smallest and largest possible values for `a` where the result
        does not overflow `num_bits`.

        Note that the lower and upper bounds are not always negatives of
        each other, due to lower/upper bounds for int_<value_bits> being
        slightly asymmetric.
    """
    if num_bits % 8:  # pragma: no cover
        raise CompilerPanic("Type is not a modulo of 8")

    if b in (0, 1):  # pragma: no cover
        raise CompilerPanic("Exponential operation is useless!")

    if b < 0:  # pragma: no cover
        raise TypeCheckFailure("Cannot calculate negative exponents")

    value_bits = num_bits - (1 if is_signed else 0)
    if b > value_bits:  # pragma: no cover
        raise TypeCheckFailure("Value is too large and will always throw")

    # CMC 2022-05-06 TODO we should be able to do this with algebra
    # instead of looping):
    # x ** b == 2**value_bits
    # b ln(x) == ln(2**value_bits)
    # ln(x) == ln(2**value_bits) / b
    # x == exp( ln(2**value_bits) / b)

    # Estimate (up to ~39 digits precision required)
    a = math.ceil(2**(decimal.Decimal(value_bits) / decimal.Decimal(b)))
    # Do a bit of iteration to ensure we have the exact number
    num_iterations = 0
    while (a + 1)**b < 2**value_bits:
        a += 1
        num_iterations += 1
        assert num_iterations < 10000
    while a**b >= 2**value_bits:
        a -= 1
        num_iterations += 1
        assert num_iterations < 10000

    if not is_signed:
        return 0, a

    if (a + 1)**b == (2**value_bits):
        # edge case: lower bound is slightly wider than upper bound
        return -(a + 1), a
    else:
        return -a, a