Trouble following Interval example for extending numba

Hello community!

I’m experimenting with creating my own Decimal class. Someone on the scientific Python discord recommended that I try to implement it by following the Interval Example already provided. I’ve taken a crack at implementing a basic type without any special methods for now.

However when I try to run it I get the exception:

No definition for lowering <class '__main__.Decimal'>(Literal[bool](True), Literal[str](123), Literal[str](123), Literal[int](1)) -> Decimal

But, to me at least, I feel as if I have already given it the definition. Am I missing something here?

Sorry if this seems simple, I’m very new to working with numba.

Thank you

Below is the full exception output and the code for my Decimal class

/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/utils.py:213: NumbaPendingDeprecationWarning: Code using Numba extension API maybe depending on 'old_style' error-capturing, which is deprecated and will be replaced by 'new_style' in a future release. See details at https://numba.readthedocs.io/en/latest/reference/deprecation.html#deprecation-of-old-style-numba-captured-errors
Exception origin:
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/base.py", line 558, in get_function
    raise NotImplementedError("No definition for lowering %s%s" % (key, sig))

  warnings.warn(msg,
Traceback (most recent call last):
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/errors.py", line 832, in new_error_context
    yield
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 285, in lower_block
    self.lower_inst(inst)
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 463, in lower_inst
    val = self.lower_assign(ty, inst)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 675, in lower_assign
    return self.lower_expr(ty, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 1211, in lower_expr
    res = self.lower_call(resty, expr)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 940, in lower_call
    res = self._lower_call_normal(fnty, expr, signature)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 1174, in _lower_call_normal
    impl = self.context.get_function(fnty, signature)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/base.py", line 556, in get_function
    return self.get_function(fn, sig, _firstcall=False)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/base.py", line 558, in get_function
    raise NotImplementedError("No definition for lowering %s%s" % (key, sig))
NotImplementedError: No definition for lowering <class '__main__.Decimal'>(Literal[bool](True), Literal[str](123), Literal[str](123), Literal[int](1)) -> Decimal

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kai/Projects/nb-decimal/nb_decimal/types.py", line 196, in <module>
    uses_decimal()
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/dispatcher.py", line 487, in _compile_for_args
    raise e
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/dispatcher.py", line 420, in _compile_for_args
    return_val = self.compile(tuple(argtypes))
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/dispatcher.py", line 965, in compile
    cres = self._compiler.compile(args, return_type)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/dispatcher.py", line 125, in compile
    status, retval = self._compile_cached(args, return_type)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/dispatcher.py", line 139, in _compile_cached
    retval = self._compile_core(args, return_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/dispatcher.py", line 152, in _compile_core
    cres = compiler.compile_extra(self.targetdescr.typing_context,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler.py", line 770, in compile_extra
    return pipeline.compile_extra(func)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler.py", line 461, in compile_extra
    return self._compile_bytecode()
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler.py", line 529, in _compile_bytecode
    return self._compile_core()
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler.py", line 508, in _compile_core
    raise e
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler.py", line 495, in _compile_core
    pm.run(self.state)
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler_machinery.py", line 368, in run
    raise patched_exception
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler_machinery.py", line 356, in run
    self._runPass(idx, pass_inst, state)
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler_machinery.py", line 311, in _runPass
    mutated |= check(pss.run_pass, internal_state)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/compiler_machinery.py", line 273, in check
    mangled = func(compiler_state)
              ^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/typed_passes.py", line 466, in run_pass
    lower.lower()
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 187, in lower
    self.lower_normal_function(self.fndesc)
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 241, in lower_normal_function
    entry_block_tail = self.lower_function_body()
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 271, in lower_function_body
    self.lower_block(block)
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/lowering.py", line 283, in lower_block
    with new_error_context('lowering "{inst}" at {loc}', inst=inst,
  File "/home/kai/.pyenv/versions/3.11.3/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/home/kai/.cache/pypoetry/virtualenvs/nb-decimal-6XUNvw28-py3.11/lib/python3.11/site-packages/numba/core/errors.py", line 846, in new_error_context
    raise newerr.with_traceback(tb)
numba.core.errors.LoweringError: Failed in nopython mode pipeline (step: native lowering)
No definition for lowering <class '__main__.Decimal'>(Literal[bool](True), Literal[str](123), Literal[str](123), Literal[int](1)) -> Decimal

File "types.py", line 193:
    def uses_decimal():
        d = Decimal(True, "123", "123", 1)
        ^

During: lowering "d = call $4load_global.0($const16.2, $const18.3, $const20.4, $const22.5, func=$4load_global.0, args=[Var($const16.2, types.py:193), Var($const18.3, types.py:193), Var($const20.4, types.py:193), Var($const22.5, types.py:193)], kws=(), vararg=None, varkwarg=None, target=None)" at /home/kai/Projects/nb-decimal/nb_decimal/types.py (193)
from contextlib import ExitStack

from numba import types
from numba.core import cgutils
from numba.extending import (
    models,
    register_model,
    typeof_impl,
    as_numba_type,
    type_callable,
    make_attribute_wrapper,
    lower_builtin,
    box,
    unbox,
    NativeValue,
)


class Decimal:

    def __init__(self, sign, base, exponent, special_code):
        self.sign = sign
        self.base = base
        self.exponent = exponent
        self.special_code = special_code


class DecimalType(types.Type):

    def __init__(self):
        super(DecimalType, self).__init__(name="Decimal")


decimal_type = DecimalType()


@typeof_impl.register(Decimal)
def typeof_index(val, c):
    return decimal_type


as_numba_type.register(Decimal, decimal_type)


@type_callable(Decimal)
def type_decimal(context):
    def typer(sign, base, exponent, special_code):
        sign_is_bool = isinstance(sign, types.Boolean)
        base_is_string = isinstance(base, types.StringLiteral)
        exponent_is_string = isinstance(exponent, types.StringLiteral)
        special_code_is_int = isinstance(special_code, types.Integer)

        if sign_is_bool and base_is_string and exponent_is_string and special_code_is_int:
            return decimal_type
    return typer


@register_model(DecimalType)
class DecimalModel(models.StructModel):

    def __init__(self, dmm, fe_type):
        members = [
            ("sign", types.boolean),
            ("base", types.string),
            ("exponent", types.string),
            ("special_code", types.int64),
        ]
        models.StructModel.__init__(self, dmm, fe_type, members)


make_attribute_wrapper(DecimalType, "sign", "sign")
make_attribute_wrapper(DecimalType, "base", "base")
make_attribute_wrapper(DecimalType, "exponent", "exponent")
make_attribute_wrapper(DecimalType, "special_code", "special_code")


@lower_builtin(Decimal, types.boolean, types.string, types.string, types.int64)
def impl_decimal(context, builder, sig, args):
    typ = sig.return_type
    sign, base, exponent, special_code = args
    decimal = cgutils.create_struct_proxy(typ)(context, builder)
    decimal.sign = sign
    decimal.base = base
    decimal.exponent = exponent
    decimal.special_code = special_code
    return decimal._getvalue()


@unbox(DecimalType)
def unbox_decimal(typ, obj, c):
    is_error_ptr = cgutils.alloca_once_value(c.builder, cgutils.false_bit)
    decimal = cgutils.create_struct_proxy(typ)(c.context, c.builder)

    with ExitStack() as stack:
        sign_obj = c.pyapi.object_getattr_string(obj, "sign")
        with cgutils.early_exit_if_null(c.builder, stack, sign_obj):
            c.builder.store(cgutils.true_bit, is_error_ptr)
        sign_native = c.unbox(types.boolean, sign_obj)
        c.pyapi.decref(sign_obj)
        with cgutils.early_exit_if(c.builder, stack, sign_native.is_error):
            c.builder.store(cgutils.true_bit, is_error_ptr)

        base_obj = c.pyapi.object_getattr_string(obj, "base")
        with cgutils.early_exit_if_null(c.builder, stack, base_obj):
            c.builder.store(cgutils.true_bit, is_error_ptr)
        base_native = c.unbox(types.string, base_obj)
        c.pyapi.decref(base_obj)
        with cgutils.early_exit_if(c.builder, stack, base_native.is_error):
            c.builder.store(cgutils.true_bit, is_error_ptr)

        exponent_obj = c.pyapi.object_getattr_string(obj, "exponent")
        with cgutils.early_exit_if_null(c.builder, stack, exponent_obj):
            c.builder.store(cgutils.true_bit, is_error_ptr)
        exponent_native = c.unbox(types.string, exponent_obj)
        c.pyapi.decref(exponent_obj)
        with cgutils.early_exit_if(c.builder, stack, exponent_native.is_error):
            c.builder.store(cgutils.true_bit, is_error_ptr)

        special_code_obj = c.pyapi.object_getattr_string(obj, "special_code")
        with cgutils.early_exit_if_null(c.builder, stack, special_code_obj):
            c.builder.store(cgutils.true_bit, is_error_ptr)
        special_code_native = c.unbox(types.int64, special_code_obj)
        c.pyapi.decref(special_code_obj)
        with cgutils.early_exit_if(c.builder, stack, special_code_native.is_error):
            c.builder.store(cgutils.true_bit, is_error_ptr)

        decimal.sign = sign_native.value
        decimal.base = base_native.value
        decimal.exponent = exponent_native.value
        decimal.special_code = special_code_native.value

        return NativeValue(decimal._getvalue(), is_error=c.builder.load(is_error_ptr))


@box(DecimalType)
def box_decimal(typ, val, c):
    ret_ptr = cgutils.alloca_once(c.builder, c.pyapi.pyobj)
    fail_obj = c.pyapi.get_null_object()

    with ExitStack as stack:
        decimal = cgutils.create_struct_proxy(typ)(c.context, c.builder, value=val)

        sign_obj = c.box(types.boolean, decimal.sign)
        with cgutils.early_exit_if_null(c.builder, stack, sign_obj):
            c.builder.store(fail_obj, ret_ptr)

        base_obj = c.box(types.string, decimal.base)
        with cgutils.early_exit_if_null(c.builder, stack, base_obj):
            c.pyapi.decref(sign_obj)
            c.builder.store(fail_obj, ret_ptr)

        exponent_obj = c.box(types.string, decimal.exponent)
        with cgutils.early_exit_if_null(c.builder, stack, exponent_obj):
            c.pyapi.decref(sign_obj)
            c.pyapi.decref(base_obj)
            c.builder.store(fail_obj, ret_ptr)

        special_code_obj = c.box(types.int64, decimal.special_code)
        with cgutils.early_exit_if_null(c.builder, stack, special_code_obj):
            c.pyapi.decref(sign_obj)
            c.pyapi.decref(base_obj)
            c.pyapi.decref(exponent_obj)
            c.builder.store(fail_obj, ret_ptr)

        class_obj = c.pyapi.unserialize(c.pyapi.serialize_object(Decimal))
        with cgutils.early_exit_if_null(c.builder, stack, class_obj):
            c.pyapi.decref(sign_obj)
            c.pyapi.decref(base_obj)
            c.pyapi.decref(exponent_obj)
            c.pyapi.decref(special_code_obj)
            c.builder.store(fail_obj, ret_ptr)

        # NOTE: The result of this call is not checked as the cleanup
        # has to occur regardless of whether it is successful. If it
        # fails `res` is set to NULL and a Python exception is set.
        res = c.pyapi.call_function_objargs(class_obj, (sign_obj, base_obj, exponent_obj, special_code_obj))
        c.pyapi.decref(sign_obj)
        c.pyapi.decref(base_obj)
        c.pyapi.decref(exponent_obj)
        c.pyapi.decref(special_code_obj)
        c.pyapi.decref(class_obj)
        c.builder.store(res, ret_ptr)

        return c.builder.load(ret_ptr)


if __name__ == "__main__":

    from numba import njit

    @njit
    def uses_decimal():
        d = Decimal(True, "123", "123", 1)
        print(d)

    uses_decimal()

Hi @Kai

When you implement the typing for Decimal your typer function rejects all candidate implementations. Your example works for example when you do:

@type_callable(Decimal)
def type_decimal(context):
    def typer(sign, base, exponent, special_code):
        if (
            isinstance(sign, types.Boolean)
            and isinstance(base, types.UnicodeType)
            and isinstance(exponent, types.UnicodeType)
            and isinstance(special_code, types.Integer)
        ):
            return decimal_type

    return typer

I don’t know what your exact goal is so update this as needed.

Once you fixed this you will notice that your boxing function also does not work. The first apparent problem is that you need to need to create an ExitStack instance because the class itself is not a context manager. Then there are some more issues with the code generation which I did not try to fix.

I hope this helps you a bit.

Thank you! That’s made some progress. I just didn’t know that types.StringLiteral wasn’t related to a Python string.

Then end goal is to make a numba compatible Decimal class for financial calculations.