Notes on Literal Types

Note

This document describes an advanced feature designed to overcome some limitations of the compilation mechanism relating to types.

Some features need to specialize based on the literal value during compilation to produce type stable code necessary for successful compilation in Numba. This can be achieved by propagating the literal value through the type system. Numba recognizes inline literal values as numba.types.Literal. For example:

def foo(x):
    a = 123
    return bar(x, a)

Numba will infer the type of a as Literal[int](123). The definition of bar() can subsequently specialize its implementation knowing that the second argument is an int with the value 123.

Literal Type

Classes and methods related to the Literal type.

class numba.types.Literal(*args, **kwargs)

Base class for Literal types. Literal types contain the original Python value in the type.

A literal type should always be constructed from the literal(val) function.

numba.types.literal(value)

Returns a Literal instance or raise LiteralTypingError

numba.types.unliteral(lit_type)

Get base type from Literal type.

numba.types.maybe_literal(value)

Get a Literal type for the value or None.

Specifying for Literal Typing

To specify a value as a Literal type in code scheduled for JIT compilation, use the following function:

numba.literally(obj)

Forces Numba to interpret obj as an Literal value.

obj must be either a literal or an argument of the caller function, where the argument must be bound to a literal. The literal requirement propagates up the call stack.

This function is intercepted by the compiler to alter the compilation behavior to wrap the corresponding function parameters as Literal. It has no effect outside of nopython-mode (interpreter, and objectmode).

The current implementation detects literal arguments in two ways:

  1. Scans for uses of literally via a compiler pass.

  2. literally is overloaded to raise numba.errors.ForceLiteralArg to signal the dispatcher to treat the corresponding parameter differently. This mode is to support indirect use (via a function call).

The execution semantic of this function is equivalent to an identity function.

See numba/tests/test_literal_dispatch.py for examples.

Code Example

from test_literally_usage of numba/tests/doc_examples/test_literally_usage.py
 1        import numba
 2
 3        def power(x, n):
 4            raise NotImplementedError
 5
 6        @numba.extending.overload(power)
 7        def ov_power(x, n):
 8            if isinstance(n, numba.types.Literal):
 9                # only if `n` is a literal
10                if n.literal_value == 2:
11                    # special case: square
12                    print("square")
13                    return lambda x, n: x * x
14                elif n.literal_value == 3:
15                    # special case: cubic
16                    print("cubic")
17                    return lambda x, n: x * x * x
18            else:
19                # If `n` is not literal, request literal dispatch
20                return lambda x, n: numba.literally(n)
21
22            print("generic")
23            return lambda x, n: x ** n
24
25        @numba.njit
26        def test_power(x, n):
27            return power(x, n)
28
29        # should print "square" and "9"
30        print(test_power(3, 2))
31
32        # should print "cubic" and "27"
33        print(test_power(3, 3))
34
35        # should print "generic" and "81"
36        print(test_power(3, 4))
37

Internal Details

Internally, the compiler raises a ForceLiteralArgs exception to signal the dispatcher to wrap specified arguments using the Literal type.

class numba.errors.ForceLiteralArg(arg_indices, fold_arguments=None, loc=None)

A Pseudo-exception to signal the dispatcher to type an argument literally

Attributes
requested_argsfrozenset[int]

requested positions of the arguments.

__init__(arg_indices, fold_arguments=None, loc=None)
Parameters
arg_indicesSequence[int]

requested positions of the arguments.

fold_arguments: callable

A function (tuple, dict) -> tuple that binds and flattens the args and kwargs.

locnumba.ir.Loc or None
__or__(other)

Same as self.combine(other)

combine(other)

Returns a new instance by or’ing the requested_args.

Inside Extensions

@overload extensions can use literally inside the implementation body like in normal jit-code.

Explicit handling of literal requirements is possible through use of the following:

class numba.extending.SentryLiteralArgs(literal_args)
Parameters
literal_argsSequence[str]

A sequence of names for literal arguments

Examples

The following line:

>>> SentryLiteralArgs(literal_args).for_pysig(pysig).bind(*args, **kwargs)

is equivalent to:

>>> sentry_literal_args(pysig, literal_args, args, kwargs)
for_function(func)

Bind the sentry to the signature of func.

Parameters
funcFunction

A python function.

Returns
objBoundLiteralArgs
for_pysig(pysig)

Bind the sentry to the given signature pysig.

Parameters
pysiginspect.Signature
Returns
objBoundLiteralArgs
class numba.extending.BoundLiteralArgs(pysig, literal_args)

This class is usually created by SentryLiteralArgs.

bind(*args, **kwargs)

Bind to argument types.

numba.extending.sentry_literal_args(pysig, literal_args, args, kwargs)

Ensures that the given argument types (in args and kwargs) are literally typed for a function with the python signature pysig and the list of literal argument names in literal_args.

Alternatively, this is the same as:

SentryLiteralArgs(literal_args).for_pysig(pysig).bind(*args, **kwargs)